diff --git a/.ecs/.header b/.ecs/.header new file mode 100644 index 0000000..404bcb7 --- /dev/null +++ b/.ecs/.header @@ -0,0 +1,6 @@ +This file is part of Chevereto. + +(c) Rodolfo Berrios + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. \ No newline at end of file diff --git a/.ecs/ecs-chevere.php b/.ecs/ecs-chevere.php new file mode 100644 index 0000000..85ee257 --- /dev/null +++ b/.ecs/ecs-chevere.php @@ -0,0 +1,212 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer; +use PhpCsFixer\Fixer\ArrayNotation\NoWhitespaceBeforeCommaInArrayFixer; +use PhpCsFixer\Fixer\ArrayNotation\WhitespaceAfterCommaInArrayFixer; +use PhpCsFixer\Fixer\Basic\BracesFixer; +use PhpCsFixer\Fixer\Basic\EncodingFixer; +use PhpCsFixer\Fixer\Casing\ConstantCaseFixer; +use PhpCsFixer\Fixer\Casing\LowercaseKeywordsFixer; +use PhpCsFixer\Fixer\CastNotation\LowercaseCastFixer; +use PhpCsFixer\Fixer\CastNotation\ShortScalarCastFixer; +use PhpCsFixer\Fixer\ClassNotation\ClassAttributesSeparationFixer; +use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer; +use PhpCsFixer\Fixer\ClassNotation\NoBlankLinesAfterClassOpeningFixer; +use PhpCsFixer\Fixer\ClassNotation\ProtectedToPrivateFixer; +use PhpCsFixer\Fixer\ClassNotation\SingleClassElementPerStatementFixer; +use PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer; +use PhpCsFixer\Fixer\Comment\HeaderCommentFixer; +use PhpCsFixer\Fixer\Comment\NoTrailingWhitespaceInCommentFixer; +use PhpCsFixer\Fixer\Comment\SingleLineCommentStyleFixer; +use PhpCsFixer\Fixer\ControlStructure\ElseifFixer; +use PhpCsFixer\Fixer\ControlStructure\IncludeFixer; +use PhpCsFixer\Fixer\ControlStructure\NoBreakCommentFixer; +use PhpCsFixer\Fixer\ControlStructure\NoUnneededControlParenthesesFixer; +use PhpCsFixer\Fixer\ControlStructure\NoUnneededCurlyBracesFixer; +use PhpCsFixer\Fixer\ControlStructure\SwitchCaseSemicolonToColonFixer; +use PhpCsFixer\Fixer\ControlStructure\SwitchCaseSpaceFixer; +use PhpCsFixer\Fixer\FunctionNotation\FunctionDeclarationFixer; +use PhpCsFixer\Fixer\FunctionNotation\MethodArgumentSpaceFixer; +use PhpCsFixer\Fixer\FunctionNotation\NoSpacesAfterFunctionNameFixer; +use PhpCsFixer\Fixer\FunctionNotation\ReturnTypeDeclarationFixer; +use PhpCsFixer\Fixer\Import\NoLeadingImportSlashFixer; +use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer; +use PhpCsFixer\Fixer\Import\OrderedImportsFixer; +use PhpCsFixer\Fixer\Import\SingleImportPerStatementFixer; +use PhpCsFixer\Fixer\Import\SingleLineAfterImportsFixer; +use PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveUnsetsFixer; +use PhpCsFixer\Fixer\LanguageConstruct\DeclareEqualNormalizeFixer; +use PhpCsFixer\Fixer\NamespaceNotation\BlankLineAfterNamespaceFixer; +use PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer; +use PhpCsFixer\Fixer\Operator\ConcatSpaceFixer; +use PhpCsFixer\Fixer\Operator\NewWithBracesFixer; +use PhpCsFixer\Fixer\Operator\ObjectOperatorWithoutWhitespaceFixer; +use PhpCsFixer\Fixer\Operator\TernaryOperatorSpacesFixer; +use PhpCsFixer\Fixer\Operator\UnaryOperatorSpacesFixer; +use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer; +use PhpCsFixer\Fixer\PhpTag\FullOpeningTagFixer; +use PhpCsFixer\Fixer\PhpTag\NoClosingTagFixer; +use PhpCsFixer\Fixer\ReturnNotation\ReturnAssignmentFixer; +use PhpCsFixer\Fixer\Semicolon\MultilineWhitespaceBeforeSemicolonsFixer; +use PhpCsFixer\Fixer\Semicolon\NoEmptyStatementFixer; +use PhpCsFixer\Fixer\Semicolon\NoSinglelineWhitespaceBeforeSemicolonsFixer; +use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer; +use PhpCsFixer\Fixer\Whitespace\BlankLineBeforeStatementFixer; +use PhpCsFixer\Fixer\Whitespace\CompactNullableTypehintFixer; +use PhpCsFixer\Fixer\Whitespace\IndentationTypeFixer; +use PhpCsFixer\Fixer\Whitespace\LineEndingFixer; +use PhpCsFixer\Fixer\Whitespace\NoExtraBlankLinesFixer; +use PhpCsFixer\Fixer\Whitespace\NoSpacesInsideParenthesisFixer; +use PhpCsFixer\Fixer\Whitespace\NoTrailingWhitespaceFixer; +use PhpCsFixer\Fixer\Whitespace\SingleBlankLineAtEofFixer; +use SlevomatCodingStandard\Sniffs\ControlStructures\RequireShortTernaryOperatorSniff; +use SlevomatCodingStandard\Sniffs\Functions\UnusedInheritedVariablePassedToClosureSniff; +use SlevomatCodingStandard\Sniffs\Operators\RequireCombinedAssignmentOperatorSniff; +use SlevomatCodingStandard\Sniffs\PHP\DisallowDirectMagicInvokeCallSniff; +use SlevomatCodingStandard\Sniffs\PHP\UselessParenthesesSniff; +use SlevomatCodingStandard\Sniffs\PHP\UselessSemicolonSniff; +use SlevomatCodingStandard\Sniffs\Variables\UnusedVariableSniff; +use SlevomatCodingStandard\Sniffs\Variables\UselessVariableSniff; +use Symplify\CodingStandard\Fixer\Commenting\ParamReturnAndVarTagMalformsFixer; +use Symplify\EasyCodingStandard\Config\ECSConfig; +use Symplify\EasyCodingStandard\ValueObject\Option; +use Symplify\EasyCodingStandard\ValueObject\Set\SetList; + +return static function (ECSConfig $containerConfigurator): void { + $headerFile = __DIR__ . '/.header'; + $parameters = $containerConfigurator->parameters(); + $parameters->set(Option::SETS, [ + SetList::COMMON, + ]); + $services = $containerConfigurator->services(); + if (file_exists($headerFile)) { + $services->set(HeaderCommentFixer::class) + ->call('configure', [[ + 'header' => file_get_contents($headerFile), + 'location' => 'after_open', + ]]); + } + $services->set(EncodingFixer::class); + $services->set(FullOpeningTagFixer::class); + $services->set(BlankLineAfterNamespaceFixer::class); + $services->set(BracesFixer::class); + $services->set(ClassDefinitionFixer::class); + $services->set(ConstantCaseFixer::class); + $services->set(ElseifFixer::class); + $services->set(FunctionDeclarationFixer::class); + $services->set(IndentationTypeFixer::class); + $services->set(LineEndingFixer::class); + $services->set(LowercaseKeywordsFixer::class); + $services->set(MethodArgumentSpaceFixer::class) + ->call('configure', [[ + 'on_multiline' => 'ensure_fully_multiline', + ]]); + $services->set(NoBreakCommentFixer::class); + $services->set(NoClosingTagFixer::class); + $services->set(NoSpacesAfterFunctionNameFixer::class); + $services->set(NoSpacesInsideParenthesisFixer::class); + $services->set(NoTrailingWhitespaceFixer::class); + $services->set(NoTrailingWhitespaceInCommentFixer::class); + $services->set(SingleBlankLineAtEofFixer::class); + $services->set(SingleClassElementPerStatementFixer::class) + ->call('configure', [[ + 'elements' => ['property'], + ]]); + $services->set(SingleImportPerStatementFixer::class); + $services->set(SingleLineAfterImportsFixer::class); + // $services->set(SwitchCaseSemicolonToColonFixer::class); broken for php 8.0 + $services->set(SwitchCaseSpaceFixer::class); + $services->set(VisibilityRequiredFixer::class); + $services->set(LowercaseCastFixer::class); + $services->set(ShortScalarCastFixer::class); + $services->set(BlankLineAfterOpeningTagFixer::class); + $services->set(NoLeadingImportSlashFixer::class); + $services->set(OrderedImportsFixer::class) + ->call('configure', [[ + 'importsOrder' => ['class', 'function', 'const'], + ]]); + $services->set(DeclareEqualNormalizeFixer::class) + ->call('configure', [[ + 'space' => 'none', + ]]); + $services->set(NewWithBracesFixer::class); + $services->set(BracesFixer::class) + ->call('configure', [[ + 'allow_single_line_closure' => false, + 'position_after_functions_and_oop_constructs' => 'next', + 'position_after_control_structures' => 'same', + 'position_after_anonymous_constructs' => 'same', + ]]); + $services->set(NoBlankLinesAfterClassOpeningFixer::class); + $services->set(VisibilityRequiredFixer::class) + ->call('configure', [[ + 'elements' => ['const', 'method', 'property'], + ]]); + $services->set(BinaryOperatorSpacesFixer::class); + $services->set(TernaryOperatorSpacesFixer::class); + $services->set(UnaryOperatorSpacesFixer::class); + $services->set(ReturnTypeDeclarationFixer::class); + $services->set(NoTrailingWhitespaceFixer::class); + $services->set(ConcatSpaceFixer::class) + ->call('configure', [[ + 'spacing' => 'one', + ]]); + $services->set(NoSinglelineWhitespaceBeforeSemicolonsFixer::class); + $services->set(NoWhitespaceBeforeCommaInArrayFixer::class); + $services->set(WhitespaceAfterCommaInArrayFixer::class); + $services->set(DeclareStrictTypesFixer::class); + $services->set(CompactNullableTypehintFixer::class); + $services->set(BlankLineBeforeStatementFixer::class); + $services->set(CombineConsecutiveUnsetsFixer::class); + $services->set(ClassAttributesSeparationFixer::class); + $services->set(MultilineWhitespaceBeforeSemicolonsFixer::class); + $services->set(SingleLineCommentStyleFixer::class); + $services->set(IncludeFixer::class); + $services->set(ObjectOperatorWithoutWhitespaceFixer::class); + $services->set(DisallowDirectMagicInvokeCallSniff::class); + $services->set(ParamReturnAndVarTagMalformsFixer::class); + $services->set(UnusedVariableSniff::class); + $services->set(UselessVariableSniff::class); + $services->set(UnusedInheritedVariablePassedToClosureSniff::class); + $services->set(UselessSemicolonSniff::class); + // $services->set(UselessParenthesesSniff::class); // broken for php 8.0 + $services->set(ArraySyntaxFixer::class) + ->call('configure', [[ + 'syntax' => 'short', + ]]); + $services->set(NoUnusedImportsFixer::class); + $services->set(OrderedImportsFixer::class); + $services->set(NoEmptyStatementFixer::class); + $services->set(ProtectedToPrivateFixer::class); + $services->set(NoUnneededControlParenthesesFixer::class); + $services->set(NoUnneededCurlyBracesFixer::class); + $services->set(ReturnAssignmentFixer::class); + $services->set(RequireShortTernaryOperatorSniff::class); + $services->set(RequireCombinedAssignmentOperatorSniff::class); + $services->set(NoExtraBlankLinesFixer::class) + ->call('configure', [[ + 'tokens' => [ + 'curly_brace_block', + 'extra', + 'parenthesis_brace_block', + 'square_brace_block', + 'throw', + 'use', + ] + ]]); + $parameters = $containerConfigurator->parameters(); + $parameters->set(Option::SKIP, [ + SingleImportPerStatementFixer::class => null, + ]); +}; diff --git a/.ecs/ecs.php b/.ecs/ecs.php new file mode 100644 index 0000000..6974065 --- /dev/null +++ b/.ecs/ecs.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer; +use Symplify\EasyCodingStandard\Config\ECSConfig; +use Symplify\EasyCodingStandard\ValueObject\Option; + +return static function (ECSConfig $containerConfigurator): void { + $containerConfigurator->import(__DIR__ . '/ecs-chevere.php'); + $services = $containerConfigurator->services(); + $services->remove(DeclareStrictTypesFixer::class); + $parameters = $containerConfigurator->parameters(); + $parameters->set(Option::SKIP, []); +}; diff --git a/.github/banner/chevereto-ultimate.png b/.github/banner/chevereto-ultimate.png new file mode 100644 index 0000000..24de8e8 Binary files /dev/null and b/.github/banner/chevereto-ultimate.png differ diff --git a/.github/screen/user-profile.jpeg b/.github/screen/user-profile.jpeg new file mode 100644 index 0000000..0a36ea9 Binary files /dev/null and b/.github/screen/user-profile.jpeg differ diff --git a/.github/test.yml b/.github/test.yml new file mode 100644 index 0000000..021e2e2 --- /dev/null +++ b/.github/test.yml @@ -0,0 +1,71 @@ +name: Test + +on: + push: + branches: + - "**" + tags-ignore: + - "**" + +jobs: + build: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-22.04] + php-versions: ["8.0", "8.1"] + env: + extensions: pcov, imagick + tools: composer + ini-values: precision=16, default_charset='UTF-8', pcov.directory=src + key: cache-1626460452817 + name: Test on PHP ${{ matrix.php-versions }} ${{ matrix.operating-system }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup cache environment + if: ${{ !env.ACT }} + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + key: ${{ env.key }} + - name: Cache extensions + if: ${{ !env.ACT }} + uses: actions/cache@v3 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + ini-values: ${{ env.ini-values }} + coverage: pcov + tools: ${{ env.tools }} + env: + fail-fast: true + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Validate composer + run: composer validate + working-directory: app + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --no-progress --ignore-platform-reqs + working-directory: app + - name: Run tests with phpunit + run: app/vendor/bin/phpunit -c app/phpunit-report.xml --coverage-clover=app/build/coverage/clover.xml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..024f4ec --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,102 @@ +name: Docker + +on: + push: + tags: + - "*" + +env: + GHCR_SLUG: ghcr.io/${{ github.repository }} + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + php: ["8.1"] + name: Build on PHP ${{ matrix.php }} ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get current branch + id: get-branch + run: | + raw=$(git branch -r --contains ${{ github.ref }}) + echo "branch=${raw##*/}" >> $GITHUB_OUTPUT + - run: echo ${{ steps.get-branch.outputs.branch }} + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.GHCR_SLUG }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + labels: | + org.opencontainers.image.title=Chevereto V4 + org.opencontainers.image.description=Ultimate image sharing software 🦄 + org.opencontainers.image.vendor=Chevereto + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build dependencies + run: composer install --no-progress --ignore-platform-reqs + working-directory: app + - name: Build Chevereto + run: | + app/bin/legacy -C langs + app/bin/legacy -C htaccess-checksum + app/bin/legacy -C htaccess-enforce + - name: Packaging + run: | + rm -rf .git .gitignore .github .ecs .vscode + rm -rf README.md chevereto.svg rector.php + rm -rf app/tests app/phpunit*.xml + rm -rf app/.editions app/bin/edition + mkdir importing/{parse-users,parse-albums,no-parse} + mv .package .. + ls -la ../.package + - name: Checkout chevereto/docker + uses: actions/checkout@v3 + with: + repository: chevereto/docker + path: docker + ref: ${{ steps.get-branch.outputs.branch }} + - run: | + mv docker ../docker + mkdir -p ../docker/chevereto + ls -la ../docker + - name: Copy to docker folder + run: | + cp -a ./. ../docker/chevereto/ + ls -la ../docker/chevereto + - name: Build + uses: docker/bake-action@v2 + with: + workdir: ../docker + set: build.args.PHP=${{ matrix.php }} + files: | + ./docker-bake.hcl + ${{ steps.meta.outputs.bake-file }} + targets: build + push: true + - name: Check manifest + run: | + docker buildx imagetools inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }} + - name: Inspect image + run: | + docker pull ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }} + docker image inspect ${{ env.GHCR_SLUG }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3d8bbc3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: Release + +on: + push: + tags: + - "*" + +jobs: + build: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest] + php-versions: ["8.1"] + env: + tools: composer + ini-values: default_charset='UTF-8' + key: cache-1633608016315 + name: Release on PHP ${{ matrix.php-versions }} ${{ matrix.operating-system }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + ini-values: ${{ env.ini-values }} + tools: ${{ env.tools }} + env: + fail-fast: true + - name: Validate composer + run: composer validate + working-directory: app + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + working-directory: app + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Build dependencies + run: composer install --no-progress --ignore-platform-reqs + working-directory: app + - name: Build Chevereto + run: | + app/bin/legacy -C langs + app/bin/legacy -C htaccess-checksum + app/bin/legacy -C htaccess-enforce + - name: Packaging + run: | + rm -rf .git .gitignore .github .ecs .vscode + rm -rf README.md chevereto.svg rector.php + rm -rf app/tests app/phpunit*.xml + rm -rf app/.editions app/bin/edition + mkdir importing/{parse-users,parse-albums,no-parse} + mv .package .. + ls -la ../.package + - name: Archive lite + uses: thedoctor0/zip-release@master + with: + directory: "." + type: "zip" + filename: "${{ github.ref_name}}-lite.zip" + exclusions: "/*app/vendor/*" + - name: Archive release + uses: thedoctor0/zip-release@master + with: + directory: "." + type: "zip" + filename: "${{ github.ref_name}}.zip" + exclusions: "${{ github.ref_name}}-lite.zip" + - name: Upload artifacts + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + removeArtifacts: true + bodyFile: "../.package/${{ github.ref_name}}.txt" + artifacts: > + ../.package/${{ github.ref_name}}.txt, + ${{ github.ref_name}}.zip, + ${{ github.ref_name}}-lite.zip + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd2bbba --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +/.env +/.idea +/app/vendor +/app/build +/app/.phpunit.cache +/app/.phpunit.result.cache +/importing/** +/images/** +!/images/.htaccess +!/importing/.htaccess +/_assets diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..bd7cf45 --- /dev/null +++ b/.htaccess @@ -0,0 +1,31 @@ +ServerSignature Off +Options -Indexes +Options -MultiViews +# CORS header (avoids font rendering issues)(replace dev\.local with your domain\.com) +# SetEnvIf Origin ^(https?://.+\.dev\.local(?::\d{1,5})?)$ CORS_ALLOW_ORIGIN=$1 +# Header append Access-Control-Allow-Origin %{CORS_ALLOW_ORIGIN}e env=CORS_ALLOW_ORIGIN +# Header merge Vary "Origin" + + Require all denied + + + RewriteEngine On + # If you have problems with the rewrite rules remove the "#" from the following RewriteBase line + # You will also have to change the path to reflect the path to your Chevereto installation + # If you are using mod alias is likely that you will need this. + #RewriteBase / + + # Image not found replacement + RewriteCond %{REQUEST_FILENAME} !-f + #RewriteRule images/.+\.(gif|jpe?g|a?png|bmp|webp) content/images/system/default/404.gif [NC,L] + RewriteRule images/.+\.(gif|jpe?g|png|bmp|webp) - [NC,L,R=404] + + # PHP front controller + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . index.php [L] + + # Single PHP-entrypoint + RewriteCond %{THE_REQUEST} ^.+?\ [^?]+\.php[?\ ] [NC] + RewriteRule \.php$ - [NC,L,F,R=404] + \ No newline at end of file diff --git a/.package/4.0.5.txt b/.package/4.0.5.txt new file mode 100644 index 0000000..6ad001b --- /dev/null +++ b/.package/4.0.5.txt @@ -0,0 +1,11 @@ +Chevereto 4.0.5 (2022-11-30) + +✅ Added more environment variables +✅ Renamed dashboard/settings/api to dashboard/settings/guest-api +✅ Renamed website modes (community, personal) +🐞 Fixed bug with Exif metadata removal +🐞 Fixed bug with single image redirect +🐞 Fixed bug with selectable items on iPad +🐞 Fixed bug with login page +🐞 Fixed bug with missing timestamp on upload +🐞 Fixed bug with ModerateContent diff --git a/.package/README.txt b/.package/README.txt new file mode 100755 index 0000000..894592d --- /dev/null +++ b/.package/README.txt @@ -0,0 +1,20 @@ + __ __ + ____/ / ___ _ _____ _______ / /____ +/ __/ _ \/ -_) |/ / -_) __/ -_) __/ _ \ +\__/_//_/\__/|___/\__/_/ \__/\__/\___/ + + https://chevereto.com/ + +This package is another quality release +made by Chevereto Software. We ship +indie software from Concepcion, Chile +to the rest of the world. + +Many thanks for using our software and +trusting our work. This software exists +thanks to your ongoing support. + + ~ + + GRACIAS + (Thank you!) diff --git a/.tinkerwell/CustomTinkerwellDriver.php b/.tinkerwell/CustomTinkerwellDriver.php new file mode 100644 index 0000000..312705f --- /dev/null +++ b/.tinkerwell/CustomTinkerwellDriver.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function Chevereto\Legacy\loaderHandler; + +class CustomTinkerwellDriver extends TinkerwellDriver +{ + public function canBootstrap($projectPath) + { + return file_exists($projectPath . '/app/legacy/load/loader.php'); + } + + public function bootstrap($projectPath) + { + define('ACCESS', 'web'); + define('REPL', true); + require $projectPath . '/app/legacy/load/loader.php'; + include loaderHandler( + _cookie: [], + _env: $_ENV, + _files: [], + _get: [], + _post: [], + _request: [], + _server: [], + _session: [ + 'G_auth_token' => str_repeat('a', 40), + ], + ); + } + + public function contextMenu() + { + return [ + Label::create('Detected Chevereto v4'), + OpenURL::create('Chevereto Docs', 'https://v4-docs.chevereto.com/'), + ]; + } +} diff --git a/.vscode/action.code-snippets b/.vscode/action.code-snippets new file mode 100644 index 0000000..5618625 --- /dev/null +++ b/.vscode/action.code-snippets @@ -0,0 +1,37 @@ +{ + "action": { + "description": "Create an action", + "prefix": "action", + "body": [ + "getResponse(key: 'value',);", + "\t}", + "}" + ] + } +} \ No newline at end of file diff --git a/.vscode/class.code-snippets b/.vscode/class.code-snippets new file mode 100644 index 0000000..ca46ed4 --- /dev/null +++ b/.vscode/class.code-snippets @@ -0,0 +1,46 @@ +{ + "interface": { + "description": "Create a interface", + "prefix": "interface", + "body": [ + " \\$resource,", + "]", + "```", + "", + "V3 `POST json/?action=${4:action}`", + "", + "* [json](../../../3.20.10/app/routes/route.json.php#${5:line})", + "", + "| Parameter | Values |", + "| --------- | ------ |", + "| | `` |", + "", + "`OK Response`", + "", + "```php", + "[", + " 'status_code' => 200,", + " 'success' => ['message' => 'message', 'code' => 200],", + " 'resource' => $resource,", + "]", + "```", + "", + ] + }, +} \ No newline at end of file diff --git a/.vscode/docblock.code-snippets b/.vscode/docblock.code-snippets new file mode 100644 index 0000000..b71d999 --- /dev/null +++ b/.vscode/docblock.code-snippets @@ -0,0 +1,62 @@ +{ + "docblock-instance": { + "description": "Insert a docblock instance", + "prefix": "docblock-instance", + "body": [ + "/**", + " * Provides access to the ${1:name} instance.", + " */" + ] + }, + "docblock-immutable": { + "description": "Insert a docblock immutable", + "prefix": "docblock-immutable", + "body": [ + "/**", + " * Return an instance with the specified ${1:name}.", + " *", + " * This method MUST retain the state of the current instance, and return", + " * an instance that contains the specified ${1:name}.", + " */" + ] + }, + "docblock-boolean": { + "description": "Insert a docblock boolean", + "prefix": "docblock-boolean", + "body": [ + "/**", + " * Indicates whether the instance has ${1:name}.", + " */" + ] + }, + "docblock-interface": { + "description": "Insert a docblock interface", + "prefix": "docblock-interface", + "body": [ + "/**", + " * Describes the component in charge of ${1:doing}.", + " */" + ] + }, + "coverage-ignore": { + "description": "Insert a code coverage ignore tag", + "prefix": "cov-ignore", + "body": [ + "@codeCoverageIgnore" + ] + }, + "coverage-ignore-start": { + "description": "Insert a code coverage ignore start tag", + "prefix": "cov-ignore-start", + "body": [ + "@codeCoverageIgnoreStart" + ] + }, + "coverage-ignore-end": { + "description": "Insert a code coverage ignore end tag", + "prefix": "cov-ignore-end", + "body": [ + "@codeCoverageIgnoreEnd" + ] + }, +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c1ce077 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "runOnSave.commands": [ + { + "match": "\\.php$", + "runIn": "backend", + "command": "cd ${workspaceFolder} && app/vendor/bin/ecs --config='.ecs/ecs.php' check ${file} --fix", + "workingDirectoryAsCWD": true, + "runningStatusMessage": "ECS ${fileBasename}", + "finishStatusMessage": "${fileBasename} OK" + }, + ] +} \ No newline at end of file diff --git a/.vscode/templates.code-snippets b/.vscode/templates.code-snippets new file mode 100644 index 0000000..e8a7032 --- /dev/null +++ b/.vscode/templates.code-snippets @@ -0,0 +1,34 @@ +{ + "with-method": { + "description": "Chevereto with method", + "prefix": "with", + "body": [ + "private ${2|int,string,bool,array,object,resource|} \\$${3:argument};", + "", + "public function with${1:Name}(${2|int,string,bool,array,object,resource|} \\$${3:argument}): ${4:self}", + "{", + "\t\\$new = clone \\$this;", + "\t\\$new->${3:argument} = \\$${3:argument};", + "", + "\treturn \\$new;", + "}" + ] + }, + "exception": { + "description": "Chevereto exception", + "prefix": "exception", + "body": [ + " 🔔 [Subscribe](https://chv.to/newsletter) to don't miss any update regarding Chevereto. + +

+ Chevereto +

+ +![CHUISS](.github/banner/chevereto-ultimate.png) + +[![Community](https://img.shields.io/badge/chv.to-community-blue?style=flat-square)](https://chv.to/community) +[![AGPL-3.0-only](https://img.shields.io/github/license/chevereto/chevereto?style=flat-square)](LICENSE) + +Chevereto enables to create an image sharing website on your own server. It's your hosting and your rules, say goodbye to closures and restrictions. + +![screen](.github/screen/user-profile.jpeg) + +## Documentation + +We provide several layers of documentation covering all aspects of our software. Chevereto documentation is Open Source and your contribution is highly appreciated. + +* Software [v4-docs.chevereto.com](https://v4-docs.chevereto.com) +* Admin [v4-admin.chevereto.com](https://v4-admin.chevereto.com) +* User [v4-user.chevereto.com](https://v4-admin.chevereto.com) + +## License + +### Open Source license + +Copyright [Rodolfo Berríos Arce](http://rodolfoberrios.com) - [AGPLv3](LICENSE). + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/. + +### Commercial license + +The commercial license is designed to for you to use Chevereto in commercial products and applications, without the provisions of the AGPLv3. With the commercial license, your code is kept proprietary, to yourself. See the Chevereto Commercial License at [chevereto.com](https://chevereto.com/license) diff --git a/app/.cache/languages/_locales.php b/app/.cache/languages/_locales.php new file mode 100644 index 0000000..58fd606 --- /dev/null +++ b/app/.cache/languages/_locales.php @@ -0,0 +1,282 @@ + + array ( + 'code' => 'ar', + 'dir' => 'rtl', + 'name' => 'العربية', + 'base' => 'ar', + 'short_name' => 'AR', + ), + 'bg-BG' => + array ( + 'code' => 'bg-BG', + 'dir' => 'ltr', + 'name' => 'Български', + 'base' => 'bg', + 'short_name' => 'BG (BG)', + ), + 'cs' => + array ( + 'code' => 'cs', + 'dir' => 'ltr', + 'name' => 'Čeština', + 'base' => 'cs', + 'short_name' => 'CS', + ), + 'da' => + array ( + 'code' => 'da', + 'dir' => 'ltr', + 'name' => 'Dansk', + 'base' => 'da', + 'short_name' => 'DA', + ), + 'de' => + array ( + 'code' => 'de', + 'dir' => 'ltr', + 'name' => 'Deutsch', + 'base' => 'de', + 'short_name' => 'DE', + ), + 'el' => + array ( + 'code' => 'el', + 'dir' => 'ltr', + 'name' => 'Ελληνικά', + 'base' => 'el', + 'short_name' => 'EL', + ), + 'en' => + array ( + 'code' => 'en', + 'dir' => 'ltr', + 'name' => 'English', + 'base' => 'en', + 'short_name' => 'EN', + ), + 'es' => + array ( + 'code' => 'es', + 'dir' => 'ltr', + 'name' => 'Español', + 'base' => 'es', + 'short_name' => 'ES', + ), + 'et-EE' => + array ( + 'code' => 'et-EE', + 'dir' => 'ltr', + 'name' => 'Eesti (Eesti)', + 'base' => 'et', + 'short_name' => 'ET (EE)', + ), + 'fa' => + array ( + 'code' => 'fa', + 'dir' => 'rtl', + 'name' => 'فارسی', + 'base' => 'fa', + 'short_name' => 'FA', + ), + 'fi' => + array ( + 'code' => 'fi', + 'dir' => 'ltr', + 'name' => 'Suomi', + 'base' => 'fi', + 'short_name' => 'FI', + ), + 'fr' => + array ( + 'code' => 'fr', + 'dir' => 'ltr', + 'name' => 'Français', + 'base' => 'fr', + 'short_name' => 'FR', + ), + 'he' => + array ( + 'code' => 'he', + 'dir' => 'rtl', + 'name' => 'עברית', + 'base' => 'he', + 'short_name' => 'HE', + ), + 'hr' => + array ( + 'code' => 'hr', + 'dir' => 'ltr', + 'name' => 'Hrvatski', + 'base' => 'hr', + 'short_name' => 'HR', + ), + 'hu' => + array ( + 'code' => 'hu', + 'dir' => 'ltr', + 'name' => 'Magyar', + 'base' => 'hu', + 'short_name' => 'HU', + ), + 'id' => + array ( + 'code' => 'id', + 'dir' => 'ltr', + 'name' => 'Bahasa Indonesia', + 'base' => 'id', + 'short_name' => 'ID', + ), + 'it' => + array ( + 'code' => 'it', + 'dir' => 'ltr', + 'name' => 'Italiano', + 'base' => 'it', + 'short_name' => 'IT', + ), + 'ja' => + array ( + 'code' => 'ja', + 'dir' => 'ltr', + 'name' => '日本語', + 'base' => 'ja', + 'short_name' => 'JA', + ), + 'ko' => + array ( + 'code' => 'ko', + 'dir' => 'ltr', + 'name' => '한국어', + 'base' => 'ko', + 'short_name' => 'KO', + ), + 'lt-LT' => + array ( + 'code' => 'lt-LT', + 'dir' => 'ltr', + 'name' => 'Lietuvių (Lietuva)', + 'base' => 'lt', + 'short_name' => 'LT (LT)', + ), + 'nb' => + array ( + 'code' => 'nb', + 'dir' => 'ltr', + 'name' => '‪Norsk Bokmål‬', + 'base' => 'nb', + 'short_name' => 'NB', + ), + 'nl' => + array ( + 'code' => 'nl', + 'dir' => 'ltr', + 'name' => 'Nederlands', + 'base' => 'nl', + 'short_name' => 'NL', + ), + 'pl' => + array ( + 'code' => 'pl', + 'dir' => 'ltr', + 'name' => 'Polski', + 'base' => 'pl', + 'short_name' => 'PL', + ), + 'pt' => + array ( + 'code' => 'pt', + 'dir' => 'ltr', + 'name' => 'Português', + 'base' => 'pt', + 'short_name' => 'PT', + ), + 'pt-BR' => + array ( + 'code' => 'pt-BR', + 'dir' => 'ltr', + 'name' => 'Português (Brasil)', + 'base' => 'pt', + 'short_name' => 'PT (BR)', + ), + 'ru' => + array ( + 'code' => 'ru', + 'dir' => 'ltr', + 'name' => 'Русский', + 'base' => 'ru', + 'short_name' => 'RU', + ), + 'sk' => + array ( + 'code' => 'sk', + 'dir' => 'ltr', + 'name' => 'Slovenčina', + 'base' => 'sk', + 'short_name' => 'SK', + ), + 'sr-RS' => + array ( + 'code' => 'sr-RS', + 'dir' => 'ltr', + 'name' => 'Српски', + 'base' => 'sr', + 'short_name' => 'SR (RS)', + ), + 'sv' => + array ( + 'code' => 'sv', + 'dir' => 'ltr', + 'name' => 'Svenska', + 'base' => 'sv', + 'short_name' => 'SV', + ), + 'th' => + array ( + 'code' => 'th', + 'dir' => 'ltr', + 'name' => 'ไทย', + 'base' => 'th', + 'short_name' => 'TH', + ), + 'tr' => + array ( + 'code' => 'tr', + 'dir' => 'ltr', + 'name' => 'Türkçe', + 'base' => 'tr', + 'short_name' => 'TR', + ), + 'uk' => + array ( + 'code' => 'uk', + 'dir' => 'ltr', + 'name' => 'Українська', + 'base' => 'uk', + 'short_name' => 'UK', + ), + 'vi' => + array ( + 'code' => 'vi', + 'dir' => 'ltr', + 'name' => 'Tiếng Việt', + 'base' => 'vi', + 'short_name' => 'VI', + ), + 'zh-CN' => + array ( + 'code' => 'zh-CN', + 'dir' => 'ltr', + 'name' => '简体中文', + 'base' => 'zh', + 'short_name' => 'ZH (CN)', + ), + 'zh-TW' => + array ( + 'code' => 'zh-TW', + 'dir' => 'ltr', + 'name' => '繁體中文', + 'base' => 'zh', + 'short_name' => 'ZH (TW)', + ), +); \ No newline at end of file diff --git a/app/.cache/languages/ar.po.cache.php b/app/.cache/languages/ar.po.cache.php new file mode 100644 index 0000000..7cabd29 --- /dev/null +++ b/app/.cache/languages/ar.po.cache.php @@ -0,0 +1,3303 @@ + 'VERSION', + 'POT-Creation-Date' => '2017-06-21 18:28+0000', + 'PO-Revision-Date' => '2017-06-21 18:28+0000', + 'Last-Translator' => 'FULL NAME ', + 'Language-Team' => 'LANGUAGE TEAM ', + 'Language' => 'ar', + 'MIME-Version' => '1.0', + 'Content-Type' => 'text/plain; charset=UTF-8', + 'Content-Transfer-Encoding' => '8bit', + 'Plural-Forms' => 'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);', +); +$translation_plural = array ( + 'nplurals' => 6, + 'function' => '($n != 1)', +); +$translation_table = [ + "Invalid email" => [ + 0 => "البريد الالكتروني غير صحيح", + ], + "Invalid username" => [ + 0 => "إسم المستخدم غير صحيح", + ], + "Invalid password" => [ + 0 => "كلمة المرور غير صحيحة", + ], + "Invalid website mode" => [ + 0 => "وضع الموقع غير صالح", + ], + "From email address" => [ + 0 => "من البريد الإلكتروني", + ], + "Sender email for emails sent to users." => [ + 0 => "مرسل البريد الإلكترونى للرسائل الموجهة للمستخدمين.", + ], + "Incoming email address" => [ + 0 => "البريد الإلكتروني الوارد", + ], + "Recipient for contact form and system alerts." => [ + 0 => "المستقبل لنظام الإشعارات ونموذج الإتصال.", + ], + "Website mode" => [ + 0 => "وضع الموقع", + ], + "You can switch the website mode anytime." => [ + 0 => "يمكنك تغير وضع الموقع في اي وقت", + ], + "Community" => [ + 0 => "المجتمع", + ], + "Personal" => [ + 0 => "شخصي", + ], + "Update in progress" => [ + 0 => "جاري التحديث", + ], + "An error occurred. Please try again later." => [ + 0 => "حدث خطأ ما. يرجى المحاولة مرة أخرى.", + ], + "Missing %s file" => [ + 0 => "الملف %s مفقود", + ], + "Invalid license info" => [ + 0 => "معلومات الترخيص غير صالحة", + ], + "Invalid license key" => [ + 0 => "مفتاح الترخيص غير صالح", + ], + "Can't save file" => [ + 0 => "لا يمكن حفظ الملف\n", + ], + "Can't download %s" => [ + 0 => "لا يمكن تحميل %s", + ], + "Can't extract %s" => [ + 0 => "لا يمكن استخراج %s", + ], + "Can't create %s directory - %e" => [ + 0 => "لا يمكن انشاء %s دليل - %e", + ], + "Can't update %s file - %e" => [ + 0 => "لا يمكن تحديث %s ملف - %e", + ], + "Untitled" => [ + 0 => "بدون عنوان", + ], + "%s's images" => [ + 0 => "%s صور", + ], + "Note: This content is private but anyone with the link will be able to see this." => [ + 0 => "ملاحظة: هذا المحتوى خاص ولكن أي شخص لديه الرابط سيكون قادرا على رؤيته.", + ], + "Note: This content is password protected. Remember to pass the content password to share." => [ + 0 => "ملاحظة: هذا المحتوى محمي . تذكر ان تجتاز كلمة المرور لهذا المحتوى لمشاركته.", + ], + "Note: This content is private. Change privacy to \"public\" to share." => [ + 0 => "تنبيه : هذا المحتوى خاص. عدل خيارات الخصوصية إلى \"عام\" لأجل مشاركته.", + ], + "Private" => [ + 0 => "خاص", + ], + "Public" => [ + 0 => "عام", + ], + "Me" => [ + 0 => "أنا", + ], + "Link" => [ + 0 => "رابط", + ], + "Password" => [ + 0 => "كلمة المرور", + ], + "view" => [ + 1 => "مشاهدة", + 5 => "مشاهدات", + ], + "After %n %t" => [ + 0 => "بعد %n %t", + ], + "Don't autodelete" => [ + 0 => "لا تقم بالحذف التلقائي", + ], + "minute" => [ + 0 => "خانة فارغة", + 1 => "دقيقة", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "دقائق", + ], + "hour" => [ + 0 => "خانة فارغة", + 1 => "ساعة", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "ساعات", + ], + "day" => [ + 0 => "خانة فارغة", + 1 => "يوم", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "أيام", + ], + "Duplicated upload" => [ + 0 => "رفع مكرر", + ], + "Error storing file in external storage server" => [ + 0 => "خطأ في تخزين الملف في خادم التخزين الخارجي", + ], + "External storage has failed" => [ + 0 => "فشل التخزين الخارجي", + ], + "Private upload" => [ + 0 => "رفع خاص", + ], + "Upload switched to local storage" => [ + 0 => "تحول التحميل الي التخزين المحلي", + ], + "System has switched to local storage due to not enough disk capacity (%c) in the external storage server(s). The image %s has been allocated to local storage." => [ + 0 => "تم تحويل النظام إلى التخزين المحلى لعدم كفاية مساحة القرص (%c) فى مشغلات التخزين الخارجى. تم وضع الصورة %s فى المخزن المحلى", + ], + "like" => [ + 1 => "إعجاب", + 5 => "إعجاب", + ], + "image" => [ + 0 => "خانة فارغة", + 1 => "صورة", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "صور", + ], + "Recent" => [ + 0 => "الأحدث", + ], + "Trending" => [ + 0 => "الأكثر رواجا", + ], + "Popular" => [ + 0 => "الأكثر شعبية", + ], + "Top users" => [ + 0 => "الأعضاء الأكثر نشاطا", + ], + "Most recent" => [ + 0 => "الأحدث", + ], + "Oldest" => [ + 0 => "الأقدم", + ], + "Most viewed" => [ + 0 => "الأكثر مشاهدة", + ], + "Most liked" => [ + 0 => "الأكثر إعجابا", + ], + "Explore" => [ + 0 => " تصفح", + ], + "Animated" => [ + 0 => "الصور المتحركة", + ], + "Search" => [ + 0 => " بحث", + ], + "People" => [ + 0 => "اشخاص", + ], + "Image" => [ + 0 => "خانة فارغة", + 1 => "صورة", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "صور", + ], + "Album" => [ + 1 => "ألبوم", + 5 => "الألبومات", + ], + "User" => [ + 1 => "مستخدم", + 5 => "مستخدمين", + ], + "Can't create %s destination dir" => [ + 0 => "لايمكن إنشاء وجهة دليل %s", + ], + "Can't open %s for writing" => [ + 0 => "لا يمكن فتح %s للكتابة", + ], + "Internal" => [ + 0 => "داخلي", + ], + "Can't insert storage." => [ + 0 => "لا يمكن إدراج التخزين", + ], + "Storage capacity can't be lower than its current usage (%s)." => [ + 0 => "سعة التخزين المستخدمة لا يمكن أن تكون أقل من الاستخدام الحالى (%s).", + ], + "Can't update storage details." => [ + 0 => "لا يمكن تحديث تفاصيل التخزين.", + ], + "requires %s" => [ + 0 => "يتطلب %s", + ], + "Unlimited" => [ + 0 => "غير محدود", + ], + "used" => [ + 0 => "مستخدم", + ], + "Private profile" => [ + 0 => "ملف شخصي خاص", + ], + "year" => [ + 1 => "سنة", + 5 => "سنوات", + ], + "month" => [ + 1 => "شهر", + 5 => "أشهر", + ], + "week" => [ + 0 => "خانة فارغة", + 1 => "إسبوع", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "أسابيع", + ], + "second" => [ + 0 => "خانة فارغة", + 1 => "ثانية", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "ثواني", + ], + "%s ago" => [ + 0 => "%s مضى", + ], + "moments ago" => [ + 0 => "لحظات مضت", + ], + "System notification" => [ + 0 => "إعلام النظام", + ], + "Dashboard" => [ + 0 => "لوحة التحكم", + ], + "There is an update available for your system. Go to %s to download and install this update." => [ + 0 => "هناك تحديث متاح لنظامك. إذهب إلى %s لتحميل و تثبيت هذا التحديث", + ], + "System database is outdated. You need to run the update tool." => [ + 0 => "قاعدة بيانات النظام قديمة. تحتاج الى تشغيل أداة التحديث ", + ], + "Website is in maintenance mode. To revert this setting go to Dashboard > Settings." => [ + 0 => "الموقع في طور الصيانة. للتراجع عن هذا الإعداد اذهب الى لوحة التحكم > الإعدادات ", + ], + "You should disable PHP error reporting for production enviroment. Go to System settings to revert this setting." => [ + 0 => "يجب ان تعطل خاصية تقارير الأخطاء ل php لبيئة الإنتاج. اذهب إلى إعدادات النظام لتبديل هذا الإعداد.", + ], + "You haven't changed the default email settings. Go to Email settings to fix this." => [ + 0 => "لم تغير أعدادات البريد الإلكترونى الأفتراضية. إذهب إلى إعدادات البريد الإلكترونى لتصليح هذا", + ], + "There is an update available for your Chevereto based website." => [ + 0 => "هذا تحديث متاح لموقعك القائم على Chevereto", + ], + "The release notes for this update are:" => [ + 0 => "ملاحظات الإصدار لهذا التحديث هى:", + ], + "admin dashboard" => [ + 0 => "لوحة تحكم الادمن", + ], + "You can apply this update directly from your %a or download it from %s and then manually install it." => [ + 0 => "يمكن تطبيق هذا التحديث مباشر من %a الخاص بك أو تحميله من %s ثم تثبيته يدويا.", + ], + "Chevereto update available (v%s)" => [ + 0 => "تحديث Chevereto المتاح (v%s)", + ], + "view on %s" => [ + 0 => "مشاهدة على %s", + ], + "We use our own and third party cookies to improve your browsing experience and our services. If you continue using our website is understood that you accept this cookie policy." => [ + 0 => "نحن نستخدم ملفات تعريف الارتباط الخاصة بنا وأيضا ملفات تعريف طرف ثالث لتحسين تجربة التصفح الخاص بك في خدماتنا. إذا كنت لا تزال تستخدم موقعنا من المفهوم أن تقبل سياسة الكوكيز.", + ], + "You have been forbidden to use this website." => [ + 0 => "تم منعك من استخدام هذا الموقع.", + ], + "Feel free to browse and discover all my shared images and albums." => [ + 0 => "لا تتردد في تصفح واكتشاف كل الصور وألبومات المشاركة بواسطتى.", + ], + "View all my images" => [ + 0 => "عرض جميع الصور الخاصة بي", + ], + "That page doesn't exist" => [ + 0 => "هذه الصفحة غير موجودة", + ], + "Forgot password?" => [ + 0 => "نسيت كلمة السر ؟", + ], + "Reset password" => [ + 0 => "إعادة تعيين كلمة المرور", + ], + "Resend account activation" => [ + 0 => "إعادة إرسال تفعيل الحساب", + ], + "Add your email address" => [ + 0 => "إضافة بريدك الإلكتروني", + ], + "Email changed" => [ + 0 => "تم تغيير البريد الإلكتروني", + ], + "The reCAPTCHA wasn't entered correctly" => [ + 0 => "لم يتم كتابة حروف التحقق بشكل صحيح", + ], + "Invalid Username/Email" => [ + 0 => "إسم المستخدم/البريد الإلكتروني غير صالح", + ], + "User doesn't have an email." => [ + 0 => "المستخدم لا يتوفر لديه بريد إلكتروني.", + ], + "Request denied" => [ + 0 => "الطلب مرفوض", + ], + "Account needs to be activated to use this feature" => [ + 0 => "يتطلب تفعيل الحساب لإستخدام هذه الخاصية", + ], + "Account already activated" => [ + 0 => "سبق وأن تم تفعيل الحساب", + ], + "Allow up to 15 minutes for the email. You can try again later." => [ + 0 => "الرجاء الإنتظار 15 دقيقة لتلقى البريد الإلكترونى، ومن ثم يمكنك المحاولة من جديد.", + ], + "Reset your password at %s" => [ + 0 => "إعادة تعيين كلمة المرور في %s", + ], + "Confirmation required at %s" => [ + 0 => "التأكيد مطلوب في %s", + ], + "Welcome to %s" => [ + 0 => "أهلاً بك في %s", + ], + "Passwords don't match" => [ + 0 => "كلمة المرور غير متطابقة", + ], + "Email already being used" => [ + 0 => "البريد الإلكتروني مستخدم مسبقا‘", + ], + "Check the errors in the form to continue." => [ + 0 => "إفحص الخطأ أولاً حتى تتمكن من المتابعة.", + ], + "Password required" => [ + 0 => "كلمة السر مطلوبة", + ], + "Share" => [ + 0 => "مشاركة", + ], + "Embed codes" => [ + 0 => "إضافة اكواد", + ], + "Full info" => [ + 0 => "المعلومات الكاملة", + ], + "%a album hosted in %w" => [ + 0 => "%a الألبوم مستضاف فى %w", + ], + "Stats" => [ + 0 => "إحصائيات", + ], + "Images" => [ + 0 => "صور", + ], + "Albums" => [ + 0 => "الألبومات", + ], + "Users" => [ + 0 => "الأعضاء", + ], + "Settings" => [ + 0 => "الإعدادات", + ], + "Chevereto version" => [ + 0 => "نسخة Chevereto", + ], + "install update" => [ + 0 => "ثبت التحديث", + ], + "check for updates" => [ + 0 => "التحقق من التحديث", + ], + "Support" => [ + 0 => "الدعم", + ], + "Need help? Go to %s and you will get help quickly." => [ + 0 => "تحتاج المساعدة؟ اذهب الى %s وسوف تحصل على المساعدة بسرعة.", + ], + "PHP version" => [ + 0 => "نسخة PHP", + ], + "Server" => [ + 0 => "الخادم", + ], + "MySQL version" => [ + 0 => "نسخة MYSQL", + ], + "MySQL server info" => [ + 0 => "بيانات خادم MYSQL", + ], + "GD Library" => [ + 0 => "مكتبة GD", + ], + "File uploads" => [ + 0 => "الملفات المرفوعة", + ], + "Enabled" => [ + 0 => "مفعل", + ], + "Disabled" => [ + 0 => "معطل", + ], + "Max. upload file size" => [ + 0 => "الحد الاقصى. حجم ملف التحميل", + ], + "Max. post size" => [ + 0 => "الحجم الأقصى للتخزين", + ], + "Max. execution time" => [ + 0 => "الحد الأقصى لوقت التنفيذ", + ], + "%d second" => [ + 0 => "خانة فارغة", + 1 => "%d ثانية", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "%d ثواني", + ], + "Memory limit" => [ + 0 => "حدود الذاكرة", + ], + "Links" => [ + 0 => "روابط", + ], + "Documentation" => [ + 0 => "المستندات", + ], + "Changelog" => [ + 0 => "سجل التغيير", + ], + "Request new features" => [ + 0 => "طلب ميزات جديدة", + ], + "Bug tracking" => [ + 0 => "تتبع الأخطاء", + ], + "Blog" => [ + 0 => "المدونة", + ], + "Website" => [ + 0 => "الموقع", + ], + "Content" => [ + 0 => "المحتوى", + ], + "Pages" => [ + 0 => "الصفحات", + ], + "Listings" => [ + 0 => "عناصر العرض", + ], + "Image upload" => [ + 0 => "رفع الصور", + ], + "Categories" => [ + 0 => "الفئات", + ], + "Consent screen" => [ + 0 => "شاشة الموافقة", + ], + "Flood protection" => [ + 0 => "حماية من التدفق الإلكتروني", + ], + "Theme" => [ + 0 => "التصميم", + ], + "Homepage" => [ + 0 => "الرئيسية", + ], + "Banners" => [ + 0 => "البانرات", + ], + "System" => [ + 0 => "النظام", + ], + "Routing" => [ + 0 => "التوجيه", + ], + "Languages" => [ + 0 => "اللغات", + ], + "External storage" => [ + 0 => "تخزين خارجي", + ], + "Email" => [ + 0 => "البريد الإلكتروني", + ], + "Social networks" => [ + 0 => "شبكات التواصل الإجتماعي", + ], + "External services" => [ + 0 => "خدمات خارجية", + ], + "IP bans" => [ + 0 => "حظر IP", + ], + "Additional settings" => [ + 0 => "إعدادات اخرى", + ], + "Tools" => [ + 0 => "الأدوات", + ], + "Can't delete all homepage cover images" => [ + 0 => "لايمكن حذف صور غلاف الصفحة الرئيسية", + ], + "Homepage cover image deleted" => [ + 0 => "تم حذف صورة غلاف الصفحة الرئيسية", + ], + "Local" => [ + 0 => "محلي", + ], + "External" => [ + 0 => "خارجي", + ], + "All" => [ + 0 => "الكل", + ], + "search content" => [ + 0 => "بحث المحتوى", + ], + "Add page" => [ + 0 => "اضف صفحة", + ], + "Edit page ID %s" => [ + 0 => "تعديل معرف الصفحة %s", + ], + "The page has been added successfully." => [ + 0 => "تم اضافة الصفحة بنجاح", + ], + "The page has been deleted." => [ + 0 => "تم مسح الصفحة", + ], + "homepage" => [ + 0 => "الصفحة الرئيسية", + ], + "Before main title (%s)" => [ + 0 => "قبل العنوان الرئيسي (%s)", + ], + "After call to action (%s)" => [ + 0 => "بعد إتخاذ إجراء (%s)", + ], + "After cover (%s)" => [ + 0 => "بعد الغلاف (%s)", + ], + "After listing (%s)" => [ + 0 => "بعد القائمة (%s)", + ], + "Before pagination" => [ + 0 => "قبل ترقيم الصفحات", + ], + "After pagination" => [ + 0 => "بعد ترقيم الصفحات", + ], + "Content (image and album)" => [ + 0 => "المحتوى (الصور والألبوم)", + ], + "Tab about column" => [ + 0 => "تبويب عن العامود", + ], + "Before comments" => [ + 0 => "قبل التعليقات", + ], + "Image page" => [ + 0 => "صفحة الصورة", + ], + "Inside viewer top (image page)" => [ + 0 => "أعلى مستعرض الصور (صفحة الصورة)", + ], + "Expected banner size 728x90" => [ + 0 => "مساحة (أبعاد) البنر 728x90", + ], + "Inside viewer foot (image page)" => [ + 0 => "بداخل ذيل مستعرض الصور (صفحة الصورة)", + ], + "After image viewer (image page)" => [ + 0 => "بعد مستعرض الصور (صفحة الصورة)", + ], + "Before header (image page)" => [ + 0 => "قبل رأس الصفحة (صفحة الصورة)", + ], + "After header (image page)" => [ + 0 => "بعد رأس الصفحة (صفحة الصورة)", + ], + "Footer (image page)" => [ + 0 => "ذيل الصفحة (صفحة الصورة)", + ], + "Album page" => [ + 0 => "صفحة الألبوم", + ], + "Before header (album page)" => [ + 0 => "قبل رأس الصفحة (صفة الألبوم)", + ], + "After header (album page)" => [ + 0 => "بعد رأس الصفحة (صفحة الألبوم)", + ], + "User profile page" => [ + 0 => "صفحة ملف المستخدم", + ], + "After top (user profile)" => [ + 0 => "أعلى الصفحة (صفحة المستخدم)", + ], + "Before listing (user profile)" => [ + 0 => "قبل عرض الصور (صفحة المستخدم)", + ], + "Explore page" => [ + 0 => "تصفح الصفحة", + ], + "After top (explore page)" => [ + 0 => "بعد أعلى الصفحة (صفحة الإستعراض)", + ], + "NSFW" => [ + 0 => "محتوى غير آمن", + ], + "Invalid website name" => [ + 0 => "إسم الموقع غير صالح", + ], + "Invalid language" => [ + 0 => "اللغة غير صالحة", + ], + "Invalid timezone" => [ + 0 => "التوقيت غير صالح", + ], + "Invalid value: %s" => [ + 0 => "قيمة غير صالحة: %s", + ], + "Invalid upload storage mode" => [ + 0 => "وضع التخزين الخاص بالتحميل غير صالح", + ], + "Invalid upload filenaming" => [ + 0 => "اسم الملف المرفوع غير صالح", + ], + "Invalid thumb width" => [ + 0 => "عرض الصورة المصغرة غير صالح", + ], + "Invalid thumb height" => [ + 0 => "ارتفاع الصورة المصغرة غير صحيح", + ], + "Invalid medium size" => [ + 0 => "متوسط الحجم غير ضالح", + ], + "Invalid watermark percentage" => [ + 0 => "علامة مائية غير صالحة", + ], + "Invalid watermark opacity" => [ + 0 => "وضوح العلامة المائية غير صالح", + ], + "Invalid theme" => [ + 0 => "سمة غير صحيحة", + ], + "Invalid value" => [ + 0 => "القيمة غير صالح", + ], + "Invalid theme tone" => [ + 0 => "اسلوب غير صحيح", + ], + "Invalid theme main color" => [ + 0 => "اللون الرئيسي للسمة غير صالح", + ], + "Invalid theme top bar color" => [ + 0 => "لون الشريط العلوي للسمة غير صالح", + ], + "Invalid theme top bar button color" => [ + 0 => "لون الزر في الشريط العلوي غير ضالح ", + ], + "Invalid theme image listing size" => [ + 0 => "حجم عرض الصور في الاستايل غير صحيح", + ], + "Invalid user id" => [ + 0 => "رقم العضوية غير صالح", + ], + "Invalid email mode" => [ + 0 => "بريد إلكتروني غير صحيح", + ], + "Invalid SMTP port" => [ + 0 => "منفذ خاطئ لـ STMP", + ], + "Invalid SMTP security" => [ + 0 => "درجة أمان SMTP غير صالحة", + ], + "Invalid personal mode user ID" => [ + 0 => "هوية مستخدم للوضع الشخصى غير صالحة", + ], + "Invalid or reserved route" => [ + 0 => "طريق غير صالح او محجوز", + ], + "Invalid website privacy mode" => [ + 0 => "وضع خصوصية للموقع غير صالح", + ], + "Invalid website content privacy mode" => [ + 0 => "محتوى وضع خصوصية محتوى الموقع غير صالح", + ], + "Invalid homepage style" => [ + 0 => "طراز صفحة رئيسية غير صالح", + ], + "Invalid homepage call to action button color" => [ + 0 => "لون زر الإجراء فى الصفحة الرئيسية غير صالح", + ], + "Invalid homepage call to action functionality" => [ + 0 => "وظيفة إجراء للصفحة الرئيسية غير صالحة", + ], + "Invalid title" => [ + 0 => "عنوان خاطئ", + ], + "Invalid status" => [ + 0 => "حالة خاطئة", + ], + "Invalid type" => [ + 0 => "نوع خاطئ", + ], + "Invalid visibility" => [ + 0 => "خيار رؤية غير صالح", + ], + "Invalid target attribute" => [ + 0 => "خاصية هدف غير صالحة", + ], + "Invalid rel attribute" => [ + 0 => "خاصية مرتبطة غير صالحة", + ], + "Invalid icon" => [ + 0 => "أيقونة غير صالحة", + ], + "Invalid URL key" => [ + 0 => "مفتاح رابط غير صالح", + ], + "Invalid file path" => [ + 0 => "مسار ملف غير صالح", + ], + "Invalid link URL" => [ + 0 => "رابط غير صحيح", + ], + "Invalid user minimum age" => [ + 0 => "سن أدنى للمستخدم غير صحيح", + ], + "Only alphanumeric, hyphen and underscore characters are allowed" => [ + 0 => "مسموح فقط بالأبجدية الرقمية, الواصلات والرموز التحتية", + ], + "Routes can't be the same" => [ + 0 => "لايمكن أن يكون الطريق متطابقا", + ], + "Invalid upload image path" => [ + 0 => "مسار تحميل الصورة غير صحيح", + ], + "Invalid call to action URL" => [ + 0 => "رابط إجراء غير صحيح", + ], + "Max. allowed %s" => [ + 0 => "الحد الأقصى المسموح %s", + ], + "Can't map %m to an existing folder (%f)" => [ + 0 => "لايمكن ترسيم %m للمجلد القائم (%f)", + ], + "Can't map %m to an existing route (%r)" => [ + 0 => "لايمكن ترسيم %m للطريق القائم (%r)", + ], + "Can't map %m to %r (username collision)" => [ + 0 => "لايمكن ترسيم %m إلى %r (تعارض المستخدمين)", + ], + "Invalid SMTP server" => [ + 0 => "سيرفر STMP غير صحيح", + ], + "Invalid SMTP username" => [ + 0 => "اسم العضوية الخاص بـ SMTP غير صحيح", + ], + "Invalid URL" => [ + 0 => "عنوان URL خاطئ", + ], + "This URL key is already being used by another page (ID %s)" => [ + 0 => "عنوان URL مستخدم من قبل صفحة اخرى (معرف %s)", + ], + "This file path is already being used by another page (ID %s)" => [ + 0 => "مسار الملف مستخدم من قبل صفحة اخرى (معرف %s)", + ], + "Can't save page contents: %s." => [ + 0 => "لا يمكن حفظ محتوى الصفحة: %s", + ], + "Following" => [ + 0 => "متابع", + ], + "About" => [ + 0 => "عن", + ], + "Image ID" => [ + 0 => "رقم هوية الصورة", + ], + "Uploader IP" => [ + 0 => "الأيبي للرافع", + ], + "Ban IP" => [ + 0 => "احظر عنوان الـIP", + ], + "IP already banned" => [ + 0 => "عنوان الـIP محظور بالفعل", + ], + "Upload date" => [ + 0 => "تاريخ الرفع", + ], + "%s images" => [ + 0 => "%s صور", + ], + "Image %i in %a album" => [ + 0 => "الصورة %i في الألبوم %a", + ], + "Image %i in %c category" => [ + 0 => "الصورة %i في الفئة %c", + ], + "Image %i hosted in %w" => [ + 0 => "الصورة %i مستضافة في %w", + ], + "Direct links" => [ + 0 => "روابط مباشرة", + ], + "Image URL" => [ + 0 => "رابط الصورة", + ], + "Image link" => [ + 0 => "رابط الصورة", + ], + "Thumbnail URL" => [ + 0 => "رابط الصورة المصغرة", + ], + "Medium URL" => [ + 0 => "رابط الصورة المتوسطة", + ], + "Full image" => [ + 0 => "الصورة كاملة", + ], + "Full image (linked)" => [ + 0 => "الصورة كاملة (رابط)", + ], + "Medium image (linked)" => [ + 0 => "الصورة المتوسطة (رابط)", + ], + "Thumbnail image (linked)" => [ + 0 => "الصورة المصغيرة (رابط)", + ], + "Login needed" => [ + 0 => "تسجيل الدخول ضروري", + ], + "IP address already banned" => [ + 0 => "عنوان الـIP محظور بالفعل", + ], + "Missing values" => [ + 0 => "قيمة غير صالحة", + ], + "Invalid role" => [ + 0 => "الوظيفة غير صالحة", + ], + "Username already being used" => [ + 0 => "اسم المستخدم مأخوذ بالفعل", + ], + "Add a password or another social connection before deleting %s" => [ + 0 => "اضف كلمة سر أو اتصال اجتماعى أخر قبل حذف%s", + ], + "Add an email or another social connection before deleting %s" => [ + 0 => "اضف بريدا إليكترونيا أو اتصال اجتماعى أخر قبل حذف %s", + ], + "%s has been disconnected." => [ + 0 => "تم قطع الإتصال بـ %s .", + ], + "Test email from %s @ %t" => [ + 0 => "بريد الكتروني تجريبي من %s@%t", + ], + "This is just a test" => [ + 0 => "هذه فقط تجربة", + ], + "Test email sent to %s." => [ + 0 => "بريد الكتروني تجريبي ارسل الى %s", + ], + "User %s followed" => [ + 0 => "تم متابعة المستخدم %s", + ], + "User %s unfollowed" => [ + 0 => "تم إلغاء متابعة المستخدم %s", + ], + "Content liked" => [ + 0 => "محتوى تم الإعجاب به", + ], + "Content disliked" => [ + 0 => "محتوى تم إلغاء الإعجاب به", + ], + "%u liked your %t %c" => [ + 0 => "%u أعجب ب %t %c", + ], + "%u is now following you" => [ + 0 => "%u الأن يتابعك", + ], + "A private user" => [ + 0 => "مستخدم خاص", + ], + "Wrong Username/Email password combination" => [ + 0 => "بريد إلكتروني\\اسم مستخدم أو كلمة مرور خاطئة", + ], + "Sign in" => [ + 0 => "تسجيل الدخول", + ], + "Logged out" => [ + 0 => "تسجيل الخروج", + ], + "General questions/comments" => [ + 0 => "أسئلة عامة / تعليقات", + ], + "DMCA complaint" => [ + 0 => "شكوى DMCA", + ], + "Invalid name" => [ + 0 => "اسم خاطئ", + ], + "Invalid message" => [ + 0 => "رسالة غير صالحة", + ], + "Invalid subject" => [ + 0 => "عنوان غير صالح", + ], + "Invalid reCAPTCHA" => [ + 0 => "reCAPTCHA غير صالحة", + ], + "Can't submit the form: %s" => [ + 0 => "لايمكن تقديم النموذج: %s", + ], + "Message sent. We will get in contact soon." => [ + 0 => "تم الارسال. سوف نتصل بك قريبا .", + ], + "Mail error" => [ + 0 => "خطأ في البريد الإلكتروني", + ], + "Image search results for %s" => [ + 0 => "نتائج البحث عن صورة لـ%s", + ], + "Album search results for %s" => [ + 0 => "نتائج بحث الألأبومات لـ %s", + ], + "User search results for %s" => [ + 0 => "نتائج بحث المستخدمين لـ%s", + ], + "Account" => [ + 0 => "الحساب", + ], + "Profile" => [ + 0 => "الملف الشخصي", + ], + "Linked accounts" => [ + 0 => "الحسابات المرتبطة", + ], + "Invalid image expiration: %s" => [ + 0 => "تاريخ إنتهاء الصورة غير صالح: %s", + ], + "An email has been sent to %s with instructions to activate this email" => [ + 0 => "تم إرسال رسالة إلى %s تحتوي على طريقة تفعيل هذا البريد الإلكتروني", + ], + "Invalid website" => [ + 0 => "عنوان الموقع إلكتروني خاطئ", + ], + "Wrong password" => [ + 0 => "كلمة مرور خاطئة", + ], + "Use a new password" => [ + 0 => "استخدم كلمة مرور جديدة", + ], + "Changes have been saved." => [ + 0 => "تم حفظ الاعدادات.", + ], + "Password has been changed" => [ + 0 => "تم تعديل كلمة المرور بنجاح", + ], + "Password has been created." => [ + 0 => "تم إنشاء كلمة السر بنجاح.", + ], + "Wrong Username/Email values" => [ + 0 => "بريد إلكتروني أو كلمة مرور خاطئة", + ], + "Settings for %s" => [ + 0 => "التهيئة لـ %s", + ], + "You must be at least %s years old to use this website." => [ + 0 => "يجب ان تكون %s سنة لتستخدم هذا الموقع", + ], + "Create account" => [ + 0 => "إنشاء حساب", + ], + "%s's Images" => [ + 0 => "%s صور", + ], + "%s's Albums" => [ + 0 => "%s البوم", + ], + "Results for" => [ + 0 => "النتائج لـ", + ], + "Liked by %s" => [ + 0 => "اعجب بواسطة ٪s", + ], + "Liked" => [ + 0 => "اعجاب", + ], + "Followers" => [ + 0 => "المتابعون", + ], + "%n (%u) albums on %w" => [ + 0 => "%n (%u) البومات على %w", + ], + "%n (%u) on %w" => [ + 0 => "%n (%u) على %w", + ], + "Discovery" => [ + 0 => "اكتشاف", + ], + "Close" => [ + 0 => "إغلاق", + ], + "Advanced search" => [ + 0 => "بحث متقدم", + ], + "Random" => [ + 0 => "عشوائي", + ], + "Notices (%s)" => [ + 0 => "الملاحظات (%s)", + ], + "Upload" => [ + 0 => "رفع", + ], + "Sign in with another account" => [ + 0 => "تسجيل الدخول بحساب آخر", + ], + "or" => [ + 0 => "أو", + ], + "Username or Email address" => [ + 0 => "اسم المستخدم أو البريد الإلكتروني", + ], + "Keep me logged in" => [ + 0 => "تذكر دخولي", + ], + "Don't have an account? Sign up now." => [ + 0 => "لا يوجد لديك حساب؟ سجل الآن.", + ], + "Sign up with another account" => [ + 0 => "الإشتراك بحساب آخر", + ], + "Email address" => [ + 0 => "البريد الإلكتروني", + ], + "Username" => [ + 0 => "إسم المستخدم", + ], + "I'm at least %s years old" => [ + 0 => "انا بعمر %s سنة على الأقل", + ], + "By signing up you agree to our Terms of service" => [ + 0 => "بمجرد إشتراكك ستكون قد وافقت على شروط الخدمة", + ], + "Notifications" => [ + 0 => "الإشعارات", + ], + "loading" => [ + 0 => "جاري التحميل", + ], + "You don't have notifications" => [ + 0 => "لا يوجد لديك اشعارات", + ], + "My Profile" => [ + 0 => "ملفي الشخصي", + ], + "Sign out" => [ + 0 => "تسجيل الخروج", + ], + "We received a request to change the email of your %n account at %w." => [ + 0 => "لقد وصلنا طلب لتغيير البريد الإلكتروني %n لحسابك في %w.", + ], + "To complete the process you must activate your email." => [ + 0 => "لمتابعة الإجراء يستوجب عليك تفعيل بريدك الإلكتروني .", + ], + "Alternatively you can copy and paste the URL into your browser: %s" => [ + 0 => "كخيار ثاني ، يمكنك نسخ ولصق الرابط التالي في متصفحك حتى تتمكن من تفعيل بريدك الإلكتروني %s", + ], + "If you didn't intend this just ignore this message." => [ + 0 => "إذا لم تقم بتنفيذ هذا الإجراء ، برجاء تجاهل هذه الرسالة.", + ], + "This request was made from IP: %s" => [ + 0 => "هذا الإجراء تم تنفيذه بواسطة الأي بي التالي : %s", + ], + "We received a request to register the %n account at %w." => [ + 0 => "لقد تلقينا طلب تسجيل %n الحساب فى %w .", + ], + "To complete the process you must activate your account." => [ + 0 => "لإنهاء العمليه يرجى تفعيل عضوية حسابك ", + ], + "We received a request to reset the password for your %n account." => [ + 0 => "لقد تلقينا طلب لتغيير كلمة المرور %n لحسابك", + ], + "To reset your password follow this link." => [ + 0 => "لإعادة تعيين كلمة المرور الخاصة بك اضغط هنا.", + ], + "Hi %n, welcome to %w" => [ + 0 => "مرحبا %n مرحبا بك في %w", + ], + "Now that your account is ready you can enjoy uploading your images, creating albums and setting the privacy of your content as well as many more cool things that you will discover." => [ + 0 => "الآن وبعد أن قمت بانشاء حساب خاص بك بامكانك الاستمتاع برفع الصور الخاصة بك واضافة ألبومات وتعديدل الخصوصية حسب ما تريد.", + ], + "By the way, here is you very own awesome profile page: %n. Go ahead and customize it, its yours!." => [ + 0 => "بالمناسبة, هاهى صفحتك الشخصية الرائعة: %n . هيا قم بتخصيصها حسب ماتحب, فهى ملك لك خصيصا!", + ], + "Thank you for joining" => [ + 0 => "شكراً لانضمامك", + ], + "This email was sent from %w %u" => [ + 0 => "تم ارسال البريد من %u %w", + ], + "Drag and drop or paste images here to upload" => [ + 0 => "اسحب و أسقط أو ألصق الصور هنا للتحميل", + ], + "Select the images to upload" => [ + 0 => "إختر الصور المراد رفعها", + ], + "browse from your computer" => [ + 0 => "تصفح من جهاز الكمبيوتر الخاص بك", + ], + "add image URLs" => [ + 0 => "أضف رابط صورة", + ], + "You can also %i or %u." => [ + 0 => "يمكنك أيضاً %i أو %u.", + ], + "take a picture" => [ + 0 => "التقط صورة", + ], + "Edit or resize any image by clicking the image preview" => [ + 0 => "تعديل أو تغيير حجم أي صورة من خلال النقر على معاينة الصورة", + ], + "Edit or resize any image by touching the image preview" => [ + 0 => "تعديل أو تغيير حجم أي صورة من خلال اللمس على معاينة الصورة", + ], + "your computer" => [ + 0 => "جهاز الكمبيوتر الخاص بك", + ], + "image URLs" => [ + 0 => "روابط الصور", + ], + "You can keep adding more images from %i or from %u." => [ + 0 => "يمكنك الاستمرار في إضافة المزيد من الصور من %i أو من %u", + ], + "your device" => [ + 0 => "جهازك", + ], + "Uploading %q %o" => [ + 0 => "يتم تحميل %q %o", + ], + "complete" => [ + 0 => "أكتمل", + ], + "The queue is being uploaded, it should take just a few seconds to complete." => [ + 0 => "يجري الآن تحميل القائمة، سوف يستغرق عدة ثواني للإنتهاء.", + ], + "Upload complete" => [ + 0 => "تم الرفع", + ], + "Uploaded content added to %s." => [ + 0 => "تمت إضافة المحتوى الذي تم تحميله إلى %s.", + ], + "You can %c with the content just uploaded or %m." => [ + 0 => "يمكنك %c فقط مع المحتوى الذي تم تحميله أو %m.", + ], + "You can %c with the content just uploaded." => [ + 0 => "يمكنك %c مع المحتوى الذي تم تحميله للتو.", + ], + "create a new album" => [ + 0 => "إنشاء ألبوم جديد.", + ], + "move it to an existing album" => [ + 0 => "نقله إلى ألبوم موجود", + ], + "create an account" => [ + 0 => "إنشاء حساب", + ], + "sign in" => [ + 0 => "تسجيل الدخول", + ], + "You can %s or %l to save this content into your account." => [ + 0 => "يمكنك %s أو %l لحفظ هذا المحتوى في حسابك.", + ], + "No %s have been uploaded" => [ + 0 => "لا %s تم تحميلها", + ], + "Some errors have occured and the system couldn't process your request." => [ + 0 => "حدث خطأ ما والنظام لا يستطيع الاستجابة لطلبك.", + ], + "Category" => [ + 0 => "فئة", + ], + "Select category" => [ + 0 => "تحديد الفئات", + ], + "Mark this if the upload is not family safe" => [ + 0 => "قم بتعليم المحتوى على أنه غير مناسب للأسرة", + ], + "Not family safe upload" => [ + 0 => "محتوى غير مناسب للأسرة", + ], + "Uploading" => [ + 0 => "جاري الرفع", + ], + "cancel" => [ + 0 => "إلغاء", + ], + "cancel remaining" => [ + 0 => "إلغاء ما تبقى", + ], + "Note: Some images couldn't be uploaded." => [ + 0 => "ملاحظة : بعض الصور لم يتم رفعها بنجاح.", + ], + "learn more" => [ + 0 => "إعرف المزيد", + ], + "Check the error report for more information." => [ + 0 => "تحقق من تقرير الخطأللمزيد من المعلومات", + ], + "max" => [ + 0 => "أقصى", + ], + "close" => [ + 0 => "إغلاق", + ], + "copy" => [ + 0 => "نسخ", + ], + "Edit" => [ + 0 => "تعديل", + ], + "Remove" => [ + 0 => "حذف", + ], + "Edit image" => [ + 0 => "تعديل الصورة", + ], + "Title" => [ + 0 => "عنوان", + ], + "optional" => [ + 0 => "خياري", + ], + "Resize image" => [ + 0 => "تغيير حجم الصورة", + ], + "Width" => [ + 0 => "عرض", + ], + "Height" => [ + 0 => "ارتفاع", + ], + "Note: Animated GIF images won't be resized." => [ + 0 => "ملاحظة: لن يتم تغيير حجم الصور المتحركة .", + ], + "Auto delete image" => [ + 0 => "حذف تلقائي للصورة", + ], + "Sign up" => [ + 0 => "تسجيل", + ], + "%s to be able to customize or disable image auto delete." => [ + 0 => "%s لتتمكن من تخصيص أو تعطيل الحذف التلقائي للصورة.", + ], + "Mark this if the image is not family safe" => [ + 0 => "الاشارة على هذه الصورة في حال كانت غير آمنة", + ], + "Flag as unsafe" => [ + 0 => "علامة لمحتوى غير آمن", + ], + "Description" => [ + 0 => "الوصف", + ], + "Brief description of this image" => [ + 0 => "وصف مختصر عن الصورة", + ], + "Add image URLs" => [ + 0 => "إضافة روابط الصورة", + ], + "Add the image URLs here" => [ + 0 => "إضافة روابط الصورة هنا", + ], + "Create album" => [ + 0 => "إنشاء ألبوم", + ], + "The uploaded content will be moved to this newly created album. You can also move the content to an existing album." => [ + 0 => "سيتم نقل المحتويات التي يتم تحميلها إلى هذا الألبوم تم إنشاؤه حديثا. يمكنك أيضا نقل المحتوى إلى الالبوم القائم", + ], + "Move to album" => [ + 0 => "نقل إلى الألبوم", + ], + "Select an existing album to move the uploaded content. You can also create a new album and move the content there." => [ + 0 => "اختر الألبوم لنقل المحتوى الذي تم تحميله. يمكنك أيضا إنشاء ألبوم جديدونقل المحتوى هناك", + ], + "Error report" => [ + 0 => "تقرير الأخطاء", + ], + "album" => [ + 0 => "خانة فارغة", + 1 => "ألبوم", + 2 => "خانة فارغة", + 3 => "خانة فارغة", + 4 => "خانة فارغة", + 5 => "ألبومات", + ], + "Viewer links" => [ + 0 => "روابط المشاهد", + ], + "HTML Codes" => [ + 0 => "رمز HTML", + ], + "HTML image" => [ + 0 => "صورة HTML", + ], + "HTML full linked" => [ + 0 => "HTML مرتبط بالكامل", + ], + "HTML medium linked" => [ + 0 => "رابط HTML للصورة المتوسطة", + ], + "HTML thumbnail linked" => [ + 0 => "رابط HTML للصورة الصغيرة", + ], + "BBCodes" => [ + 0 => "رمز بي بي", + ], + "BBCode full" => [ + 0 => "BBCode الكامل", + ], + "BBCode full linked" => [ + 0 => "كود BB مرتبط بالكامل", + ], + "BBCode medium linked" => [ + 0 => "رابط كود BB المتوسط", + ], + "BBCode thumbnail linked" => [ + 0 => "رمز بي بي للصورة المصغرة", + ], + "Markdown full" => [ + 0 => "تخفيض كامل", + ], + "Markdown full linked" => [ + 0 => "رابط التخفيض بالكامل", + ], + "Markdown medium linked" => [ + 0 => "رابط التخفيض المتوسط", + ], + "Markdown thumbnail linked" => [ + 0 => "ربط صورة التخفيض المصغرة", + ], + "All these words" => [ + 0 => "كل الكلمات", + ], + "Type the important words: tri-colour rat terrier" => [ + 0 => "اكتب الكلمات المهمة: ثلاثية الألوان", + ], + "This exact word or phrase" => [ + 0 => "نفس الكلمة او العبارة", + ], + "Put exact words in quotes: \"rat terrier\"" => [ + 0 => "ضع الكلمات بالضبط في الاقتباس : \"rat terrier\"", + ], + "None of these words" => [ + 0 => "أيا من هذه الكلمات", + ], + "Put a minus sign just before words you don't want: -rodent -\"Jack Russell\"" => [ + 0 => "وضع علامة الطرح قبل الكلمات التي لا تريد:-rodent -\"Jack Russell\"", + ], + "Storage" => [ + 0 => "المخزن", + ], + "IP address" => [ + 0 => "عنوان IP", + ], + "Album name" => [ + 0 => "اسم الألبوم", + ], + "move to existing album" => [ + 0 => "نقل إلى الألبوم القائم", + ], + "Album description" => [ + 0 => "وصف الألبوم", + ], + "Brief description of this album" => [ + 0 => "وصف موجز لهذا الألبوم", + ], + "Album privacy" => [ + 0 => "خصوصية الألبوم", + ], + "Who can view this content" => [ + 0 => "من سيشاهد هذا المحتوى", + ], + "Private (just me)" => [ + 0 => "خاص (أنا فقط)", + ], + "Private (anyone with the link)" => [ + 0 => "خاص (أي شخص مع الرابط)", + ], + "Private (password protected)" => [ + 0 => "خاص ( محمى بكلمة سر )", + ], + "Album password" => [ + 0 => "كلمة سر الألبوم", + ], + "Name" => [ + 0 => "الإسم", + ], + "Category name" => [ + 0 => "اسم الفئة", + ], + "URL key" => [ + 0 => "مفتاح الرابط", + ], + "Category URL key" => [ + 0 => "مفتاح رابط الفئة", + ], + "Only letters, numbers, and hyphens" => [ + 0 => "فقط الحروف، الأرقام، و الوصلات", + ], + "Brief description of this category" => [ + 0 => "وصف موجز لهذه الفئة", + ], + "Untitled image" => [ + 0 => "صورة غير معنونة", + ], + "Expiration date" => [ + 0 => "تاريخ انتهاء الصلاحية", + ], + "YYYY-MM-DD HH:MM:SS" => [ + 0 => "سنة-شهر-يوم ساعة-دقيقة-ثانية", + ], + "Example" => [ + 0 => "مثال", + ], + "Until which date this IP address will be banned? Leave it empty for no expiration." => [ + 0 => "حتى أى تاريخ سيمنع عنوان الأى بى هذا؟ اتركه فارغا لإلغاء تاريخ الصلاحية.", + ], + "Message" => [ + 0 => "الرسالة", + ], + "Text message, HTML or a redirect URL" => [ + 0 => "رسالة نصية, html أو رابط إعادة توجيه", + ], + "Existing album" => [ + 0 => "المجلد القائم", + ], + "create new album" => [ + 0 => "إنشاء ألبوم جديد", + ], + "Storage name" => [ + 0 => "اسم التخزين", + ], + "API" => [ + 0 => "المعرف API", + ], + "Region" => [ + 0 => "منطقة", + ], + "Storage bucket" => [ + 0 => "مكان التخزين", + ], + "Key" => [ + 0 => "مفتاح", + ], + "Storage key" => [ + 0 => "مفتاح التخزين", + ], + "Secret" => [ + 0 => "سري", + ], + "Storage secret" => [ + 0 => "التخزين السري", + ], + "Client email" => [ + 0 => "البريد الإلكتروني للعميل", + ], + "Google Cloud client email" => [ + 0 => "البريد الأكتروني لمستخدم جوجل كلاود", + ], + "You will need a service account for this." => [ + 0 => "انت تحتاج الى حساب خدمة لهذا", + ], + "Private key" => [ + 0 => "مفتاح خاص", + ], + "Google Cloud JSON key" => [ + 0 => "مفتاح JSON لجوجل كلاود", + ], + "Service name" => [ + 0 => "اسم الخدمة", + ], + "Identity URL" => [ + 0 => "رابط الهوية", + ], + "Identity API endpoint" => [ + 0 => "نقطة نهاية هوية API", + ], + "API endpoint for OpenStack identity" => [ + 0 => "نقطة نهاية API لهوية أوبن ستاك ", + ], + "Storage region" => [ + 0 => "منطقة التخزين", + ], + "Container" => [ + 0 => "حاوية", + ], + "Storage container" => [ + 0 => "حاوية التخزين", + ], + "Tenant id" => [ + 0 => "هوية المستأجر", + ], + "Tenant id (account id)" => [ + 0 => "هوية المستأجر ( هوية الحساب )", + ], + "Tenant name" => [ + 0 => "اسم المستأجر", + ], + "Tenant name (account name)" => [ + 0 => "اسم المستأجر ( اسم الحساب ) ", + ], + "Hostname or IP of the storage server" => [ + 0 => "اسم المضيف أو IP لخادم التخزين", + ], + "Path" => [ + 0 => "مسار", + ], + "Server path" => [ + 0 => "مسار الخادم", + ], + "Server path where the files will be stored" => [ + 0 => "مسار الخادم المعتمد حيث سيتم تخزين ملفات", + ], + "Server username" => [ + 0 => "اسم مستخدم الخادم", + ], + "Server password" => [ + 0 => "كلمة مرور الخادم", + ], + "Storage capacity" => [ + 0 => "مساحة التخزين", + ], + "Example: 20 GB, 1 TB, etc." => [ + 0 => "مثال : 20 GB, 1 TB ...", + ], + "This storage will be disabled when it reach this capacity. Leave it blank or zero for no limit." => [ + 0 => "سيتم تعطيل هذا التخزين عندما يصل إلى أقصى قدراته. أتركه فارغ أو \"0\" للامحدود.", + ], + "Storage URL" => [ + 0 => "URL التخزين", + ], + "The system will map the images of this storage to this URL." => [ + 0 => "سيقوم النظام بترسيم صور هذا التخزين لهذا الرابط.", + ], + "view more" => [ + 0 => "شاهد المزيد", + ], + "Load more" => [ + 0 => "عرض المزيد", + ], + "Select all" => [ + 0 => "تحديد الكل", + ], + "Clear selection" => [ + 0 => "مسح المحدد", + ], + "Selection" => [ + 0 => "إختيار", + ], + "Action" => [ + 0 => "إجراء", + ], + "Get embed codes" => [ + 0 => "أحصل على كود العرض", + ], + "Assign category" => [ + 0 => "عين فئة", + ], + "Flag as safe" => [ + 0 => "علم بأنه محتوى آمن", + ], + "Delete" => [ + 0 => "حذف", + ], + "Create new album" => [ + 0 => "إنشاء ألبوم جديد", + ], + "To use all the features of this site you must be logged in. If you don't have an account you can sign up right now." => [ + 0 => "حتى تتمكن من إستخدام جميع المميزات يجب عليك تسجيل الدخول. إذا لم يكن لديك حساب اشتركالان", + ], + "There's nothing to show here." => [ + 0 => "لا يوجد شيء لإظهاره هنا.", + ], + "Upload images" => [ + 0 => "تحميل الصور", + ], + "Edit image details" => [ + 0 => "تعديل بيانات الصورة", + ], + "Edit album details" => [ + 0 => "تعديل معلومات الألبوم", + ], + "All the images will be moved to this newly created album. You can also move the images to an existing album." => [ + 0 => "جميع الصور سوف يتم نقلها إلى أحدث ألبوم تم إنشاؤه ، كما يمكنك نقل الصور إلى الألبومات الحالية .", + ], + "Select an existing album to move the image. You can also create a new album and move the image there." => [ + 0 => "إختر الألبوم الذي ترغب نقل الصور فيه ، كما يمكنك إنشاء ألبوم جديد ونقل الصور فيه", + ], + "Select an existing album to move the album contents. You can also create a new album and move the album contents there." => [ + 0 => "حدد ألبوم من القائمة لنقل محتوياته. يمكنك أيضا إنشاء ألبوم جديد ونقل محتويات الألبوم المحدد مسبقا إلى هناك.", + ], + "Select an existing album to move the images. You can also create a new album and move the images there." => [ + 0 => "إختر الألبوم الذي ترغب نقل الصور فيه ، كما يمكنك إنشاء ألبوم جديد ونقل الصور فيه", + ], + "All the selected images will be assigned to this category." => [ + 0 => "سيتم تعيين جميع الصور المختارة لهذه الفئة.", + ], + "There is no categories." => [ + 0 => "لايوجد فئات.", + ], + "Confirm flag content as safe" => [ + 0 => "قم بالتأكيد بأن المحتوى آمن", + ], + "Do you really want to flag this content as safe?" => [ + 0 => "هل أنت متأكد من أن المحتوى آمن ؟", + ], + "Confirm flag content as unsafe" => [ + 0 => "تأكيد بأن المحتوى غير آمن", + ], + "Do you really want to flag this content as unsafe?" => [ + 0 => "هل تريد حقا الإشارة الى هذا المحتوى بأنه غير آمن ؟", + ], + "Confirm deletion" => [ + 0 => "تأكيد الحذف", + ], + "Do you really want to remove this content? This can't be undone." => [ + 0 => "هل أنت متأكد من رغبتك في مسح هذا المحتوى ؟ لا يمكن التراجع عن هذا الإجراء.", + ], + "Do you really want to remove all the selected content? This can't be undone." => [ + 0 => "هل أنت متأكد من رغبتك في مسح المحتويات المختارة ؟ لا يمكنك التراجع عن هذا الإجراء", + ], + "Uploaded by guest" => [ + 0 => "مرفوع بواسطة ضيف", + ], + "From %s" => [ + 0 => "من %s", + ], + "Uploaded by private" => [ + 0 => "مرفوع بواسطة خاص", + ], + "by %u" => [ + 0 => "بواسطة %u", + ], + "Select" => [ + 0 => "إختيار", + ], + "Toggle unsafe flag" => [ + 0 => "تبديل العلم الغير أمن ", + ], + "The requested page was not found." => [ + 0 => "الصفحة المطلوبة غير موجودة.", + ], + "Search something else" => [ + 0 => "إبحث في شئ آخر", + ], + "The user has been deleted" => [ + 0 => "تم حذف المستخدم بنجاح", + ], + "The content has been deleted." => [ + 0 => "تم حذف المحتوى.", + ], + "Your account is almost ready" => [ + 0 => "حسابك جاهز تقريبا", + ], + "An email to %s has been sent with instructions to activate your account. The activation link is only valid for 48 hours. If you don't receive the instructions try checking your junk or spam filters." => [ + 0 => "تم ارسال بريد الكتروني الى %s مع تعليمات لتفعيل حسابك. رابط التفعيل صالح فقط لمدة 48 ساعة. إذا لم تتلقى التعليمات في صندوق الوارد حاول البحث عنه في مجلد البريد الغير مرغوب.", + ], + "Go to homepage" => [ + 0 => "إذهب إلى الصفحة الرئيسية", + ], + "Resend activation" => [ + 0 => "إعادة إرسال التفعيل", + ], + "You have successfully changed your account email to %s" => [ + 0 => "لقد قمت بنجاج بتغيير البريد الإلكتروني الخاص بحسابك إلى %s", + ], + "Go to my profile" => [ + 0 => "إذهب إلى ملفي الشخصي", + ], + "A confirmation link will be sent to this email with details to activate your account." => [ + 0 => "سوف يتم إرسال رسالة تأكيد إلى هذا البريد يحتوي على تعليمات لتفعيل الحساب.", + ], + "Your email address" => [ + 0 => "بريدك الإلكتروني", + ], + "Add email" => [ + 0 => "إضافة بريد إلكتروني", + ], + "An email with instructions to reset your password has been sent to the registered email address. If you don't receive the instructions try checking your junk or spam filters." => [ + 0 => "تم إرسال رسالة بريد إلكتروني مع تعليمات لإعادة تعيين كلمة المرور الخاصة بك إلى عنوان البريد الإلكتروني المسجل. إذا لم تتلقى تلك التعليمات في صندوق بريدك الوارد قم بالبحث عن الرسالة في صندوق مرشحات البريد المزعج.", + ], + "Resend instructions" => [ + 0 => "إعادة إرسال التعليمات", + ], + "A previous email has been sent with instructions to reset your password. If you did not receive the instructions try checking your junk or spam filters." => [ + 0 => "تم ارسال بريد الكتروني مع معلومات اعادة تعين كلمة المرور. اذا لم تستلم البريد برجاء مراجعة جميع المجلدات في بريدك من الممكن ان تكون في مرشحات البريد المزعج.", + ], + "Enter your username or the email address that you used to create your account." => [ + 0 => "إدخل البريد الإلكتروني أو اسم العضوية التي قمت بتزويدهما أثناء الإشتراك.", + ], + "Submit" => [ + 0 => "تقديم", + ], + "Your password has been changed. You can now try logging in using your new password." => [ + 0 => "تم تغيير كلمة السر.. تستطيع الان الدخول باستخدام كلمة السر الجديدة.", + ], + "Login now" => [ + 0 => "سجل دخولك", + ], + "Enter the new password that you want to use." => [ + 0 => "إدخل كلمة المرور الجديدة التي تود إستخدامها.", + ], + "New Password" => [ + 0 => "كلمة المرور الجديدة", + ], + "%d characters min" => [ + 0 => "%d كحد ادنى من الحروف", + ], + "Enter your new password" => [ + 0 => "إدخل كلمة المرور الجديدة", + ], + "Confirm password" => [ + 0 => "تأكيد كلمة المرور", + ], + "Re-enter your new password" => [ + 0 => "إعادة إدخال كلمة المرور", + ], + "An email to %s has been sent with instructions to activate your account. If you don't receive the instructions try checking your junk or spam filters." => [ + 0 => "تم إرسال رسالة بالبريد الالكتروني إلى٪s مع تعليمات لتفعيل حسابك. إذا لم تتلقى تعليمات, يرجى التحقق من البريد غير المرغوب فيه أو مرشحات البريد المزعج.", + ], + "Enter the username or email address that you used to create your account to continue." => [ + 0 => "أدخل اسم المستخدم أو عنوان البريد الإلكتروني الذي استخدمته لإنشاء حسابك للمتابعة.", + ], + "This content is private" => [ + 0 => "هذا المحتوى خاص", + ], + "Do you really want to delete this album and all of its images? This can't be undone." => [ + 0 => "هل حقا تريد حذف هذا الألبوم وكافة الصور الموجودة بداخله ؟ لا يمكن التراجع.", + ], + "Delete album" => [ + 0 => "حذف الألبوم", + ], + "You like this" => [ + 0 => "انت معحب بهذا", + ], + "Like" => [ + 0 => "إعجاب", + ], + "Upload to album" => [ + 0 => "تحميل إلى الألبوم", + ], + "Album link" => [ + 0 => "رابط الألبوم", + ], + "Please read and comply with the following conditions before you continue:" => [ + 0 => "يرجى قراءة والالتزام بالشروط التالية قبل المتابعة:", + ], + "This website contains information, links and images of sexually explicit material. If you are under the age of %s, if such material offends you or if it's illegal to view such material in your community please do not continue.\n\nI am at least %s years of age and I believe that as an adult it is my inalienable right to receive/view sexually explicit material. I desire to receive/view sexually explicit material. \n\nI believe that sexual acts between consenting adults are neither offensive nor obscene. The viewing, reading and downloading of sexually explicit materials does not violate the standards of my community, town, city, state or country.\n\nThe sexually explicit material I am viewing is for my own personal use and I will not expose minors to the material.\n\nI am solely responsible for any false disclosures or legal ramifications of viewing, reading or downloading any material in this site. Furthermore this website nor its affiliates will be held responsible for any legal ramifications arising from fraudulent entry into or use of this website.\n\nThis consent screen constitutes a legal agreement between this website and you and/or any business in which you have any legal or equitable interest. If any portion of this agreement is deemed unenforceable by a court of competent jurisdiction it shall not affect the enforceability of the other portions of the agreement." => [ + 0 => "هذا الموقع يحتوى على معلومات, روابط وصور ذات محتوى للبالغين. إذا كنت تحت سن %s. أو إذا كانت المحتوى يؤذيك أو من الغير قانونى مشاهدته فى مجتمعك, رجاء عدم الاستمرار. \n\nانا عمرى على الأقل %s وأعتقد بصفتى شخصا بالغا ان من حقى تلقى \\ مشاهدة محتوى البالغين . واريد ان اتلقى\\ اشاهد ذلك المحتوى. \n\nاعتقد أن الممارسة الجنسية بالتراضى بين البالغين ليست مسيئة أو فاضحة . و المشاهدة, القرائة أو تحميل المحتوى الصريح لاينتهك معايير المجتمع الخاص بى, المدينة أو الولاية أو الدولة . \n\nالمحتوى الجنسى الصريح الذى أقوم بمشاهدته هو لاستخدامى الشخصى ولن أقوم بعرضه على الصغار .\n\nانا المسؤول الوحيد عن أى عواقب قانونية تتبع مشاهدة , قرائة او تحميل أى محتوى على هذا الموقع. وعلاوة على ذلك فإن الموقع وأى من الكيانات التابعة له لن تتحمل أية مسؤولية بخصوص أآ عواقب قانونية تنشأ من الدخول المزور أو استخدام هذا الموقع. \n\nشاشة الموافقة هذة تشكل اتفاقية قانونية بين هذا الموقع وبينك و \\ أو أى اعمال لديك فيها مصلحة قانونية. إذا اعتبر أى جزء من تلك الاتفاقية غير قابل للتنفيذ من قبل محكمة مختصة . فلايجب ان تؤثر على قابلية تنفيذ أى من الأجزاء الأخرى من الاتفاقية.", + ], + "By clicking in \"I Agree\" you declare that you have read and understood all the conditions mentioned above." => [ + 0 => "عن طريق النقر \"أوافق\" أنت تعلن أنك قد قرأت وفهمت كل الشروط المذكورة.", + ], + "I Agree" => [ + 0 => "أنا موافق", + ], + "Disk used" => [ + 0 => "القرص المستخدم", + ], + "Add user" => [ + 0 => "إضافة عضو", + ], + "Role" => [ + 0 => "الصلاحية", + ], + "Administrator" => [ + 0 => "المتحكم الرئيسي", + ], + "This setting is always diabled when using personal website mode." => [ + 0 => "دائما تعطيل هذا الإعداد عند استخدام وضع موقع شخصي.", + ], + "documentation" => [ + 0 => "التوثيق", + ], + "Learn more about %s at our %d." => [ + 0 => "تعلم أكثر عن %s على %d الخاص بنا.", + ], + "Add category" => [ + 0 => "اضافة فئة", + ], + "Add IP ban" => [ + 0 => "إضافة حظر IP", + ], + "Add storage" => [ + 0 => "إضافة التخزين", + ], + "Return to pages" => [ + 0 => "العودة إلى صفحات", + ], + "Website name" => [ + 0 => "اسم الموقع", + ], + "Website doctitle" => [ + 0 => "عنوان الموقع", + ], + "Website description" => [ + 0 => "وصف الموقع", + ], + "Website keywords" => [ + 0 => "كلمات الموقع", + ], + "Default time zone" => [ + 0 => "المنطقة الزمنية الافتراضية", + ], + "Select region" => [ + 0 => "اختر المنطقة", + ], + "Allows to search images, albums and users based on a given search query." => [ + 0 => "يسمح للبحث في الصور والألبومات والمستخدمين استنادا إلى استعلام بحث معين.", + ], + "Enables to browse public uploaded images. It also enables categories." => [ + 0 => "تمكين التصفح للصور العامة التي تم تحميلها . كما أنها تمكن الفئات.", + ], + "Enables to browse images randomly." => [ + 0 => "تمكين لتصفح الصور بشكل عشوائي.", + ], + "Likes" => [ + 0 => "الإعجابات", + ], + "Allows users to like content and populate \"Most liked\" listings." => [ + 0 => "السماح للمستخدمين بالإعجاب بالمحتوى و نشر \" القوائم الأكثر إعجابا \"", + ], + "Followers allows users to follow each other." => [ + 0 => "يسمح المتابعون للمستخدمين بمتابعة بعضهم البعض.", + ], + "Personal mode target user" => [ + 0 => "وضع المستخدم الشخصى ", + ], + "User ID" => [ + 0 => "رقم العضو", + ], + "Your user id is: %s" => [ + 0 => "رقم الهوية الخاص بك هو: %s", + ], + "Numeric ID of the target user for personal mode." => [ + 0 => "الهوية الرقمية للمستخدم المستهدف للوضع الشخصى.", + ], + "Personal mode routing" => [ + 0 => "توجيه الوضع الشخصى", + ], + "Custom route to map /username to /something. Use \"/\" to map to homepage." => [ + 0 => "توجيه مخصص لترسيم / المستخدم إلى / شيئ ما. استخدم \" /\" لترسيم الصفحة الشخصية. ", + ], + "Website privacy mode" => [ + 0 => "وضع خصوصية الموقع", + ], + "Private mode will make the website only available for registered users." => [ + 0 => "الوضع الخاص سيجعل الموقع يظهر فقط للاعضاء المسجلين.", + ], + "Content privacy mode" => [ + 0 => "وضع المحتوى الخاص", + ], + "Default" => [ + 0 => "الإفتراضي", + ], + "Force private (self)" => [ + 0 => "إجبار الوضع الخاص (الذاتي)", + ], + "Force private (anyone with the link)" => [ + 0 => "إجبار الوضع الخاص (أي شخص لديه الرابط)", + ], + "Forced privacy modes will override user selected privacy." => [ + 0 => "الوضع الخاص يجبر على تجاوز خيار المستخدم في اختيار الخصوصية.", + ], + "Page title" => [ + 0 => "عنوان الصفحة", + ], + "Page status" => [ + 0 => "حالة الصفحة", + ], + "Active page" => [ + 0 => "الصفحة النشطة", + ], + "Inactive page (%s)" => [ + 0 => "صفحة غير نشطة (%s)", + ], + "Only active pages will be accessible." => [ + 0 => "يمكن الوصول فقط للصفحات النشطة", + ], + "Type" => [ + 0 => "اكتب", + ], + "Page visibility" => [ + 0 => "إمكانية رؤية الصفحة", + ], + "Visible page" => [ + 0 => "صفحة مرئية", + ], + "Hidden page" => [ + 0 => "صفحة مخفية", + ], + "Hidden pages won't be show in system menus, but anyone can access to it with the link." => [ + 0 => "لن تظهر الصفحات المخفية فى قوائم النظام, لكن يمكن لأى كان الوصول إليها عبر الرابط", + ], + "Only alphanumerics, hyphens and forward slash" => [ + 0 => "مسموح فقط فالأبجدية الرقمية والواصلات والخطوط المائلة إلى الأمام", + ], + "File path" => [ + 0 => "مسار الملف", + ], + "A %f file relative to %s" => [ + 0 => "هناك ملف %f مرتبط بـ %s", + ], + "Meta keywords" => [ + 0 => "الكلمات المفتاحية الوصفية", + ], + "Meta description" => [ + 0 => "وصف الميتا", + ], + "Source code" => [ + 0 => "الكود المصدرى", + ], + "No write permission in %s path you will need to add this file using an external editor." => [ + 0 => "لايوجد اذن كتابة فى مسار %s يجب أن تضيف هذا الملف باستخدام أداة تحرير خارجية.", + ], + "No write permission in %s you will need to edit the contents of this file using an external editor." => [ + 0 => "لايوجد اذن كتابة فى مسار %s يجب أن تعدل محتوى هذا الملف باستخدام أداة تحرير خارجية.", + ], + "Taken from: %s" => [ + 0 => "مأخوذ من: %s", + ], + "Link URL" => [ + 0 => "رابط الصفحة", + ], + "Link target attribute" => [ + 0 => "خاصية هدف الرابط", + ], + "Select %s to open the page or link in a new window." => [ + 0 => "اختر %s لفتح الصفحة أو الرابط فى نافذة جديدة", + ], + "Link rel attribute" => [ + 0 => "خاصية ارتباط الرابط", + ], + "Only alphanumerics, hyphens and whitespaces" => [ + 0 => "مسموح فقط بالأبجدية الرقمية والواصلات والمسافات", + ], + "HTML <a> %s attribute" => [ + 0 => "خاصية <a> %s HTML", + ], + "Link icon" => [ + 0 => "أيقونة الرابط", + ], + "Check the icon reference for the complete list of supported icons." => [ + 0 => "اختر مرجعية الأيقونات للقائمة الكاملة للأيقونات المدعومة ", + ], + "Sort order display" => [ + 0 => "ترتيب العرض", + ], + "Page sort order display for menus and listings. Use \"1\" for top priority." => [ + 0 => "ترتيب العرض فى الصفحة للقوائم . استخدم \"1\" للأولوية الأعلى", + ], + "pages" => [ + 0 => "صفحات", + ], + "Do you really want to delete the page ID %s? This can't be undone." => [ + 0 => "هل تريد حذف هوية الصورة %s ؟ لايمكن التراجع عن هذا الإجراء", + ], + "Unchecked image formats won't be allowed to be uploaded." => [ + 0 => "تنسيقات الصور غير المحددة لن يسمح بتحميلها.", + ], + "Enable uploads" => [ + 0 => "تفعيل الرفع", + ], + "Enable this if you want to allow image uploads. This setting doesn't affect administrators." => [ + 0 => "قم بتفعيل هذه الخاصية إذا أردت فتح ميزة الرفع ، هذه الخاصية لا تؤثر على المديرين.", + ], + "Guest uploads" => [ + 0 => "تحميلات الزوار", + ], + "Enable this if you want to allow non registered users to upload." => [ + 0 => "فعل هذا الخيار إن كنت تريد السماح لغير المسجلين برفع الصور.", + ], + "Enable embed codes (uploader)" => [ + 0 => "تفعيل تضمين الرموز (رافع)", + ], + "Enable this if you want to show embed codes when upload gets completed." => [ + 0 => "قم بتفعيل هذا الخيار إذا كنت تريد أن تظهرالرموز المضمنة بعد انتهاء التحميل.", + ], + "Upload threads" => [ + 0 => "Upload threads", + ], + "Number of simultaneous upload threads (parallel uploads)" => [ + 0 => "عدد سلاسل التحميل المتزامنة (التحميل المتوازي)", + ], + "Redirect on single upload" => [ + 0 => "إعادة التوجيه عند الرفع المفرد", + ], + "Enable this if you want to redirect to image page on single upload." => [ + 0 => "قم بتفعيل هذا لإعادة التوجيه للصفحة الرئيسية عند الرفع المفرد.", + ], + "Enable duplicate uploads" => [ + 0 => "تفعيل الرفع المزدوج", + ], + "Enable this if you want to allow duplicate uploads from the same IP within 24hrs. This setting doesn't affect administrators." => [ + 0 => "تفعيل هذا إذا اردت السماح بالرفع المزدوج من نفس الأى بى فى خلال 24 ساعة. هذا الإعداد لايؤثر على الإداريين.", + ], + "Enable expirable uploads" => [ + 0 => "تمكين المرفوعات القابلة للانتهاء", + ], + "Enable this if you want to allow uploads with an automatic delete option." => [ + 0 => "تفعيل هذا إذا اردت أن تسمح برفوعات ذات خاصية المسح التلقائى.", + ], + "Auto delete guest uploads" => [ + 0 => "حذف تحميلات الضيف تلقائيا", + ], + "Enable this if you want to force guest uploads to be auto deleted after certain time." => [ + 0 => "تمكين هذا إذا كنت ترغب في حذف تحميلات الضيوف بعد وقت معين.", + ], + "Maximum image size" => [ + 0 => "الحد الأقصى لحجم الصورة", + ], + "Images greater than this size will get automatically downsized. Use zero (0) to don't set a limit." => [ + 0 => "سيتم تصغير حجم الصور التي تزيد عن هذا الحجم تلقائيا. استخدم صفر (0) لتجاهل هذا.", + ], + "Image Exif data" => [ + 0 => "بيانات وصف الصورة", + ], + "Keep" => [ + 0 => "الاحتفاظ", + ], + "Select the default setting for image Exif data on upload." => [ + 0 => "اختر الإعدادات الافتراضية لبيانات وصف الصورة عند الرفع.", + ], + "Image Exif data (user setting)" => [ + 0 => "بيانات وصف الصورة ( إعداد المستخدم )", + ], + "Enable this if you want to allow each user to configure how image Exif data will be handled." => [ + 0 => "تفعيل هذا إذا اردت ان تسمح لكل مستخدم بضبط كيفية إدارة بيانات الصورة الوصفية.", + ], + "Maximum upload file size" => [ + 0 => "الحد الأقصى لحجم ملف التحميل", + ], + "Image path" => [ + 0 => "مسار الصورة", + ], + "Relative to Chevereto root" => [ + 0 => "متعلق بـ Chevereto root", + ], + "Where to store the images? Relative to Chevereto root." => [ + 0 => "اين يتم حفظ الصور؟ المرتبطة ب Chevereto root.", + ], + "Storage mode" => [ + 0 => "وضع التخزين", + ], + "Datefolders" => [ + 0 => "ملفات التواريخ", + ], + "Direct" => [ + 0 => "مباشر", + ], + "Datefolders creates %s structure" => [ + 0 => "انشاء تاريخ الملفات %s الهيكلية", + ], + "File naming method" => [ + 0 => "طريقة تسمية الملف", + ], + "Original" => [ + 0 => "الأصلي", + ], + "Mix original + random" => [ + 0 => "خلط الأصلى + العشوائى", + ], + "\"Original\" will try to keep the image source name while \"Random\" will generate a random name. \"ID\" will name the image just like the image ID." => [ + 0 => "\" الأصلى\" سيحاول الحفاظ على اسم الصورة من المصدر فى حين \" العشوائى\" سينتج اسما عشوائيا. \" الهوية\" ستسمى الصورة بمثل هوية الصورة.", + ], + "Thumb size" => [ + 0 => "حجم الصورة المصغرة", + ], + "Thumbnails will be fixed to this size." => [ + 0 => "الصورة المصغرة سوف يتم تثبيتها لهذا الحجم.", + ], + "Medium image fixed dimension" => [ + 0 => "ابعاد ثابتة للصورة المتوسطة", + ], + "Medium sized images will be fixed to this dimension. For example, if you select \"width\" that dimension will be fixed and image height will be automatically calculated." => [ + 0 => "ستثبت الصور متوسطة الحج إلى هذة الأبعاد. على سبيل المثال, إذا اخترت \"العرض\" سيتم تثبيت هذا البعد ويتم حساب الارتفاع تلقائيا", + ], + "Medium image fixed size" => [ + 0 => "حجم ثابت للصورة المتوسطة", + ], + "Width or height will be automatically calculated." => [ + 0 => "سيتم حساب العرض والارتفاع تلقائيا", + ], + "Watermarks" => [ + 0 => "علامات مائية", + ], + "Enable this to put a logo or anything you want in image uploads." => [ + 0 => "تفعيل هذا الخيار يسمح بوضع شعار أو أي شيء تريده في رفع الصور.", + ], + "Warning: Can't write in %s" => [ + 0 => "تحذير: لا يمكن الكتابة في %s", + ], + "Watermark user toggles" => [ + 0 => "تبديلات علامات المستخدم المائية", + ], + "Enable watermark on guest uploads" => [ + 0 => "تفعيل العلامات المائية على مرفوعات الضيوف", + ], + "Enable watermark on user uploads" => [ + 0 => "تفعيل العلامات المائية على مرفوعات المستخدمين", + ], + "Enable watermark on admin uploads" => [ + 0 => "تفعيل العلامة المائية على مرفوعات المديرين", + ], + "Watermark file toggles" => [ + 0 => "تبديلات ملف العلامة المائية", + ], + "Enable watermark on GIF image uploads" => [ + 0 => "تفعيل العلامة المائية على مرفوعات صور بصيغة GIF", + ], + "Minimum image size needed to apply watermark" => [ + 0 => "ادنى حجم مطلوب لتطبيق العلامة المائية", + ], + "Images smaller than this won't be watermarked. Use zero (0) to don't set a minimum image size limit." => [ + 0 => "الصور الأصغر من الحد الادنى لن تكون ذات علامة مائية . استخدم صفر (0) حتى لا يتم ضبط حدا ادنى للحجم.", + ], + "Watermark image" => [ + 0 => "صورة بعلامة مائية", + ], + "You will get best results with plain logos with drop shadow. You can use a large image if the file size is not that big (recommended max. is 16KB). Must be a PNG." => [ + 0 => "ستحصل على أفضل النتائج بالشعارات الواضحة مع إضافة ظل. يمكنك استخدام صور كبيرة إذا لم يكن حجم الملف كبيرا ( الحد الأقصى الموصى به هو 16كب). ويجب ان تكون فى صيغة PNG.", + ], + "Watermark position" => [ + 0 => "موقع العلامة المائية", + ], + "left top" => [ + 0 => "يسار/فوق", + ], + "left center" => [ + 0 => "يسار/وسط", + ], + "left bottom" => [ + 0 => "يسار/أسفل", + ], + "center top" => [ + 0 => "وسط/فوق", + ], + "center center" => [ + 0 => "وسط/وسط", + ], + "center bottom" => [ + 0 => "وسط/أسفل", + ], + "right top" => [ + 0 => "يمين/فوق", + ], + "right center" => [ + 0 => "يمين/وسط", + ], + "right bottom" => [ + 0 => "يمين/أسفل", + ], + "Relative position of the watermark image. First horizontal align then vertical align." => [ + 0 => "مكان وضع الصورة المائية. محاذاة أفقية أولاَ ثم محاذاة عمودية.", + ], + "Watermark percentage" => [ + 0 => "نسبة العلامة المائية", + ], + "Watermark percentual size relative to the target image area. Values 1 to 100." => [ + 0 => "حجم العلامة المائية التناسبى مع مساحة الصورة. القيم من 1 إلى 100", + ], + "Watermark margin" => [ + 0 => "هامش العلامة المائية", + ], + "Margin from the border of the image to the watermark image." => [ + 0 => "تحديد الهامش من حدود الصورة لتظهر الصورة المائية عليها.", + ], + "Watermark opacity" => [ + 0 => "درجة الوضوح للعلامة المائية", + ], + "Opacity of the watermark in the final watermarked image. Values 0 to 100." => [ + 0 => "درجة وضوح العلامة المائية في الصورة النهائية. القيم 0 إلى 100.", + ], + "Dashboard > Settings > Website" => [ + 0 => "لوحة التحكم>الإعدادات>الموقع", + ], + "Categories won't work when the explorer feature is turned off. To revert this setting go to %s." => [ + 0 => "لن تعمل الفئات عند إغلاق ميزة التصفح. لتبديل هذا الإعداد اذهب إلى %s", + ], + "Do you really want to delete the %s category? This can't be undone." => [ + 0 => "هل انت متأكد من حذف مساحة %s التخزين هذه؟ لايمكن التراجع بعد الحذف.", + ], + "Note: Deleting a category doesn't delete the images that belongs to that category." => [ + 0 => "ملاحظة: حذف الفئة لا يحذف الصور الموجودة بداخل هذه الفئة.", + ], + "Edit category" => [ + 0 => "تعديل الفئة", + ], + "Expires" => [ + 0 => "انتهاء الصلاحية", + ], + "Do you really want to remove the ban to the IP %s? This can't be undone." => [ + 0 => "هل تريد حذف المنع من الأى بى %s ؟ هذا الإجراء لايمكن التراجع عنه", + ], + "Banned IP address will be forbidden to use the entire website." => [ + 0 => "الأى بى الممنوع سيحرم من استخدام الموقع بالكامل.", + ], + "Edit IP ban" => [ + 0 => "تعديل منع الأى بى ", + ], + "Enable signups" => [ + 0 => "تفعيل التسجيل", + ], + "Enable this if you want to allow users to signup." => [ + 0 => "قم بتنشيط هذا الخيار إذا كنت ترغب في تفعيل ميزة التسجيل.", + ], + "Minimum age required" => [ + 0 => "السن الأدنى مطلوب", + ], + "Empty" => [ + 0 => "فارغ", + ], + "Leave it empty to don't require a minimum age to use the website." => [ + 0 => "اتركه فارغا حتى لايطلب عمرا أدنى لاستخدام الموقع", + ], + "Username routing" => [ + 0 => "تحويل اسم المستخدم", + ], + "Enable this if you want to use %s/username URLs instead of %s/user/username." => [ + 0 => "تفعيل هذا الإختيار إذا أردت إعتماد %s رابط اسم المستخدم بدلامن %s إسم المستخدم.", + ], + "Require email confirmation" => [ + 0 => "تحتاج إلى تأكيد البريد الإلكتروني", + ], + "Enable this if users must validate their email address on sign up." => [ + 0 => "فعل هذا الإختيار إذا أردت من المستخدمين التحقق من صحة عنوان البريد الإلكتروني الخاصة بهم على قبل الاشتراك.", + ], + "Require email for social signup" => [ + 0 => "طلب البريد الإلكتروني للتسجيل بإستخدام الشبكات الاجتماعية", + ], + "Enable this if users using social networks to register must provide an email address." => [ + 0 => "تمكين هذا الإختيار إذا أردت من المستخدمين إستخدام الشبكات الاجتماعية للتسجيل وتوفير عنوان البريد الإلكتروني الخاصة بهم.", + ], + "User avatar max. filesize" => [ + 0 => "صورة المستخدم أقصى حجم للملف", + ], + "Max. allowed filesize for user avatar image. (Max allowed by server is %s)" => [ + 0 => "أقصى حجم ملف مسموح به لصورة الأفاتار. ( الحد الأقصى المسموح به على الخادم هو %s )", + ], + "User background max. filesize" => [ + 0 => "أقصى حجم لصورة خلفية المستخدم", + ], + "Max. allowed filesize for user background image. (Max allowed by server is %s)" => [ + 0 => "أقصى حجم ملف لصورة خلفية المستخدم. ( أقصى حجم مسموح فى الخادم هو %s )", + ], + "Shows a consent screen before accessing the website. Useful for adult content websites where minors shouldn't be allowed." => [ + 0 => "إظهار شاشة موافقة قبل الدخول إلى الموقع. فهى مفيدة بالنسبة لمواقع البالغين حيث لايسمح للصغار الدخول.", + ], + "Enable consent screen" => [ + 0 => "تفعيل شاشة الموافقة", + ], + "Consent screen cover image" => [ + 0 => "صورة غلاف شاشة الموافقة", + ], + "Block image uploads by IP if the system notice a flood behavior based on the number of uploads per time period. This setting doesn't affect administrators." => [ + 0 => "هذا الخيار سوف يقوم بحجب الأي بي للمستخدم الذي يقوم برفع صور خلال وقت أكثر من الحد المسموح به ، هذا الخيار لا يؤثر على مدير الموقع.", + ], + "Notify to email" => [ + 0 => "الإشعار عبر البريد الإلكتروني", + ], + "If enabled the system will send an email on flood incidents." => [ + 0 => "إذا تم تنشيط هذا الخيار سوف يقوم النظام بإرسال رسالة تحذيرية إلى البريد الإلكتروني.", + ], + "Minute limit" => [ + 0 => "الحد دقيقة", + ], + "Hourly limit" => [ + 0 => "الحد ساعة", + ], + "Daily limit" => [ + 0 => "الحد يوم", + ], + "Weekly limit" => [ + 0 => "الحد أسبوع", + ], + "Monthly limit" => [ + 0 => "الحد شهر", + ], + "Show not safe content in listings" => [ + 0 => "إستعرض المحتويات الغير آمنة في القوائم", + ], + "Enable this if you want to show not safe content in listings. This setting doesn't affect administrators and can be overridden by user own settings." => [ + 0 => "تفعيل هذا عندما تريد إظهار قوائم المحتوى الغير آمن. هذا النوع من الضبط لايؤثر على الإداريين ويمكن تخطيه بواسطة إعدادات المستخدم.", + ], + "Blur NSFW content in listings" => [ + 0 => "طمس المحتوى الغير أمن NSFW فى القوائم", + ], + "Enable this if you want to apply a blur effect on the NSFW images in listings." => [ + 0 => "تفعيل هذا إذا اردت أن تطبق تأثير الطمس على صور المحتوى الغير آمن فى القوائم", + ], + "Show banners in not safe content" => [ + 0 => "أستعرض البانر في المحتويات الغير آمنة", + ], + "Enable this if you want to show banners in not safe content pages." => [ + 0 => "تمكين هذا الخيار إذا كنت تريد أن يظهر البانر في صفحات ذات المحتوى الغير آمن.", + ], + "Show not safe content in random mode" => [ + 0 => "اظهار المحتوى الغير آمن في الوضع العشوائي", + ], + "List items per page" => [ + 0 => "العناصر القائمة لكل صفحة", + ], + "How many items should be displayed per page listing." => [ + 0 => "كم عدد العناصر التي ستظهر في كل صفحة من القائمة.", + ], + "List pagination mode" => [ + 0 => "نسق ترقيم الصفحات", + ], + "Endless scrolling" => [ + 0 => "تمرير لانهائي", + ], + "Classic pagination" => [ + 0 => "ترقيم تقليدي", + ], + "What pagination method should be used." => [ + 0 => "أي نسق ترغب في إستعراضه في ترقيم الصفحات", + ], + "Image listing size" => [ + 0 => "حجم عرض الصور", + ], + "Fluid" => [ + 0 => "سلس", + ], + "Fixed" => [ + 0 => "محدد", + ], + "Both methods use a fixed width but fluid method uses automatic heights." => [ + 0 => "كلتا الطريقتين تستخدم عرض ثابت ولكن يستخدم الأسلوب السلس ارتفاع تلقائي.", + ], + "Listing columns number" => [ + 0 => "عرض رقم العامود", + ], + "Here you can set how many columns are used based on each target device." => [ + 0 => "هنا يمكنك ضبط عدد العواميد المستخدمة بناء على كل جهاز مستهدف", + ], + "Phone" => [ + 0 => "الهاتف", + ], + "Phablet" => [ + 0 => "الهاتف اللوحى", + ], + "Tablet" => [ + 0 => "الجهاز اللوحى", + ], + "Laptop" => [ + 0 => "الحاسب الشخصى", + ], + "Desktop" => [ + 0 => "الحاسب المكتبى", + ], + "Put your themes in the %s folder" => [ + 0 => "ضع السمات الخاصة بك في هذا %s المجلد", + ], + "Tone" => [ + 0 => "اسلوب", + ], + "Light" => [ + 0 => "فاتح", + ], + "Dark" => [ + 0 => "غامق", + ], + "Main color" => [ + 0 => "اللون الرئيسى", + ], + "Hexadecimal color value" => [ + 0 => "قيمة اللون بنظام الهكسا", + ], + "Use this to set the main theme color. Value must be in hex format." => [ + 0 => "استخدم هذا لضبط لون السمة الرئيسي. يجب ان تكون القيمة بنظام الهكسا", + ], + "Top bar color" => [ + 0 => "لون الشريط العلوى", + ], + "Black" => [ + 0 => "أسود", + ], + "White" => [ + 0 => "أبيض", + ], + "If you set this to \"white\" the top bar and all the black tones will be changed to white tones." => [ + 0 => "إذا تم ضبط هذا على \" الأبيض\" فإن الشريط العلوى وكل الأساليب السوداء ستتغير إلى الأساليب البيضاء؟", + ], + "Top bar button color" => [ + 0 => "لون زر الشريط العلوى", + ], + "Blue" => [ + 0 => "أزرق", + ], + "Green" => [ + 0 => "أخضر", + ], + "Orange" => [ + 0 => "برتقالى", + ], + "Red" => [ + 0 => "أحمر", + ], + "Grey" => [ + 0 => "رمادى", + ], + "Color for the top bar buttons like the \"Create account\" button." => [ + 0 => "لون أزرار الشريط العلوى مثل زر\" إنشاء حساب\" ", + ], + "Enable vector logo" => [ + 0 => "تفعيل الشعار الفيكتور", + ], + "Enable vector logo for high quality logo in devices with high pixel density." => [ + 0 => "الموافقة على الشعارعالي الجودة في الأجهزة ذات كثافة بيكسل عالية.", + ], + "Vector logo image" => [ + 0 => "شعار الفيكتور للصورة", + ], + "Vector version or your website logo in SVG format." => [ + 0 => "الصور من نوع Vector امتدادها SVG .", + ], + "Raster logo image" => [ + 0 => "الشعار النقطي للصورة", + ], + "Bitmap version or your website logo. PNG format is recommended." => [ + 0 => "الصور من نوع Bitmap او شعار موقعك. يوصى بامتداد PNG.", + ], + "Logo height" => [ + 0 => "إرتفاع الشعار", + ], + "No value" => [ + 0 => "لا يوجد قيمة", + ], + "Use this to set the logo height if needed." => [ + 0 => "إستخدام هذا لتعيين ارتفاع الشعار إذا لزم الأمر.", + ], + "Favicon image" => [ + 0 => "أيقونة الصورة", + ], + "Favicon image. Image must have same width and height." => [ + 0 => "صورة الشعار بجنب الرابط (Favicon) يجب أن تكون بنفس الطول والعرض.", + ], + "Enable download button" => [ + 0 => "تفعيل أيقونة التحميل", + ], + "Enable this if you want to show the image download button." => [ + 0 => "قم بتنشيط هذا الخيار إذا كنت ترغب في إظهار أيقونة تحميل الصورة.", + ], + "Enable right click on image" => [ + 0 => "تمكين الضغطة اليمنى على الصورة", + ], + "Enable this if you want to allow right click on image viewer page." => [ + 0 => "تمكين هذا إذا اردت السماح بالضغطة اليمنى على صفحة مشاهدة الصورة", + ], + "Enable show Exif data" => [ + 0 => "تمكين إظهار بيانات الصورة", + ], + "Enable this if you want to show image Exif data." => [ + 0 => "تمكين هذا إذا اردت أن تظهر بيانات الصورة", + ], + "Enable social share" => [ + 0 => "تفعيل خاصية المشاركة الإجتماعية", + ], + "Enable this if you want to show social network buttons to share content." => [ + 0 => "قم بتفعيل هذا الخيار إذا كنت تريد أن تظهر أزرار الشبكات الاجتماعية لمشاركة المحتوى.", + ], + "Enable embed codes (content)" => [ + 0 => "تفعيل تضمين الرموز (المحتوى)", + ], + "Enable this if you want to show embed codes for the content." => [ + 0 => "قم بتفعيل هذا الخيار إذا كنت تريد أن تظهررموز المحتوى.", + ], + "Not safe content checkbox in uploader" => [ + 0 => "قم بالاشارة على المربع عند الرفع اذا كان محتوى الصور غير آمن", + ], + "Enable this if you want to show a checkbox to indicate not safe content upload." => [ + 0 => "تمكين هذا الخيار إذا كنت تريد أن يظهر مربع للإشارة إلى ان المحتوى غير آمن عند رفع الصور.", + ], + "Custom CSS code" => [ + 0 => "اضافة CSS مخصص", + ], + "Put your custom CSS code here. It will be placed as '; + $handler::setVar('plugin', $plugin); + $handler::setVar('pre_doctitle', _s('Upload plugin')); +}; diff --git a/app/legacy/routes/redirect.php b/app/legacy/routes/redirect.php new file mode 100644 index 0000000..46534d6 --- /dev/null +++ b/app/legacy/routes/redirect.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function Chevereto\Legacy\decryptString; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\is_url_web; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Vars\get; + +/** + * This is an internal redirection try to avoid spammers trying to get some + * SEO juice by putting links all over your Chevereto website. + * + * It also avoids spammers to use this redirection to hang spam and whatnot in + * third-party websites (auth_token stuff). + * + * The redirection is only issued if the URL was generated by crypt(). + */ +return function (Handler $handler) { + $encrypted = get()['to'] ?? null; + if ($encrypted === null) { + $handler->issueError(404); + + return; + } + $url = decryptString($encrypted); + $validations = [ + is_url_web($url), + $handler::checkAuthToken(get()['auth_token'] ?? '') + ]; + if (in_array(false, $validations)) { + $handler->issueError(404); + + return; + } + redirect($url, 302); +}; diff --git a/app/legacy/routes/search.php b/app/legacy/routes/search.php new file mode 100644 index 0000000..0d98120 --- /dev/null +++ b/app/legacy/routes/search.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Chevereto\Legacy\Classes\Listing; +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\Search; +use Chevereto\Legacy\Classes\User; +use function Chevereto\Legacy\G\check_value; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\get_share_links; +use function Chevereto\Vars\env; +use function Chevereto\Vars\post; +use function Chevereto\Vars\request; + +return function (Handler $handler) { + if (!(bool) env()['CHEVERETO_ENABLE_USERS']) { + $handler->issueError(403); + + return; + } + if ($handler::cond('search_enabled') == false) { + $handler->issueError(404); + + return; + } + if (post() !== [] && !$handler::checkAuthToken(request()['auth_token'] ?? '')) { + $handler->issueError(403); + + return; + } + if ($handler->isRequestLevel(4)) { + $handler->issueError(404); + + return; + } // Allow only 3 levels + if (is_null($handler->request()[0] ?? null)) { + $handler->issueError(404); + + return; + } + $logged_user = Login::getUser(); + User::statusRedirect($logged_user['status'] ?? null); + if (!in_array($handler->request()[0], ['images', 'albums', 'users'])) { + $handler->issueError(404); + + return; + } + $search = new Search(); + $search->q = request()['q'] ?? null; + $search->type = $handler->request()[0]; + $search->request = request(); + $search->requester = Login::getUser(); + $search->build(); + if (!check_value($search->q)) { + redirect(); + + return; + } + $safe_html_search = safe_html($search->display); + + try { + $getParams = Listing::getParams(request()); + $handler::setVar('list_params', $getParams); + $listing = new Listing(); + $listing->setType($search->type); + if (isset($getParams['reverse'])) { + $listing->setReverse($getParams['reverse']); + } + if (isset($getParams['seek'])) { + $listing->setSeek($getParams['seek']); + } + $listing->setOffset($getParams['offset']); + $listing->setLimit($getParams['limit']); // how many results? + $listing->setSortType($getParams['sort'][0]); // date | size | views + $listing->setSortOrder($getParams['sort'][1]); // asc | desc + $listing->setWhere($search->wheres); + $listing->setRequester(Login::getUser()); + foreach ($search->binds as $v) { + $listing->bind($v['param'], $v['value']); + } + $listing->setOutputTpl($search->type); + $listing->exec(); + $handler::setVar('listing', $listing); + } catch (Exception $e) { + $getParams = []; + } + $tabs = Listing::getTabs([ + 'listing' => 'search', + 'basename' => 'search', + 'params' => ['q' => $safe_html_search['q'], 'page' => '1'], + 'params_remove_keys' => ['sort'], + ], $getParams); + foreach ($tabs as $k => &$v) { + $v['current'] = $v['type'] == $search->type; + } + $meta_description = ''; + switch ($search->type) { + case 'images': + $meta_description = _s('Image search results for %s'); + + break; + case 'albums': + $meta_description = _s('Album search results for %s'); + + break; + case 'users': + $meta_description = _s('User search results for %s'); + + break; + } + $handler::setVar('pre_doctitle', $search->q . ' - ' . _s('Search')); + $handler::setVar('meta_description', sprintf($meta_description, $safe_html_search['q'])); + $handler::setVar('search', $search->display); + $handler::setVar('safe_html_search', $safe_html_search); + $handler::setVar('tabs', $tabs); + if ($handler::cond('content_manager')) { + $handler::setVar('user_items_editor', false); + } + $handler::setVar('share_links_array', get_share_links()); +}; diff --git a/app/legacy/routes/settings.php b/app/legacy/routes/settings.php new file mode 100644 index 0000000..73c4b0d --- /dev/null +++ b/app/legacy/routes/settings.php @@ -0,0 +1,573 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Chevereto\Legacy\Classes\Akismet; +use Chevereto\Legacy\Classes\ApiKey; +use Chevereto\Legacy\Classes\Confirmation; +use Chevereto\Legacy\Classes\DB; +use Chevereto\Legacy\Classes\Image; +use Chevereto\Legacy\Classes\IpBan; +use Chevereto\Legacy\Classes\L10n; +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\RequestLog; +use Chevereto\Legacy\Classes\Settings; +use Chevereto\Legacy\Classes\TwoFactor; +use Chevereto\Legacy\Classes\User; +use function Chevereto\Legacy\G\array_filter_array; +use function Chevereto\Legacy\G\dateinterval; +use function Chevereto\Legacy\G\datetime_diff; +use function Chevereto\Legacy\G\get_base_url; +use function Chevereto\Legacy\G\get_public_url; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\is_url_web; +use function Chevereto\Legacy\G\nullify_string; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\timing_safe_compare; +use function Chevereto\Legacy\generate_hashed_token; +use function Chevereto\Legacy\get_available_languages; +use function Chevereto\Legacy\getIpButtonsArray; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\getSettings; +use function Chevereto\Legacy\send_mail; +use function Chevereto\Vars\env; +use function Chevereto\Vars\post; +use function Chevereto\Vars\request; +use function Chevereto\Vars\session; +use function Chevereto\Vars\sessionVar; + +return function (Handler $handler) { + $POST = post(); + if ($POST !== [] and !$handler::checkAuthToken(request()['auth_token'] ?? '')) { + $handler->issueError(403); + + return; + } + $logged_user = Login::getUser(); + if ($logged_user === []) { + redirect('login'); + } + User::statusRedirect($logged_user['status'] ?? null); + $handler->setTemplate('settings'); + $is_dashboard_user = $handler::cond('dashboard_user'); + if (!$is_dashboard_user) { + RequestLog::getCounts('account-edit', 'fail'); + } + $allowed_to_edit = ['name', 'username', 'email', 'avatar_filename', 'website', 'background_filename', 'timezone', 'language', 'image_keep_exif', 'image_expiration', 'newsletter_subscribe', 'bio', 'show_nsfw_listings', 'is_private', 'status']; + if ($is_dashboard_user) { + $allowed_to_edit = array_merge($allowed_to_edit, ['is_admin', 'is_manager']); + } + if (!getSetting('enable_expirable_uploads')) { + $key = array_search('image_expiration', $allowed_to_edit); + unset($allowed_to_edit[$key]); + } + $user = $is_dashboard_user + ? User::getSingle($handler->request()[1], 'id') + : $logged_user; + if ($user === []) { + $handler->issueError(404); + + return; + } + $is_owner = $user['id'] == Login::getUser()['id']; + if ($is_dashboard_user && $user['is_content_manager'] && Login::isAdmin() == false) { + $handler->issueError(404); + + return; + } + if (in_array('language', $allowed_to_edit) + && isset($POST['language']) + && $logged_user['language'] !== $POST['language'] + && $logged_user['id'] == $user['id'] + && array_key_exists($POST['language'], L10n::getEnabledLanguages()) + ) { + L10n::processTranslation($POST['language']); + } + $routes = [ + 'account' => _s('Account'), + 'profile' => _s('Profile'), + 'password' => _s('Password'), + 'security' => _s('Security'), + 'api' => 'API', + 'connections' => _s('Connections'), + 'homepage' => _s('Homepage'), + 'powered' => _s('Powered by'), + ]; + $icons = [ + 'account' => 'fas fa-user', + 'profile' => 'fas fa-id-card', + 'api' => 'fas fa-project-diagram', + 'password' => 'fas fa-key', + 'security' => 'fas fa-shield-alt', + 'connections' => 'fas fa-plug', + 'homepage' => 'fas fa-home', + 'powered' => 'fas fa-power-off', + ]; + $default_route = 'account'; + $route_homepage = false; + if (getSetting('website_mode') == 'personal' and getSetting('website_mode_personal_routing') !== '/' and $logged_user['id'] == getSetting('website_mode_personal_uid')) { + $route_homepage = true; + } + $is_email_required = (bool) getSetting('require_user_email_confirmation'); + if ($handler::cond('content_manager') && $is_owner == false) { + $is_email_required = false; + } + $providersEnabled = Login::getProviders('enabled'); + if ($is_email_required && getSetting('require_user_email_social_signup') == false) { + foreach (array_keys($providersEnabled) as $k) { + if (array_key_exists($k, $user['login'])) { + $is_email_required = false; + + break; + } + } + } + $doing_level = $is_dashboard_user ? 2 : 0; + $doing = $handler->request()[$doing_level] ?? $default_route; + if (!$user || isset($handler->request()[$doing_level + 1]) || (!is_null($doing) and !array_key_exists($doing, $routes))) { + $handler->issueError(404); + + return; + } + if ($doing == '') { + $doing = $default_route; + } + $tabs = []; + foreach ($routes as $route => $label) { + $aux = str_replace('_', '-', $route); + $handler::setCond('settings_' . $aux, $doing == $aux); + if ($handler::cond('settings_' . $aux)) { + $handler::setVar('setting', $aux); + } + if ($aux == 'homepage' and !$route_homepage) { + continue; + } + $tabs[$aux] = [ + 'icon' => $icons[$route], + 'label' => $label, + 'url' => get_base_url( + ($is_dashboard_user ? ('dashboard/user/' . $user['id']) : 'settings') + . ($route == $default_route ? '' : '/' . $route) + ), + 'current' => $handler::cond('settings_' . $aux) + ]; + } + if (count($providersEnabled) == 0 || ($is_dashboard_user && Login::isAdmin() == false)) { + unset($routes['connections']); + $tabs = array_filter_array($tabs, ['connections'], 'rest'); + } + $handler::setVar('tabs', $tabs); + + if (!array_key_exists($doing, $routes)) { + $handler->issueError(404); + + return; + } + $SAFE_POST = $handler::var('safe_post'); + $is_error = false; + $is_changed = false; + $captcha_needed = false; + $input_errors = []; + $error_message = null; + $changed_email_message = null; + if ($POST !== []) { + $field_limits = 255; + foreach ($allowed_to_edit as $k) { + if (isset($POST[$k])) { + $POST[$k] = substr($POST[$k], 0, $field_limits); + } + } + switch ($doing) { + case null: + case 'account': + $checkboxes = ['upload_image_exif', 'newsletter_subscribe', 'show_nsfw_listings', 'is_private']; + foreach ($checkboxes as $k) { + if (!isset($POST[$k])) { + continue; + } + $POST[$k] = in_array($POST[$k], ['On', 1]) ? 1 : 0; + } + nullify_string($POST['image_expiration']); + $__post = []; + $__safe_post = []; + foreach (['username', 'email'] as $v) { + if (isset($POST[$v])) { + $POST[$v] = $v == 'email' ? trim($POST[$v]) : strtolower(trim($POST[$v])); + $__post[$v] = $POST[$v]; + $__safe_post[$v] = safe_html($POST[$v]); + } + } + $handler::updateVar('post', $__post); + $handler::updateVar('safe_post', $__safe_post); + if (!User::isValidUsername($POST['username'])) { + $input_errors['username'] = _s('Invalid username'); + } + if ($is_email_required and !filter_var($POST['email'], FILTER_VALIDATE_EMAIL)) { + $input_errors['email'] = _s('Invalid email'); + } + if (getSetting('enable_expirable_uploads')) { + if ($POST['image_expiration'] !== null && (!dateinterval($POST['image_expiration']) || !array_key_exists($POST['image_expiration'], Image::getAvailableExpirations()))) { + $input_errors['image_expiration'] = _s('Invalid image expiration: %s', $POST['image_expiration']); + } + } + if (getSetting('language_chooser_enable') && !array_key_exists($POST['language'], get_available_languages())) { + $POST['language'] = getSetting('default_language'); + } + if (!in_array($POST['timezone'], timezone_identifiers_list())) { + $POST['timezone'] = date_default_timezone_get(); + } + if (is_array($input_errors) && count($input_errors) > 0) { + $is_error = true; + } + if (!$is_error) { + $user_db = DB::get('users', ['username' => $POST['username'], 'email' => $POST['email']], 'OR', []); + if ($user_db) { + foreach ($user_db as $row) { + if ($row['user_id'] == $user['id']) { + continue; + } // Same guy? + if (!in_array($row['user_status'], ['valid', 'banned'])) { // Don't touch the valid and banned users + $must_delete_old_user = false; + $confirmation_db = Confirmation::get(['user_id' => $row['user_id']]); + if ($confirmation_db !== false) { + if (datetime_diff($confirmation_db['confirmation_date_gmt'], null, 'h') > 48) { + Confirmation::delete(['id' => $confirmation_db['confirmation_id']]); + $must_delete_old_user = true; + } + } else { + $must_delete_old_user = true; + } + if ($must_delete_old_user) { + DB::delete('users', ['id' => $row['user_id']]); + + continue; + } + } + if (timing_safe_compare($row['user_username'], $POST['username']) and $user['username'] !== $row['user_username']) { + $input_errors['username'] = 'Username already being used'; + } + if ( + !empty($POST['email']) && timing_safe_compare($row['user_email'], $POST['email']) && + $user['email'] !== $row['user_email'] + ) { + $input_errors['email'] = _s('Email already being used'); + } + } + if (count($input_errors) > 0) { + $is_error = true; + } + } + } + if (!$is_error && $is_email_required && !empty($POST['email']) && !timing_safe_compare($user['email'] ?? '', $POST['email'])) { + Confirmation::delete(['type' => 'account-change-email', 'user_id' => $user['id']]); + $hashed_token = generate_hashed_token((int) $user['id']); + Confirmation::insert([ + 'type' => 'account-change-email', + 'user_id' => $user['id'], + 'token_hash' => $hashed_token['hash'], + 'status' => 'active', + 'extra' => $POST['email'] + ]); + $email_confirm_link = get_public_url( + 'account/change-email-confirm/?token=' + . $hashed_token['public_token_format'] + ); + $changed_email_message = _s('An email has been sent to %s with instructions to activate this email', $SAFE_POST['email']); + global $theme_mail; + $theme_mail = [ + 'user' => $user, + 'link' => $email_confirm_link + ]; + ob_start(); + require_once PATH_PUBLIC_LEGACY_THEME . 'mails/account-change-email.php'; + $mail_body = ob_get_contents(); + ob_end_clean(); + $mail['subject'] = _s('Confirmation required at %s', getSettings()['website_name']); + $mail['message'] = $mail_body; + send_mail($POST['email'], $mail['subject'], $mail['message']); + unset($POST['email']); + } + + break; + case 'profile': + if (!preg_match('/^.{1,60}$/', $POST['name'] ?? '')) { + $input_errors['name'] = _s('Invalid name'); + } + if (!empty($POST['website'])) { + if (!is_url_web($POST['website'])) { + $input_errors['website'] = _s('Invalid website'); + } + } + if (!$handler::cond('content_manager') && getSetting('akismet')) { + $akismet = new Akismet(); + $isSpam = $akismet->isSpam($POST['bio'], $POST['name'], $user['email'], $POST['website']); + $is_error = $isSpam; + $error_message = _s('Spam detected'); + } + + break; + + case 'password': + if (!$is_dashboard_user) { + if (isset($POST['current-password'])) { + if (!Login::checkPassword($user['id'], $POST['current-password'])) { + $input_errors['current-password'] = _s('Wrong password'); + } + if ($POST['current-password'] == ($POST['new-password'] ?? null)) { + $input_errors['new-password'] = _s('Use a new password'); + $handler::updateVar('safe_post', ['current-password' => null]); + } + } + } + if (!preg_match('/' . getSetting('user_password_pattern') . '/', $POST['new-password'] ?? '')) { + $input_errors['new-password'] = _s('Invalid password'); + } + if ($POST['new-password'] !== $POST['new-password-confirm']) { + $input_errors['new-password-confirm'] = _s("Passwords don't match"); + } + + break; + + case 'security': + if (!TwoFactor::hasFor($user['id']) && sessionVar()->hasKey('two_factor_secret')) { + $twoFactor = new TwoFactor(); + $twoFactor = $twoFactor->withSecret(session()['two_factor_secret']); + sessionVar()->remove('two_factor_secret'); + if (!$twoFactor->verify($POST['two-factor-code'])) { + $input_errors['two-factor-code'] = _s('Invalid code'); + } else { + $twoFactor->insert($user['id']); + } + } + + break; + + case 'homepage': + if (!array_key_exists($doing, $routes)) { + $handler->issueError(404); + + return; + } + $allowed_to_edit = ['homepage_title_html', 'homepage_paragraph_html', 'homepage_cta_html']; + $editing_array = array_filter_array($POST, $allowed_to_edit, 'exclusion'); + $update_settings = []; + foreach ($allowed_to_edit as $k) { + if (!array_key_exists($k, Settings::get()) or Settings::get($k) == $editing_array[$k]) { + continue; + } + $update_settings[$k] = $editing_array[$k]; + } + if ($update_settings !== []) { + $db = DB::getInstance(); + $db->beginTransaction(); + $db->query('UPDATE ' . DB::getTable('settings') . ' SET setting_value = :value WHERE setting_name = :name;'); + foreach ($update_settings as $k => $v) { + $db->bind(':name', $k); + $db->bind(':value', $v); + $db->exec(); + } + if ($db->endTransaction()) { + $is_changed = true; + foreach ($update_settings as $k => $v) { + Settings::setValue($k, $v); + } + } + } + + break; + + default: + $handler->issueError(404); + + return; + + break; + } + if (is_array($input_errors) && count($input_errors) > 0) { + $is_error = true; + } + if (!$is_error) { + if (in_array($doing, [null, 'account', 'profile'])) { + foreach ($POST as $k => $v) { + if (($user[$k] ?? null) !== $v) { + $is_changed = true; + } + } + if ($is_changed) { + $editing_array = array_filter_array($POST, $allowed_to_edit, 'exclusion'); + if (!$is_dashboard_user) { + unset($editing_array['status'], $editing_array['is_admin'], $editing_array['is_manager']); + } else { + if (!in_array($editing_array['status'] ?? null, ['valid', 'banned', 'awaiting-confirmation', 'awaiting-email'])) { + unset($editing_array['status']); + } + if ($logged_user['is_manager']) { + unset($POST['email'], $editing_array['email']); + } + } + if ($logged_user['is_admin'] && isset($POST['role'])) { + $is_manager = 0; + $is_admin = 0; + switch ($POST['role']) { + case 'manager': + $is_manager = 1; + + break; + case 'admin': + $is_admin = 1; + + break; + } + if ($user['is_admin'] != $is_admin) { + $handler::setCond('admin', (bool) $is_admin); + $editing_array['is_admin'] = $is_admin; + } + if ($user['is_manager'] != $is_manager) { + $editing_array['is_manager'] = $is_manager; + } + if ($POST['role'] == 'admin') { + $editing_array['status'] = 'valid'; + } + unset($POST['role']); + } + if (empty($POST['email'])) { + unset($editing_array['email']); + } + if (User::update($user['id'], $editing_array)) { + $user = array_merge($user, $editing_array); + $handler::updateVar('safe_post', [ + 'name' => safe_html($user['name']), + ]); + } + + if (!$is_dashboard_user) { + $logged_user = User::getSingle($user['id']); + } else { + $user = User::getSingle($user['id'], 'id'); + } + $changed_message = _s('Changes have been saved.'); + } + } + if ($doing == 'password') { + if (Login::hasPassword($user['id'])) { + Login::deleteCookies('cookie', ['user_id' => $user['id']]); + Login::deleteCookies('session', ['user_id' => $user['id']]); + $is_changed = Login::changePassword((int) $user['id'], $POST['new-password']); // This inserts the session login + $changed_message = _s('Password has been changed'); + } else { + $is_changed = Login::addPassword((int) $user['id'], $POST['new-password']); + $changed_message = _s('Password has been created.'); + if (!$is_dashboard_user || $logged_user['id'] == $user['id']) { + $logged_user = Login::login($user['id']); + } + } + if (!$is_dashboard_user) { + Login::insertCookie('cookie', $user['id']); + } + $unsets = ['current-password', 'new-password', 'new-password-confirm']; + foreach ($unsets as $unset) { + $handler::updateVar('safe_post', [$unset => null]); + } + } + } else { + if (in_array($doing, ['', 'account']) && !$is_dashboard_user) { + RequestLog::insert([ + 'type' => 'account-edit', + 'result' => 'fail' + ]); + $error_message = _s('Wrong Username/Email values'); + } + } + } + if ($doing == 'connections') { + $connections = Login::getUserConnections($user['id']); + $has_password = Login::hasPassword($user['id']); + $handler::setCond('has_password', $has_password); + $handler::setVar('connections', $connections); + $handler::setVar('providers_enabled', $providersEnabled); + } + if ($doing === 'api') { + if (!ApiKey::has(intval($user['id']))) { + $apiCreated = ApiKey::insert(intval($user['id'])); + $handler::setVar('api_v1_key', $apiCreated); + } + $apiPub = ApiKey::getUserPublic(intval($user['id'])); + $handler::setVar('api_v1_public_display', $apiPub['public']); + $handler::setVar('api_v1_date_created', $apiPub['date_gmt']); + } + $hasTwoFactor = TwoFactor::hasFor($user['id']); + if ($doing === 'security' && !$hasTwoFactor) { + $twoFactor = new TwoFactor(); + $twoFactorArgs = [ + 'company' => Settings::get('website_name') . ' ' . env()['CHEVERETO_HOSTNAME'], + 'holder' => $user['username'] . '#' . $user['id_encoded'], + ]; + $qrImage = $twoFactor->getQRCodeInline(...$twoFactorArgs); + $handler::setVar('totp_qr_image', $qrImage); + sessionVar()->put('two_factor_secret', $twoFactor->secret()); + } + $pre_doctitle = [$routes[$doing]]; + $pre_doctitle[] = $is_dashboard_user + ? _s('Settings for %s', $user['username']) + : _s('Settings'); + $handler::setCond('two_factor_enabled', $hasTwoFactor); + $handler::setCond('owner', $is_owner); + $handler::setCond('error', $is_error); + $handler::setCond('changed', $is_changed); + $handler::setCond('dashboard_user', $is_dashboard_user); + $handler::setCond('email_required', $is_email_required); + $handler::setCond('captcha_needed', $captcha_needed); + $handler::setVar('content_ip', $user['registration_ip']); + $handler::setVar('pre_doctitle', implode(' - ', $pre_doctitle)); + $handler::setVar('error_message', $error_message); + $handler::setVar('input_errors', $input_errors); + $handler::setVar('changed_message', $changed_message ?? null); + $handler::setVar('changed_email_message', $changed_email_message); + $handler::setVar('user', $is_dashboard_user ? $user : $logged_user); + $handler::setVar('safe_html_user', safe_html($handler::var('user'))); + if ($doing === 'account') { + $bannedIp = IpBan::getSingle(['ip' => $handler::var('user')['registration_ip']]); + $user_list_values = [ + [ + 'label' => _s('Username'), + 'content' => '' . $handler::var('user')['username'] . '' . ( + $handler::cond('dashboard_user') + ? (' ' . _s('Delete user') . '') + : '' + ) + ], + [ + 'label' => _s('User ID'), + 'content' => $handler::var('user')['id'] . ' (' . $handler::var('user')['id_encoded'] . ')' + ], + [ + 'label' => _s('Images'), + 'content' => $handler::var('user')['image_count'] + ], + [ + 'label' => _s('Albums'), + 'content' => $handler::var('user')['album_count'] + ], + [ + 'label' => _s('Register date'), + 'content' => $handler::var('user')['date'] + ], + [ + 'label' => '' . _s('Register date') . '', + 'content' => $handler::var('user')['date_gmt'] . ' (GMT)' + ] + ]; + if ($handler::var('user')['registration_ip']) { + $user_list_values[] = getIpButtonsArray($bannedIp, $handler::var('user')['registration_ip']); + } + $handler::setVar('user_list_values', $user_list_values); + } +}; diff --git a/app/legacy/routes/signup.php b/app/legacy/routes/signup.php new file mode 100644 index 0000000..ce2ce7c --- /dev/null +++ b/app/legacy/routes/signup.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function Chevereto\Legacy\captcha_check; +use Chevereto\Legacy\Classes\Confirmation; +use Chevereto\Legacy\Classes\DB; +use Chevereto\Legacy\Classes\L10n; +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\RequestLog; +use Chevereto\Legacy\Classes\StopForumSpam; +use Chevereto\Legacy\Classes\User; +use function Chevereto\Legacy\G\datetime_diff; +use function Chevereto\Legacy\G\get_client_ip; +use function Chevereto\Legacy\G\get_public_url; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\timing_safe_compare; +use function Chevereto\Legacy\generate_hashed_token; +use function Chevereto\Legacy\get_email_body_str; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\getSettings; +use function Chevereto\Legacy\must_use_captcha; +use function Chevereto\Legacy\send_mail; +use function Chevereto\Vars\post; +use function Chevereto\Vars\request; + +return function (Handler $handler) { + $POST = post(); + $SAFE_POST = $handler::var('safe_post'); + if (!getSetting('enable_signups')) { + $handler->issueError(404); + + return; + } + if ($POST !== [] && !$handler::checkAuthToken(request()['auth_token'] ?? '')) { + $handler->issueError(403); + + return; + } + if ($handler->isRequestLevel(2)) { + $handler->issueError(404); + + return; + } // Allow only 1 level + if (Login::hasSignup()) { + $SAFE_POST['email'] = Login::getSignup()['email']; + redirect('account/awaiting-confirmation'); + } + $logged_user = Login::getUser(); + User::statusRedirect($logged_user['status'] ?? null); + if ($logged_user) { + redirect(User::getUrl($logged_user)); + } + $failed_access_requests = $handler::var('failed_access_requests'); + $is_error = false; + $input_errors = []; + $error_message = null; + $captcha_needed = $handler::cond('captcha_needed'); + if ($captcha_needed && $POST !== []) { + $captcha = captcha_check(); + if (!$captcha->is_valid) { + $is_error = true; + $error_message = _s('%s says you are a robot', 'CAPTCHA'); + } + } + $handler::setCond('show_resend_activation', false); + if ($POST !== [] && !$is_error && !Login::hasSignup()) { + $__post = []; + $__safe_post = []; + foreach (['username', 'email'] as $v) { + if (isset($POST[$v])) { + $POST[$v] = $v == 'email' ? trim($POST[$v]) : strtolower(trim($POST[$v])); + $__post[$v] = $POST[$v]; + $__safe_post[$v] = safe_html($POST[$v]); + } + } + $handler::updateVar('post', $__post); + $handler::updateVar('safe_post', $__safe_post); + if (!filter_var($POST['email'], FILTER_VALIDATE_EMAIL)) { + $input_errors['email'] = _s('Invalid email'); + } + if (!User::isValidUsername($POST['username'])) { + $input_errors['username'] = _s('Invalid username'); + } + if (!preg_match('/' . getSetting('user_password_pattern') . '/', $POST['password'] ?? '')) { + $input_errors['password'] = _s('Invalid password'); + } + if (!filter_var($POST['email'], FILTER_VALIDATE_EMAIL)) { + $input_errors['email'] = _s('Invalid email'); + } + if ($POST['signup-accept-terms-policies'] != 1) { + $input_errors['signup-accept-terms-policies'] = _s('You must agree to the terms and privacy policy'); + } + if (getSetting('user_minimum_age') > 0 && !isset($POST['minimum-age-signup'])) { + $input_errors['minimum-age-signup'] = _s('You must be at least %s years old to use this website.', getSetting('user_minimum_age')); + } + if (count($input_errors) > 0) { + $is_error = true; + } elseif (getSetting('stopforumspam')) { + $sfs = new StopForumSpam(get_client_ip(), $POST['email'], $POST['username']); + if ($sfs->isSpam()) { + $is_error = true; + $error_message = _s('Spam detected'); + } + } + if (!$is_error) { + $user_db = DB::get('users', ['username' => $POST['username'], 'email' => $POST['email']], 'OR', []); + if ($user_db !== []) { + $is_error = true; + $show_resend_activation = false; + foreach ($user_db as $row) { + if (!in_array($row['user_status'], ['valid', 'banned'])) { // Don't touch the valid and banned users + $must_delete_old_user = false; + $confirmation_db = Confirmation::get(['user_id' => $row['user_id']]); + if ($confirmation_db !== false) { + // 24x2 = 48 tic tac tic tac + if (datetime_diff($confirmation_db['confirmation_date_gmt'], null, 'h') > 48) { + Confirmation::delete(['id' => $confirmation_db['confirmation_id']]); + $must_delete_old_user = true; + } + } else { + $must_delete_old_user = true; + } + if ($must_delete_old_user) { + DB::delete('users', ['id' => $row['user_id']]); + + continue; + } + } + if (timing_safe_compare($row['user_username'], $POST['username'])) { + $input_errors['username'] = 'Username already being used'; + } + if (timing_safe_compare($row['user_email'], $POST['email'])) { + $input_errors['email'] = _s('Email already being used'); + } + if (!$show_resend_activation) { + $show_resend_activation = $row['user_status'] == 'awaiting-confirmation'; + } + } + $handler::setCond('show_resend_activation', $show_resend_activation); + } else { + $user_array = [ + 'username' => $POST['username'], + 'email' => $POST['email'], + 'timezone' => getSetting('default_timezone'), + 'language' => L10n::getLocale(), + 'status' => getSetting('require_user_email_confirmation') ? 'awaiting-confirmation' : 'valid' + ]; + + try { + $inserted_user = User::insert($user_array); + } catch (Exception $e) { + if ($e->getCode() === 666) { + $handler->issueError(403); + + return; + } elseif ($e->getCode() === 999) { + $is_error = true; + $error_message = $e->getMessage(); + } else { + throw new Exception($e, $e->getCode(), $e); + } + } + if (!$is_error) { + if ($inserted_user !== 0) { + $insert_password = Login::addPassword($inserted_user, $POST['password']); + } + if (!$inserted_user || !$insert_password) { + throw new Exception("Can't insert user to the DB", 400); + } elseif (getSetting('require_user_email_confirmation')) { + $hashed_token = generate_hashed_token($inserted_user); + Confirmation::insert([ + 'user_id' => $inserted_user, + 'type' => 'account-activate', + 'token_hash' => $hashed_token['hash'], + 'status' => 'active' + ]); + $activation_link = get_public_url( + 'account/activate/?token=' + . $hashed_token['public_token_format'] + ); + global $theme_mail; + $theme_mail = [ + 'user' => $user_array, + 'link' => $activation_link + ]; + $mail['subject'] = _s('Confirmation required at %s', getSettings()['website_name']); + $mail['message'] = get_email_body_str('mails/account-confirm'); + if (send_mail($POST['email'], $mail['subject'], $mail['message'])) { + $is_process_done = true; + } + } else { + $user = User::getSingle($inserted_user, 'id'); + $logged_user = Login::login($user['id']); + Login::insertCookie('cookie', $inserted_user); + + try { + global $theme_mail; + $theme_mail = [ + 'user' => $logged_user, + 'link' => $logged_user['url'] + ]; + + $mail['subject'] = _s('Welcome to %s', getSetting('website_name')); + $mail['message'] = get_email_body_str('mails/account-welcome'); + send_mail($logged_user['email'], $mail['subject'], $mail['message']); + } catch (Exception $e) { + } // Silence + redirect($user['url']); + } + Login::setSignup([ + 'status' => 'awaiting-confirmation', + 'email' => $SAFE_POST['email'] + ]); + redirect('account/awaiting-confirmation'); + } + } + } + if ($is_error) { + RequestLog::insert([ + 'type' => 'signup', + 'result' => 'fail' + ]); + $error_message = $error_message ?? _s('Check the errors in the form to continue.'); + if (getSettings()['captcha'] && must_use_captcha($failed_access_requests['day'] + 1)) { + $captcha_needed = true; + } + } + } + $handler::setCond('error', $is_error); + $handler::setCond('captcha_needed', $captcha_needed); + $handler::setVar('pre_doctitle', _s('Create account')); + $handler::setVar('error', $error_message); + $handler::setVar('input_errors', $input_errors); + $handler::setVar('signup_email', $SAFE_POST['email'] ?? null); +}; diff --git a/app/legacy/routes/update.php b/app/legacy/routes/update.php new file mode 100644 index 0000000..f647569 --- /dev/null +++ b/app/legacy/routes/update.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\Settings; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Vars\env; + +return function (Handler $handler) { + if (!(bool) env()['CHEVERETO_ENABLE_UPDATE_HTTP'] + || Settings::get('chevereto_version_installed') === null + ) { + $handler->issueError(404); + + return; + } + if (!Login::isAdmin()) { + $handler->issueError(403); + + return; + } + require_once PATH_APP_LEGACY_INSTALL . 'installer.php'; +}; diff --git a/app/legacy/routes/upload.php b/app/legacy/routes/upload.php new file mode 100644 index 0000000..9fd7504 --- /dev/null +++ b/app/legacy/routes/upload.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Chevereto\Legacy\Classes\Album; +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\Settings; +use Chevereto\Legacy\Classes\User; +use function Chevereto\Legacy\decodeID; +use function Chevereto\Legacy\G\get_base_url; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Vars\get; + +return function (Handler $handler) { + if (!$handler::cond('upload_allowed')) { + if (Login::isLoggedUser()) { + $handler->issueError(403); + + return; + } else { + redirect('login'); + } + } + $logged_user = Login::getUser(); + User::statusRedirect($logged_user['status'] ?? null); + $album = null; + if (isset(get()['toAlbum'])) { + $toAlbumId = decodeID(get()['toAlbum']); + $album = Album::getSingle(id: $toAlbumId, requester: $logged_user); + $is_owner = isset($album['user']['id']) && $album['user']['id'] == $logged_user['id']; + if (!$is_owner) { + $album = []; + } + } + $handler::setVar('album', $album); + $handler::setVar('pre_doctitle', _s('Upload')); + if (getSetting('homepage_style') == 'route_upload') { + if ($handler->requestArray()[0] === '/') { + $handler::setVar('doctitle', Settings::get('website_doctitle')); + $handler::setVar('pre_doctitle', Settings::get('website_name')); + } + $handler::setVar('canonical', get_base_url('')); + } +}; diff --git a/app/legacy/routes/user.php b/app/legacy/routes/user.php new file mode 100644 index 0000000..92ecbed --- /dev/null +++ b/app/legacy/routes/user.php @@ -0,0 +1,423 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Chevere\String\ModifyString; +use Chevereto\Legacy\Classes\Follow; +use Chevereto\Legacy\Classes\Listing; +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\User; +use function Chevereto\Legacy\G\get_current_url; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\get_share_links; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\redirectIfRouting; +use function Chevereto\Vars\env; +use function Chevereto\Vars\request; +use function Chevereto\Vars\server; + +return function (Handler $handler) { + $currentUrl = get_current_url(); + redirectIfRouting('user', $handler->requestArray()[0]); + $userIndex = (getSetting('root_route') === 'user' + || getSetting('website_mode') == 'personal') + ? 0 + : 1; + if ($handler->isRequestLevel($handler::cond('mapped_route') ? 4 : 5)) { + $handler->issueError(404); + + return; + } + $request_handle = $userIndex === 0 + ? $handler->requestArray() + : $handler->request(); + if (getSetting('website_mode') == 'personal' + && getSetting('website_mode_personal_routing') == '/' + && in_array($request_handle[0], ['albums', 'search', 'following', 'followers']) + ) { + $personal_mode_user = User::getSingle(getSetting('website_mode_personal_uid')); + if ($personal_mode_user !== []) { + $request_handle = [ + 0 => $personal_mode_user['username'], + 1 => $request_handle[0] + ]; + } + } + if ($request_handle[0] === getSetting('route_user') && getSetting('root_route') !== 'user') { + array_shift($request_handle); + } + $username = $request_handle[0] ?? null; + if ($handler::cond('mapped_route') && $handler::mappedArgs() !== []) { + $mapped_args = $handler::mappedArgs(); + } + if (isset($mapped_args['id'])) { + $id = $handler::mappedArgs()['id']; + } + if (!isset($username) && isset($id)) { + $handler->issueError(404); + + return; + } + $logged_user = Login::getUser(); + User::statusRedirect($logged_user['status'] ?? null); + $userHandle = isset($id) ? 'id' : 'username'; + $user = $personal_mode_user ?? User::getSingle($$userHandle, $userHandle); + $is_owner = false; + if (isset($user['id'], $logged_user['id'])) { + $is_owner = $user['id'] == $logged_user['id']; + } + if (!$user + || ($user['status'] ?? '') !== 'valid' + && ($logged_user === [] || !$handler::cond('content_manager'))) { + $handler->issueError(404); + + return; + } + if (!$is_owner && !$handler::cond('content_manager') && (bool) $user['is_private']) { + $handler->issueError(404); + + return; + } + if (!(bool) env()['CHEVERETO_ENABLE_USERS'] && $user['id'] != getSetting('website_mode_personal_uid')) { + $handler->issueError(404); + + return; + } + if (getSetting('website_mode') == 'personal' && getSetting('website_mode_personal_routing') === '/') { + if (str_starts_with($currentUrl, '/' . $user['username'])) { + $redirectTo = (new ModifyString($currentUrl)) + ->withReplaceFirst('/' . $user['username'], '') + ->__toString(); + redirect($redirectTo); + } + } + $pre_doctitle = ''; + $user_routes = []; + $user_views = [ + 'images' => [ + 'title' => _s("Images by %s"), + 'title_short' => _s("Images"), + ], + 'albums' => [ + 'title' => _s("Albums by %s"), + 'title_short' => _s("Albums"), + ], + 'search' => [ + 'title' => _s('Search'), + 'title_short' => _s('Search'), + ], + ]; + foreach (array_keys($user_views) as $k) { // Need to use $k => $v to fetch array key easily + $user_routes[] = $k == 'images' ? $username : $k; + } + if (getSetting('enable_likes')) { + $user_views['liked'] = [ + 'title' => _s("Liked by %s"), + 'title_short' => _s("Liked"), + ]; + $user_routes[] = 'liked'; + } + if (getSetting('enable_followers')) { + $user_views['following'] = [ + 'title' => _s("People followed by %s"), + 'title_short' => _s('Following'), + ]; + $user_views['followers'] = [ + 'title' => _s("People following %s"), + 'title_short' => _s('Followers'), + ]; + $user_routes[] = 'following'; + $user_routes[] = 'followers'; + } + foreach (array_keys($user_views) as $k) { + $user_views[$k]['current'] = false; + } + if (isset($request_handle[1])) { + if ($request_handle[1] == 'search') { + if (!$handler::cond('search_enabled')) { + $handler->issueError(404); + + return; + } + if (!(request()['q'] ?? false)) { + redirect($user['url']); + } + $user['search'] = [ + 'type' => empty(request()['list']) ? 'images' : request()['list'], + 'q' => request()['q'], + 'd' => strlen(request()['q']) >= 25 ? (substr(request()['q'], 0, 22) . '...') : request()['q'] + ]; + } + if ($request_handle[1] !== server()['QUERY_STRING'] && !in_array($request_handle[1], $user_routes)) { + $handler->issueError(404); + + return; + } + if ($request_handle[1] == 'search') { + if (!server()['QUERY_STRING']) { + $handler->issueError(404); + + return; + } + if (!empty(request()['list']) && !in_array(request()['list'], ['images', 'albums', 'users'])) { + $handler->issueError(404); + + return; + } + } + if (array_key_exists($request_handle[1], $user_views)) { + $user_views[$request_handle[1]]['current'] = true; + } + } else { + $user_views['images']['current'] = true; + } + $user['followed'] = false; + $show_follow_button = false; + if (getSetting('website_mode') != 'personal') { + $user['followed'] = false; + $show_follow_button = false; + if ($logged_user !== []) { + $user['followed'] = ($user['id'] == $logged_user['id']) + ? false + : Follow::doesFollow( + (int) $logged_user['id'], + (int) $user['id'] + ); + $show_follow_button = $user['id'] != $logged_user['id'] + && $logged_user['is_private'] == 0; + } + } + $handler::setCond('show_follow_button', $show_follow_button); + $base_user_url = $user['url']; + $type = 'images'; + $current_view = 'images'; + $tools = false; + foreach ($user_views as $k => $v) { + $handler::setCond('user_' . $k, (bool) $v['current']); + if ($v['current']) { + $current_view = $k; + if ($current_view !== 'images') { + $base_user_url .= $k; + } + } + } + $currentKey = 0; + $safe_html_user = safe_html($user); + switch ($current_view) { + case 'images': + case 'liked': + $type = "images"; + $tools = $is_owner || $handler::cond('content_manager'); + if ($current_view == 'liked') { + $tools_available = $handler::cond('content_manager') ? ['delete', 'category', 'flag'] : ['embed']; + } + + break; + case 'following': + case 'followers': + $type = 'users'; + $tools = false; + $params_hidden = [$current_view . '_user_id' => $user['id_encoded']]; + $params_remove_keys = ['list']; + + break; + case 'albums': + $icon = 'fas fa-images'; + $type = "albums"; + $tools = true; + + break; + case 'search': + $icon = 'fas fa-search'; + $type = $user['search']['type']; + $currentKey = (isset(request()['list']) && request()['list'] == 'images') || !isset(request()['list']) + ? 0 : 1; + $tabs = [ + [ + 'icon' => 'fas fa-image', + 'type' => 'images', + 'label' => _n('Image', 'Images', 2), + 'id' => 'list-user-images', + 'current' => $currentKey === 0, + ], + [ + 'icon' => 'fas fa-images', + 'type' => 'albums', + 'label' => _n('Album', 'Albums', 2), + 'id' => 'list-user-albums', + 'current' => $currentKey === 1, + ] + ]; + foreach ($tabs as $k => $v) { + $params = [ + 'list' => $v['type'], + 'q' => $safe_html_user['search']['q'], + 'sort' => 'date_desc', + 'page' => '1', + ]; + $tabs[$k]['params'] = http_build_query($params); + $tabs[$k]['url'] = $base_user_url . '/?' . $tabs[$k]['params']; + } + + break; + } + $icon = 'fas fa-id-card'; + $icon = [ + 'images' => 'fas fa-id-card', + 'albums' => 'fas fa-images', + 'liked' => 'fas fa-heart', + 'following' => 'fas fa-rss', + 'followers' => 'fas fa-users', + 'search' => 'fas fa-search', + ][$current_view]; + if ($user_views['albums']['current']) { + $params_hidden['list'] = 'albums'; + } + $params_hidden[$current_view == 'liked' ? 'like_user_id' : 'userid'] = $user['id_encoded']; + $params_hidden['from'] = 'user'; + if (!isset($tabs)) { + $tabs = Listing::getTabs([ + 'listing' => $type, + 'basename' => $base_user_url, + 'tools' => $tools, + 'tools_available' => $tools_available ?? null, + 'params_hidden' => $params_hidden, + 'params_remove_keys' => $params_remove_keys ?? null, + ], [], true); + $currentKey = $tabs['currentKey']; + $tabs = $tabs['tabs']; + } + foreach ($tabs as $k => &$v) { + if (!array_key_exists('params_hidden', $tabs)) { + $tabs[$k]['params_hidden'] = http_build_query($params_hidden); + } + $v['disabled'] = $user[($user_views['images']['current'] ? 'image' : 'album') . '_count'] == 0 ? !$v['current'] : false; + } + $listing = new Listing(); + if ($user["image_count"] > 0 + || $user["album_count"] > 0 + || in_array($current_view, ['liked', 'following', 'followers'])) { + $getParams = Listing::getParams(request()); + Listing::fillCurrentTabPeekSeek($tabs, $currentKey, $getParams); + $handler::setVar('list_params', $getParams); + if ($getParams['sort'][0] == 'likes' && !getSetting('enable_likes')) { + $handler->issueError(404); + + return; + } + $tpl = $type; + switch ($current_view) { + case 'liked': + $where = 'WHERE like_user_id=:user_id'; + $tpl = 'liked'; + + break; + case 'following': + $where = 'WHERE follow_user_id=:user_id'; + + break; + case 'followers': + $where = 'WHERE follow_followed_user_id=:user_id'; + + break; + default: + $where = $type == 'images' + ? 'WHERE image_user_id=:user_id' + : 'WHERE album_user_id=:user_id AND album_parent_id IS NULL'; + + break; + } + $output_tpl = 'user/' . $tpl; + if ($user_views['search']['current']) { + $type = $user["search"]["type"]; + $where = $user["search"]["type"] == "images" ? "WHERE image_user_id=:user_id AND MATCH(image_name, image_title, image_description, image_original_filename) AGAINST(:q)" : "WHERE album_user_id=:user_id AND MATCH(album_name, album_description) AGAINST(:q)"; + } + $show_user_items_editor = Login::isLoggedUser(); + if ($type == 'albums') { + $show_user_items_editor = false; + } + + try { + $listing = new Listing(); + $listing->setType($type); // images | users | albums + if (isset($getParams['reverse'])) { + $listing->setReverse($getParams['reverse']); + } + if (isset($getParams['seek'])) { + $listing->setSeek($getParams['seek']); + } + $listing->setOffset($getParams['offset']); + $listing->setLimit($getParams['limit']); // how many results? + $listing->setSortType($getParams['sort'][0]); // date | size | views | likes + $listing->setSortOrder($getParams['sort'][1]); // asc | desc + $listing->setWhere($where); + $listing->setOwner((int) $user["id"]); + $listing->setRequester(Login::getUser()); + if ($is_owner || $handler::cond('content_manager')) { + if ($type == 'users') { + $listing->setTools(false); + $show_user_items_editor = false; + } elseif ($current_view == 'liked') { + $listing->setTools( + $user['id'] == $logged_user['id'] + ? ['embed'] + : false + ); + } else { + $listing->setTools(true); + } + } + $listing->bind(":user_id", $user["id"]); + if ($user_views['search']['current'] && !empty($user['search']['q'])) { + $listing->bind(':q', $user['search']['q']); + } + $listing->setOutputTpl($output_tpl); + $listing->exec(); + } catch (Exception $e) { + } // Silence to avoid wrong input queries + } + $title = sprintf($user_views[$current_view]['title'], $user['name_short_html']); + $title_short = sprintf($user_views[$current_view]['title_short'], $user['firstname_html']); + if ($safe_html_user['search']['d'] ?? false) { + $title = _s('Search results for %s', '' . $user['search']['d'] . ''); + $pre_doctitle .= $user['search']['d'] . ' - '; + } + $pre_doctitle .= sprintf($user_views[$current_view]['title'], $user['name_html']); + if (getSetting('website_mode') == 'community' || $user['id'] !== getSetting('website_mode_personal_uid')) { + $pre_doctitle .= ' (' . $user['username'] . ')'; + } + $handler::setVar('pre_doctitle', $pre_doctitle); + $handler::setCond('owner', (bool) $is_owner); + $handler::setCond('show_user_items_editor', $show_user_items_editor ?? false); + $handler::setVar('user', $user); + $handler::setVar('safe_html_user', $safe_html_user); + $handler::setVar('title', $title); + $handler::setVar('title_short', $title_short); + $handler::setVar('tabs', $tabs); + $handler::setVar('listing', $listing); + $handler::setVar('icon', $icon); + if ($user_views['albums']['current']) { + $meta_description = _s('%n (%u) albums on %w'); + } elseif ($user['bio'] ?? false) { + $meta_description = $safe_html_user['bio']; + } else { + $meta_description = _s('%n (%u) on %w'); + } + $handler::setVar('meta_description', strtr($meta_description, ['%n' => $user['name'], '%u' => $user['username'], '%w' => getSetting('website_name')])); + if ($handler::cond('content_manager') || $is_owner) { + $handler::setVar('user_items_editor', [ + "user_albums" => User::getAlbums($user), + "type" => $user_views['albums']['current'] ? "albums" : "images" + ]); + } + $handler::setVar('share_links_array', get_share_links()); +}; diff --git a/app/phpstan-bootstrap.php b/app/phpstan-bootstrap.php new file mode 100644 index 0000000..c714b40 --- /dev/null +++ b/app/phpstan-bootstrap.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +const ACCESS = 'web'; +const APP_NAME = 'chevereto'; +const CHV_PATH_IMAGES = '/path/'; +const HTTP_APP_PROTOCOL = 'http'; +const IMAGE_FORMATS_FAILING = []; +const PATH_APP = '/path/'; +const PATH_APP_CACHE = '/path/'; +const PATH_APP_CONTENT = '/path/'; +const PATH_APP_LANGUAGES = '/path/'; +const PATH_APP_LEGACY = '/path/'; +const PATH_APP_LEGACY_INSTALL = '/path/'; +const PATH_APP_LEGACY_ROUTES = '/path/'; +const PATH_APP_LEGACY_ROUTES_OVERRIDES = '/path/'; +const PATH_PUBLIC = '/path/'; +const PATH_PUBLIC_CONTENT = '/path/'; +const PATH_PUBLIC_CONTENT_IMAGES_SYSTEM = '/path/'; +const PATH_PUBLIC_CONTENT_IMAGES_USERS = '/path/'; +const PATH_PUBLIC_CONTENT_LEGACY_SYSTEM = '/path/'; +const PATH_PUBLIC_CONTENT_LEGACY_THEMES = '/path/'; +const PATH_PUBLIC_CONTENT_LEGACY_THEMES_PEAFOWL_LIB = '/path/'; +const PATH_PUBLIC_CONTENT_PAGES = '/path/'; +const PATH_PUBLIC_LEGACY_THEME = '/path/'; +const REPL = false; +const TIME_EXECUTION_START = 0; +const URL_APP_PUBLIC = 'https://chevereto.com/'; +const URL_APP_PUBLIC_STATIC = 'https://chevereto.com/'; +const URL_APP_THEME = 'https://chevereto.com/'; diff --git a/app/phpstan.neon b/app/phpstan.neon new file mode 100644 index 0000000..e01858e --- /dev/null +++ b/app/phpstan.neon @@ -0,0 +1,12 @@ +parameters: + level: 5 + bootstrapFiles: + - phpstan-bootstrap.php + paths: + - bin + - src/Legacy + - src/Encryption + - ../content/ + excludePaths: + - src/Legacy/Classes/Akismet.php + - src/Legacy/G/Gettext.php diff --git a/app/phpunit-report.xml b/app/phpunit-report.xml new file mode 100644 index 0000000..055eb93 --- /dev/null +++ b/app/phpunit-report.xml @@ -0,0 +1,31 @@ + + + + + + + + tests/Unit/ + + + + + src/ + + + src/Components/Legacy/ + src/Components/Legacy/**/ + src/ + + + + + + + \ No newline at end of file diff --git a/app/phpunit.xml b/app/phpunit.xml new file mode 100644 index 0000000..a56f35d --- /dev/null +++ b/app/phpunit.xml @@ -0,0 +1,33 @@ + + + + + + + + tests/Integration + + + + + src/ + + + src/Components/Legacy/ + src/Components/Legacy/**/ + src/ + + + + + + + \ No newline at end of file diff --git a/app/rector.php b/app/rector.php new file mode 100644 index 0000000..f17612f --- /dev/null +++ b/app/rector.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +use Rector\Core\Configuration\Option; +use Rector\Core\ValueObject\PhpVersion; +use Rector\Php74\Rector\Property\TypedPropertyRector; +use Rector\Set\ValueObject\SetList; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +return static function (ContainerConfigurator $containerConfigurator): void { + // get parameters + $parameters = $containerConfigurator->parameters(); + $parameters->set(Option::PATHS, [ + __DIR__ . '/app' + ]); + + // Define what rule sets will be applied + $containerConfigurator->import(SetList::DEAD_CODE); + $containerConfigurator->import(SetList::PHP_80); + $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_81); + // get services (needed for register a single rule) + // $services = $containerConfigurator->services(); + + // register a single rule + // $services->set(TypedPropertyRector::class); +}; diff --git a/app/routing/admin.api-v4.php b/app/routing/admin.api-v4.php new file mode 100644 index 0000000..092d468 --- /dev/null +++ b/app/routing/admin.api-v4.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function Chevere\Router\route; +use function Chevere\Router\routes; +use Chevereto\Controllers\Api\V4\Ban\Ip\BanIpDeleteController; +use Chevereto\Controllers\Api\V4\Ban\Ip\BanIpPatchController; +use Chevereto\Controllers\Api\V4\Ban\Ip\BanIpPostController; +use Chevereto\Controllers\Api\V4\Category\CategoryPostController; +use Chevereto\Controllers\Api\V4\Image\Bulk\ImageBulkPatchController; +use Chevereto\Controllers\Api\V4\Stat\Rebuild\StatRebuildPostController; +use Chevereto\Controllers\Api\V4\Storage\Migrate\StorageMigratePostController; +use Chevereto\Controllers\Api\V4\Storage\Stat\Regen\StorageStatRegenPostController; +use Chevereto\Controllers\Api\V4\Storage\StoragePostController; +use Chevereto\Controllers\Api\V4\Tool\Id\Decode\ToolDecodeIdGetController; +use Chevereto\Controllers\Api\V4\Tool\Id\Encode\ToolEncodeIdGetController; +use Chevereto\Controllers\Api\V4\Tool\Probe\Email\ToolProbeEmailPostController; +use Chevereto\Controllers\Api\V4\User\Export\UserExportGetController; +use Chevereto\Controllers\Api\V4\User\UserGetController; +use Chevereto\Controllers\Api\V4\User\UserPostController; + +$prefix = '/api/4/admin/'; + +return routes( + route( + path: $prefix . 'bans/ip/', + POST: new BanIpPostController(), + ), + route( + path: $prefix . 'bans/ip/{ip}/', + DELETE: new BanIpDeleteController(), + PATCH: new BanIpPatchController(), + ), + route( + path: $prefix . 'categories/', + POST: new CategoryPostController(), + ), + route( + path: $prefix . 'categories/{id}/', + // DELETE: , + // PATCH: , + ), + route( + path: $prefix . 'images/bulk/approve/', + PATCH: new ImageBulkPatchController(), + ), + route( + path: $prefix . 'imports/', + // POST: , + ), + route( + path: $prefix . 'imports/{id}/', + // DELETE: , + // GET: , + // PATCH: , + ), + route( + path: $prefix . 'imports/{id}/process/', + // POST: , + ), + route( + path: $prefix . 'imports/{id}/reset/', + // POST: , + ), + route( + path: $prefix . 'imports/{id}/resume/', + // POST: , + ), + route( + path: $prefix . 'stats/rebuild/', + POST: new StatRebuildPostController(), + ), + route( + path: $prefix . 'storages/', + POST: new StoragePostController(), + ), + route( + path: $prefix . 'storages/{id}/', + // PATCH: , + ), + route( + path: $prefix . 'storages/{id}/migrate/', + POST: new StorageMigratePostController(), + ), + route( + path: $prefix . 'storages/{id}/stats/regen/', + POST: new StorageStatRegenPostController(), + ), + route( + path: $prefix . 'tools/id/{id}/decode/', + GET: new ToolDecodeIdGetController(), + ), + route( + path: $prefix . 'tools/id/{id}/encode/', + GET: new ToolEncodeIdGetController(), + ), + route( + path: $prefix . 'tools/probe/email/', + POST: new ToolProbeEmailPostController(), + ), + route( + path: $prefix . 'users/', + POST: new UserPostController(), + ), + route( + path: $prefix . 'users/{id}/', + GET: new UserGetController(), + ), + route( + path: $prefix . 'users/{id}/export/', + GET: new UserExportGetController(), + ), +); diff --git a/app/routing/admin.web.php b/app/routing/admin.web.php new file mode 100644 index 0000000..a8335de --- /dev/null +++ b/app/routing/admin.web.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function App\Controllers\legacyController; +use function Chevere\Router\route; +use function Chevere\Router\routes; + +return routes( + route( + name: 'dashboard', + path: '/dashboard/', + GET: legacyController('route.dashboard.php'), + POST: legacyController('route.dashboard.php'), + ), + route( + name: 'importer-jobs', + path: '/importer-jobs/', + GET: legacyController('route.importer-jobs.php'), + ), + route( + name: 'install', + path: '/install/', + GET: legacyController('route.install.php'), + POST: legacyController('route.install.php'), + ), + route( + name: 'update', + path: '/update/', + POST: legacyController('route.update.php'), + ), +); diff --git a/app/routing/user.api-v1.php b/app/routing/user.api-v1.php new file mode 100644 index 0000000..42fcae6 --- /dev/null +++ b/app/routing/user.api-v1.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function Chevere\Router\route; +use function Chevere\Router\routes; +use Chevereto\Controllers\Api\V1\Upload\UploadPostController; + +return routes( + route( + path: '/api/1/upload/', + POST: new UploadPostController() + ), +); diff --git a/app/routing/user.api-v4.php b/app/routing/user.api-v4.php new file mode 100644 index 0000000..9d481ae --- /dev/null +++ b/app/routing/user.api-v4.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +use function Chevere\Router\route; +use function Chevere\Router\routes; +use Chevereto\Controllers\Api\V4\Album\AlbumDeleteController; +use Chevereto\Controllers\Api\V4\Album\AlbumGetController; +use Chevereto\Controllers\Api\V4\Album\AlbumPatchController; +use Chevereto\Controllers\Api\V4\Album\AlbumPostController; +use Chevereto\Controllers\Api\V4\Album\Like\AlbumLikeDeleteController; +use Chevereto\Controllers\Api\V4\Album\Like\AlbumLikePostController; +use Chevereto\Controllers\Api\V4\Image\Bulk\ImageBulkPatchController; +use Chevereto\Controllers\Api\V4\Image\ImageGetController; +use Chevereto\Controllers\Api\V4\Image\ImagePatchController; +use Chevereto\Controllers\Api\V4\Image\ImagePostController; +use Chevereto\Controllers\Api\V4\Image\Like\ImageLikeDeleteController; +use Chevereto\Controllers\Api\V4\Image\Like\ImageLikePostController; +use Chevereto\Controllers\Api\V4\User\Asset\Avatar\UserAssetAvatarDeleteController; +use Chevereto\Controllers\Api\V4\User\Asset\Avatar\UserAssetAvatarPostController; +use Chevereto\Controllers\Api\V4\User\Asset\Background\UserAssetBackgroundDeleteController; +use Chevereto\Controllers\Api\V4\User\Asset\Background\UserAssetBackgroundPostController; +use Chevereto\Controllers\Api\V4\User\Follow\UserFollowDeleteController; +use Chevereto\Controllers\Api\V4\User\Follow\UserFollowPostController; +use Chevereto\Controllers\Api\V4\User\Setting\UserSettingPatchController; + +$prefix = '/api/4/user/'; + +return routes( + route( + path: $prefix . 'account/notifications/social/', + // GET: , + ), + route( + path: $prefix . 'account/notifications/social/{id}/', + // PATCH: , + ), + route( + path: $prefix . 'account/settings/', + PATCH: new UserSettingPatchController(), + ), + route( + path: $prefix . 'account/login/{service}/', + // DELETE: , + ), + route( + path: $prefix . 'albums/', + POST: new AlbumPostController(), + ), + route( + path: $prefix . 'albums/{id}/', + DELETE: new AlbumDeleteController(), + GET: new AlbumGetController(), + PATCH: new AlbumPatchController(), + ), + route( + path: $prefix . 'albums/{id}/contents/', + // GET: , + ), + route( + path: $prefix . 'albums/{id}/like/', + DELETE: new AlbumLikeDeleteController(), + POST: new AlbumLikePostController(), + ), + route( + path: $prefix . 'albums/bulk/', + // DELETE: , + ), + route( + path: $prefix . 'albums/bulk/parent/', + // PATCH: , + ), + route( + path: $prefix . 'albums/list/', + // GET: + ), + route( + path: $prefix . 'images/', + POST: new ImagePostController(), + ), + route( + path: $prefix . 'images/{id}/', + // DELETE: , + GET: new ImageGetController(), + PATCH: new ImagePatchController(), + ), + route( + path: $prefix . 'images/{id}/like/', + DELETE: new ImageLikeDeleteController(), + POST: new ImageLikePostController(), + ), + route( + path: $prefix . 'images/bulk/', + PATCH: new ImageBulkPatchController(), + ), + route( + path: $prefix . 'images/list/', + // GET: + ), + route( + path: $prefix . 'user/{username}/assets/avatar/', + DELETE: new UserAssetAvatarDeleteController(), + POST: new UserAssetAvatarPostController() + ), + route( + path: $prefix . 'user/{username}/assets/background/', + DELETE: new UserAssetBackgroundDeleteController(), + POST: new UserAssetBackgroundPostController() + ), + route( + path: $prefix . 'users/{username}/follow/', + DELETE: new UserFollowDeleteController(), + POST: new UserFollowPostController(), + ), + route( + path: $prefix . 'users/list/', + // GET:, + ), +); diff --git a/app/routing/user.web.php b/app/routing/user.web.php new file mode 100644 index 0000000..46dce2e --- /dev/null +++ b/app/routing/user.web.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use function App\Controllers\legacyController; +use function Chevere\Router\route; +use function Chevere\Router\routes; + +return routes( + route( + name: 'index', + path: '/', + ), + route( + name: 'account', + path: '/account/', + GET: legacyController('account.php'), + POST: legacyController('account.php'), + ), + route( + name: 'album', + path: '/album/', + GET: legacyController('album.php'), + POST: legacyController('album.php'), + ), + route( + name: 'category', + path: '/category/', + GET: legacyController('category.php'), + ), + route( + name: 'connect', + path: '/connect/', + GET: legacyController('connect.php'), + ), + route( + name: 'explore', + path: '/explore/', + GET: legacyController('explore.php'), + ), + route( + name: 'following', + path: '/following/', + GET: legacyController('following.php'), + ), + route( + name: 'image', + path: '/image/', + GET: legacyController('image.php'), + ), + route( + name: 'login', + path: '/login/', + GET: legacyController('login.php'), + POST: legacyController('login.php'), + ), + route( + name: 'logout', + path: '/logout/', + GET: legacyController('logout.php'), + ), + route( + name: 'moderate', + path: '/moderate/', + GET: legacyController('moderate.php'), + ), + route( + name: 'oembed', + path: '/oembed/', + GET: legacyController('oembed.php'), + ), + route( + name: 'page', + path: '/page/', + GET: legacyController('page.php'), + ), + route( + name: 'plugin', + path: '/plugin/', + GET: legacyController('plugin.php'), + ), + route( + name: 'captcha-verify', + path: '/captcha-verify/', + GET: legacyController('captcha-verify.php'), + ), + route( + name: 'redirect', + path: '/redirect/', + GET: legacyController('redirect.php'), + ), + route( + name: 'search', + path: '/search/', + GET: legacyController('search.php'), + POST: legacyController('search.php'), + ), + route( + name: 'settings', + path: '/settings/', + GET: legacyController('settings.php'), + POST: legacyController('settings.php'), + ), + route( + name: 'signup', + path: '/signup/', + GET: legacyController('signup.php'), + POST: legacyController('signup.php'), + ), + route( + name: 'upload', + path: '/upload/', + GET: legacyController('upload.php'), + ), + route( + name: 'user', + path: '/user/', + GET: legacyController('user.php'), + ), +); diff --git a/app/schemas/mysql-5/albums.sql b/app/schemas/mysql-5/albums.sql new file mode 100644 index 0000000..833a86c --- /dev/null +++ b/app/schemas/mysql-5/albums.sql @@ -0,0 +1,26 @@ +DROP TABLE IF EXISTS `%table_prefix%albums`; +CREATE TABLE `%table_prefix%albums` ( + `album_id` bigint(32) NOT NULL AUTO_INCREMENT, + `album_name` varchar(100) NOT NULL, + `album_user_id` bigint(32) DEFAULT NULL, + `album_date` datetime NOT NULL, + `album_date_gmt` datetime NOT NULL, + `album_creation_ip` varchar(255) NOT NULL, + `album_privacy` enum('public','password','private','private_but_link','custom') DEFAULT 'public', + `album_privacy_extra` mediumtext, + `album_password` mediumtext, + `album_image_count` bigint(32) NOT NULL DEFAULT '0', + `album_description` mediumtext, + `album_likes` bigint(32) NOT NULL DEFAULT '0', + `album_views` bigint(32) NOT NULL DEFAULT '0', + `album_cover_id` bigint(32) DEFAULT NULL, + `album_parent_id` bigint(32) DEFAULT NULL, + PRIMARY KEY (`album_id`), + KEY `album_name` (`album_name`), + KEY `album_user_id` (`album_user_id`), + KEY `album_date_gmt` (`album_date_gmt`), + KEY `album_privacy` (`album_privacy`), + KEY `album_image_count` (`album_image_count`), + KEY `album_creation_ip` (`album_creation_ip`), + FULLTEXT KEY `searchindex` (`album_name`,`album_description`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/api_keys.sql b/app/schemas/mysql-5/api_keys.sql new file mode 100644 index 0000000..b0a83c1 --- /dev/null +++ b/app/schemas/mysql-5/api_keys.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `%table_prefix%api_keys`; +CREATE TABLE `%table_prefix%api_keys` ( + `api_key_id` bigint(32) NOT NULL AUTO_INCREMENT, + `api_key_user_id` bigint(32) DEFAULT NULL, + `api_key_name` varchar(100) DEFAULT NULL, + `api_key_date_gmt` datetime NOT NULL, + `api_key_hash` mediumtext NOT NULL, + PRIMARY KEY (`api_key_id`), + KEY `api_key_user_id` (`api_key_user_id`), + KEY `api_key_name` (`api_key_name`), + KEY `api_key_date_gmt` (`api_key_date_gmt`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/assets.sql b/app/schemas/mysql-5/assets.sql new file mode 100644 index 0000000..2eade0c --- /dev/null +++ b/app/schemas/mysql-5/assets.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS `%table_prefix%assets`; +CREATE TABLE `%table_prefix%assets` ( + `asset_id` bigint(32) NOT NULL AUTO_INCREMENT, + `asset_key` varchar(255) NOT NULL, + `asset_md5` varchar(32) NOT NULL, + `asset_filename` varchar(255) NOT NULL, + `asset_file_path` varchar(255) NOT NULL, + `asset_blob` blob, + PRIMARY KEY (`asset_id`), + UNIQUE KEY `key` (`asset_key`(191)) USING BTREE, + KEY `md5` (`asset_md5`), + KEY `filename` (`asset_filename`), + KEY `file_path` (`asset_file_path`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/categories.sql b/app/schemas/mysql-5/categories.sql new file mode 100644 index 0000000..35f5bf5 --- /dev/null +++ b/app/schemas/mysql-5/categories.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS `%table_prefix%categories`; +CREATE TABLE `%table_prefix%categories` ( + `category_id` bigint(32) NOT NULL AUTO_INCREMENT, + `category_name` varchar(32) NOT NULL, + `category_url_key` varchar(32) NOT NULL, + `category_description` mediumtext, + PRIMARY KEY (`category_id`), + UNIQUE KEY `url_key` (`category_url_key`) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/confirmations.sql b/app/schemas/mysql-5/confirmations.sql new file mode 100644 index 0000000..e65a5f1 --- /dev/null +++ b/app/schemas/mysql-5/confirmations.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `%table_prefix%confirmations`; +CREATE TABLE `%table_prefix%confirmations` ( + `confirmation_id` bigint(32) NOT NULL AUTO_INCREMENT, + `confirmation_user_id` bigint(32) NOT NULL, + `confirmation_type` enum('account-activate','account-change-email','account-password-forgot') NOT NULL, + `confirmation_date` datetime NOT NULL, + `confirmation_date_gmt` datetime NOT NULL, + `confirmation_token_hash` varchar(255) NOT NULL, + `confirmation_status` enum('active','valid','invalid') NOT NULL, + `confirmation_extra` mediumtext, + PRIMARY KEY (`confirmation_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/deletions.sql b/app/schemas/mysql-5/deletions.sql new file mode 100644 index 0000000..1aca667 --- /dev/null +++ b/app/schemas/mysql-5/deletions.sql @@ -0,0 +1,20 @@ +DROP TABLE IF EXISTS `%table_prefix%deletions`; +CREATE TABLE `%table_prefix%deletions` ( + `deleted_id` bigint(32) NOT NULL AUTO_INCREMENT, + `deleted_date_gmt` datetime NOT NULL, + `deleted_content_id` bigint(32) NOT NULL, + `deleted_content_date_gmt` datetime NOT NULL, + `deleted_content_user_id` bigint(32) DEFAULT NULL, + `deleted_content_ip` varchar(255) NOT NULL, + `deleted_content_md5` varchar(32) DEFAULT NULL, + `deleted_content_original_filename` varchar(255) DEFAULT NULL, + `deleted_content_views` bigint(32) NOT NULL DEFAULT '0', + `deleted_content_likes` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`deleted_id`), + KEY `deleted_content_id` (`deleted_content_id`), + KEY `deleted_content_user_id` (`deleted_content_user_id`), + KEY `deleted_content_ip` (`deleted_content_ip`), + KEY `deleted_content_md5` (`deleted_content_md5`), + KEY `deleted_content_views` (`deleted_content_views`), + KEY `deleted_content_likes` (`deleted_content_likes`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/follows.sql b/app/schemas/mysql-5/follows.sql new file mode 100644 index 0000000..5e51ed9 --- /dev/null +++ b/app/schemas/mysql-5/follows.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `%table_prefix%follows`; +CREATE TABLE `%table_prefix%follows` ( + `follow_id` bigint(32) NOT NULL AUTO_INCREMENT, + `follow_date` datetime NOT NULL, + `follow_date_gmt` datetime NOT NULL, + `follow_user_id` bigint(32) NOT NULL, + `follow_followed_user_id` bigint(32) NOT NULL, + `follow_ip` varchar(255) NOT NULL, + PRIMARY KEY (`follow_id`), + KEY `follow_user_id` (`follow_user_id`), + KEY `follow_followed_user_id` (`follow_followed_user_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/images.sql b/app/schemas/mysql-5/images.sql new file mode 100644 index 0000000..14b785d --- /dev/null +++ b/app/schemas/mysql-5/images.sql @@ -0,0 +1,60 @@ +DROP TABLE IF EXISTS `%table_prefix%images`; +CREATE TABLE `%table_prefix%images` ( + `image_id` bigint(32) NOT NULL AUTO_INCREMENT, + `image_name` varchar(255) NOT NULL, + `image_extension` varchar(255) NOT NULL, + `image_size` int(11) NOT NULL, + `image_width` int(11) NOT NULL, + `image_height` int(11) NOT NULL, + `image_date` datetime NOT NULL, + `image_date_gmt` datetime NOT NULL, + `image_title` varchar(100) DEFAULT NULL, + `image_description` mediumtext, + `image_nsfw` tinyint(1) NOT NULL DEFAULT '0', + `image_user_id` bigint(32) DEFAULT NULL, + `image_album_id` bigint(32) DEFAULT NULL, + `image_uploader_ip` varchar(255) NOT NULL, + `image_storage_mode` enum('datefolder','direct','old','path') NOT NULL DEFAULT 'datefolder', + `image_path` varchar(4096) DEFAULT NULL, + `image_storage_id` bigint(32) DEFAULT NULL, + `image_md5` varchar(32) NOT NULL, + `image_source_md5` varchar(32) DEFAULT NULL, + `image_original_filename` varchar(255) NOT NULL, + `image_original_exifdata` longtext, + `image_views` bigint(32) NOT NULL DEFAULT '0', + `image_category_id` bigint(32) DEFAULT NULL, + `image_chain` tinyint(128) NOT NULL, + `image_thumb_size` int(11) NOT NULL, + `image_medium_size` int(11) NOT NULL DEFAULT '0', + `image_expiration_date_gmt` datetime DEFAULT NULL, + `image_likes` bigint(32) NOT NULL DEFAULT '0', + `image_is_animated` tinyint(1) NOT NULL DEFAULT '0', + `image_is_approved` tinyint(1) NOT NULL DEFAULT '1', + `image_is_360` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`image_id`), + KEY `image_name` (`image_name`), + KEY `image_extension` (`image_extension`), + KEY `image_size` (`image_size`), + KEY `image_width` (`image_width`), + KEY `image_height` (`image_height`), + KEY `image_date_gmt` (`image_date_gmt`), + KEY `image_nsfw` (`image_nsfw`), + KEY `image_user_id` (`image_user_id`), + KEY `image_album_id` (`image_album_id`), + KEY `image_uploader_ip` (`image_uploader_ip`), + KEY `image_storage_mode` (`image_storage_mode`), + KEY `image_path` (`image_path`(255)), + KEY `image_storage_id` (`image_storage_id`), + KEY `image_md5` (`image_md5`), + KEY `image_source_md5` (`image_source_md5`), + KEY `image_views` (`image_views`), + KEY `image_category_id` (`image_category_id`), + KEY `image_chain` (`image_chain`), + KEY `image_expiration_date_gmt` (`image_expiration_date_gmt`), + KEY `image_likes` (`image_likes`), + KEY `image_is_animated` (`image_is_animated`), + KEY `image_is_approved` (`image_is_approved`), + KEY `image_is_360` (`image_is_360`), + KEY `image_album_id_image_id` (`image_album_id`, `image_id`), + FULLTEXT KEY `searchindex` (`image_name`,`image_title`,`image_description`,`image_original_filename`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/images_hash.sql b/app/schemas/mysql-5/images_hash.sql new file mode 100644 index 0000000..d5e819e --- /dev/null +++ b/app/schemas/mysql-5/images_hash.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS `%table_prefix%images_hash`; +CREATE TABLE `%table_prefix%images_hash` ( + `image_hash_image_id` bigint(32) NOT NULL, + `image_hash_hash` mediumtext NOT NULL, + PRIMARY KEY (`image_hash_image_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/importing.sql b/app/schemas/mysql-5/importing.sql new file mode 100644 index 0000000..020db9f --- /dev/null +++ b/app/schemas/mysql-5/importing.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `%table_prefix%importing`; +CREATE TABLE `%table_prefix%importing` ( + `importing_id` bigint(32) NOT NULL AUTO_INCREMENT, + `importing_import_id` bigint(32) NOT NULL, + `importing_path` varchar(4096) NOT NULL, + `importing_content_type` enum('user','album','image') NOT NULL, + `importing_content_id` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`importing_id`), + UNIQUE KEY `importing_path` (`importing_path`(191)) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/imports.sql b/app/schemas/mysql-5/imports.sql new file mode 100644 index 0000000..af44023 --- /dev/null +++ b/app/schemas/mysql-5/imports.sql @@ -0,0 +1,21 @@ +DROP TABLE IF EXISTS `%table_prefix%imports`; +CREATE TABLE `%table_prefix%imports` ( + `import_id` bigint(32) NOT NULL AUTO_INCREMENT, + `import_path` varchar(4096) NOT NULL, + `import_options` varchar(255) DEFAULT NULL, + `import_status` enum('queued','working','paused','canceled','completed') NOT NULL, + `import_users` bigint(32) NOT NULL DEFAULT '0', + `import_images` bigint(32) NOT NULL DEFAULT '0', + `import_albums` bigint(32) NOT NULL DEFAULT '0', + `import_time_created` datetime DEFAULT NULL, + `import_time_updated` datetime DEFAULT NULL, + `import_errors` tinyint(1) NOT NULL DEFAULT '0', + `import_started` tinyint(1) NOT NULL DEFAULT '0', + `import_continuous` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`import_id`), + KEY `import_path` (`import_path`(191)) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; + +INSERT INTO `%table_prefix%imports` VALUES ('1', '%rootPath%importing/no-parse', 'a:1:{s:4:"root";s:5:"plain";}', 'working', '0', '0', '0', NOW(), NOW(), '0', '1', '1'); +INSERT INTO `%table_prefix%imports` VALUES ('2', '%rootPath%importing/parse-users', 'a:1:{s:4:"root";s:5:"users";}', 'working', '0', '0', '0', NOW(), NOW(), '0', '1', '1'); +INSERT INTO `%table_prefix%imports` VALUES ('3', '%rootPath%importing/parse-albums', 'a:1:{s:4:"root";s:6:"albums";}', 'working', '0', '0', '0', NOW(), NOW(), '0', '1', '1'); diff --git a/app/schemas/mysql-5/ip_bans.sql b/app/schemas/mysql-5/ip_bans.sql new file mode 100644 index 0000000..72f2c1c --- /dev/null +++ b/app/schemas/mysql-5/ip_bans.sql @@ -0,0 +1,13 @@ +DROP TABLE IF EXISTS `%table_prefix%ip_bans`; +CREATE TABLE `%table_prefix%ip_bans` ( + `ip_ban_id` bigint(20) NOT NULL AUTO_INCREMENT, + `ip_ban_date` datetime NOT NULL, + `ip_ban_date_gmt` datetime NOT NULL, + `ip_ban_expires` datetime DEFAULT NULL, + `ip_ban_expires_gmt` datetime DEFAULT NULL, + `ip_ban_ip` varchar(255) NOT NULL, + `ip_ban_message` text, + PRIMARY KEY (`ip_ban_id`), + KEY `ip_ban_date_gmt` (`ip_ban_date_gmt`), + UNIQUE KEY `ip_ban_ip` (`ip_ban_ip`(191)) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/likes.sql b/app/schemas/mysql-5/likes.sql new file mode 100644 index 0000000..4e92ad0 --- /dev/null +++ b/app/schemas/mysql-5/likes.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%likes`; +CREATE TABLE `%table_prefix%likes` ( + `like_id` bigint(32) NOT NULL AUTO_INCREMENT, + `like_date` datetime NOT NULL, + `like_date_gmt` datetime NOT NULL, + `like_user_id` bigint(32) DEFAULT NULL, + `like_content_type` enum('image','album') DEFAULT NULL, + `like_content_id` bigint(32) NOT NULL, + `like_content_user_id` bigint(32) DEFAULT NULL, + `like_ip` varchar(255) NOT NULL, + PRIMARY KEY (`like_id`), + KEY `like_date_gmt` (`like_date_gmt`), + KEY `like_user_id` (`like_user_id`), + KEY `like_content_type` (`like_content_type`), + KEY `like_content_id` (`like_content_id`), + KEY `like_content_user_id` (`like_content_user_id`), + KEY `like_ip` (`like_ip`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/locks.sql b/app/schemas/mysql-5/locks.sql new file mode 100644 index 0000000..db3b44a --- /dev/null +++ b/app/schemas/mysql-5/locks.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `%table_prefix%locks`; +CREATE TABLE `%table_prefix%locks` ( + `lock_id` bigint(20) NOT NULL AUTO_INCREMENT, + `lock_name` varchar(255) NOT NULL, + `lock_date_gmt` datetime NOT NULL, + `lock_expires_gmt` datetime DEFAULT NULL, + PRIMARY KEY (`lock_id`), + KEY `lock_date_gmt` (`lock_date_gmt`), + KEY `lock_expires_gmt` (`lock_expires_gmt`), + UNIQUE KEY `lock_name` (`lock_name`(191)) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/login_connections.sql b/app/schemas/mysql-5/login_connections.sql new file mode 100644 index 0000000..2626ce6 --- /dev/null +++ b/app/schemas/mysql-5/login_connections.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS `%table_prefix%login_connections`; +CREATE TABLE `%table_prefix%login_connections` ( + `login_connection_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_connection_user_id` bigint(32) NOT NULL, + `login_connection_provider_id` bigint(32) NOT NULL, + `login_connection_date_gmt` datetime NOT NULL, + `login_connection_resource_id` varchar(255) NOT NULL, + `login_connection_resource_name` mediumtext, + `login_connection_token` mediumtext NOT NULL COMMENT 'Ciphertext', + PRIMARY KEY (`login_connection_id`), + UNIQUE KEY `login_connection_unique` (`login_connection_user_id`,`login_connection_provider_id`), + KEY `login_connection_user_id` (`login_connection_user_id`), + KEY `login_connection_date_gmt` (`login_connection_date_gmt`), + KEY `login_connection_provider_id` (`login_connection_provider_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/login_cookies.sql b/app/schemas/mysql-5/login_cookies.sql new file mode 100644 index 0000000..7ef2a80 --- /dev/null +++ b/app/schemas/mysql-5/login_cookies.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS `%table_prefix%login_cookies`; +CREATE TABLE `%table_prefix%login_cookies` ( + `login_cookie_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_cookie_user_id` bigint(32) NOT NULL, + `login_cookie_connection_id` bigint(32) DEFAULT 0, + `login_cookie_date_gmt` datetime NOT NULL, + `login_cookie_ip` varchar(255) DEFAULT NULL, + `login_cookie_user_agent` mediumtext NOT NULL, + `login_cookie_hash` mediumtext NOT NULL, + PRIMARY KEY (`login_cookie_id`), + UNIQUE KEY `login_cookie_unique` (`login_cookie_user_id`,`login_cookie_connection_id`,`login_cookie_date_gmt`), + KEY `login_cookie_user_id_date_gmt` (`login_cookie_user_id`, `login_cookie_date_gmt`), + KEY `login_cookie_user_id` (`login_cookie_user_id`), + KEY `login_cookie_ip` (`login_cookie_ip`), + KEY `login_cookie_connection_id` (`login_cookie_connection_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/login_passwords.sql b/app/schemas/mysql-5/login_passwords.sql new file mode 100644 index 0000000..58d6b52 --- /dev/null +++ b/app/schemas/mysql-5/login_passwords.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS `%table_prefix%login_passwords`; +CREATE TABLE `%table_prefix%login_passwords` ( + `login_password_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_password_user_id` bigint(32) NOT NULL, + `login_password_date_gmt` datetime NOT NULL, + `login_password_hash` mediumtext NOT NULL, + PRIMARY KEY (`login_password_id`), + UNIQUE KEY `login_password_user_id` (`login_password_user_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/login_providers.sql b/app/schemas/mysql-5/login_providers.sql new file mode 100644 index 0000000..97c0574 --- /dev/null +++ b/app/schemas/mysql-5/login_providers.sql @@ -0,0 +1,43 @@ +DROP TABLE IF EXISTS `%table_prefix%login_providers`; +CREATE TABLE `%table_prefix%login_providers` ( + `login_provider_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_provider_name` varchar(255) DEFAULT NULL, + `login_provider_label` varchar(255) DEFAULT NULL, + `login_provider_key_id` mediumtext DEFAULT NULL, + `login_provider_key_secret` mediumtext DEFAULT NULL, + `login_provider_is_enabled` tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (`login_provider_id`), + UNIQUE KEY `login_provider_name` (`login_provider_name`(191)), + KEY `login_provider_is_enabled` (`login_provider_is_enabled`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; +INSERT INTO `%table_prefix%login_providers` VALUES ('1', 'facebook', 'Facebook', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('2', 'twitter', 'Twitter', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('3', 'google', 'Google', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('4', 'vkontakte', 'VK', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('5', 'apple', 'Apple', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('6', 'amazon', 'Amazon', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('7', 'bitbucket', 'BitBucket', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('8', 'discord', 'Discord', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('9', 'dribbble', 'Dribbble', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('10', 'dropbox', 'Dropbox', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('11', 'github', 'GitHub', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('12', 'gitlab', 'GitLab', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('13', 'instagram', 'Instagram', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('14', 'linkedin', 'LinkedIn', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('15', 'mailru', 'Mailru', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('16', 'medium', 'Medium', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('17', 'odnoklassniki', 'Odnoklassniki', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('18', 'orcid', 'ORCID', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('19', 'reddit', 'Reddit', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('20', 'spotify', 'Spotify', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('21', 'steam', 'Steam', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('22', 'strava', 'Strava', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('23', 'telegram', 'Telegram', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('24', 'tumblr', 'Tumblr', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('25', 'twitchtv', 'Twitch', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('26', 'wechat', 'WeChat', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('27', 'wordpress', 'WordPress', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('28', 'yandex', 'Yandex', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('29', 'yahoo', 'Yahoo', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('30', 'qq', 'QQ', null, null, '0'); + diff --git a/app/schemas/mysql-5/logins.sql b/app/schemas/mysql-5/logins.sql new file mode 100644 index 0000000..ba60dd3 --- /dev/null +++ b/app/schemas/mysql-5/logins.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%logins`; +CREATE TABLE `%table_prefix%logins` ( + `login_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_user_id` bigint(32) NOT NULL, + `login_type` enum('password','session','cookie','facebook','twitter','google','vk','cookie_facebook','cookie_twitter','cookie_google','cookie_vk') NOT NULL, + `login_ip` varchar(255) DEFAULT NULL, + `login_hostname` mediumtext, + `login_date` datetime NOT NULL, + `login_date_gmt` datetime NOT NULL, + `login_resource_id` varchar(255) DEFAULT NULL, + `login_resource_name` mediumtext, + `login_resource_avatar` mediumtext, + `login_resource_url` mediumtext, + `login_secret` mediumtext DEFAULT NULL COMMENT 'The secret part', + `login_token_hash` mediumtext COMMENT 'Hashed complement to secret if needed', + PRIMARY KEY (`login_id`), + KEY `login_user_id` (`login_user_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/notifications.sql b/app/schemas/mysql-5/notifications.sql new file mode 100644 index 0000000..51044c3 --- /dev/null +++ b/app/schemas/mysql-5/notifications.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS `%table_prefix%notifications`; +CREATE TABLE `%table_prefix%notifications` ( + `notification_id` bigint(32) NOT NULL AUTO_INCREMENT, + `notification_date_gmt` datetime NOT NULL, + `notification_user_id` bigint(32) NOT NULL, + `notification_trigger_user_id` bigint(32) DEFAULT NULL, + `notification_type` enum('follow','like') NOT NULL, + `notification_content_type` enum('user','image','album') NOT NULL, + `notification_type_id` bigint(32) NOT NULL COMMENT 'type_id based on action (type) table', + `notification_is_read` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`notification_id`), + KEY `notification_date_gmt` (`notification_date_gmt`), + KEY `notification_user_id` (`notification_user_id`), + KEY `notification_trigger_user_id` (`notification_trigger_user_id`), + KEY `notification_type` (`notification_type`), + KEY `notification_content_type` (`notification_content_type`), + KEY `notification_type_id` (`notification_type_id`), + KEY `notification_is_read` (`notification_is_read`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/pages.sql b/app/schemas/mysql-5/pages.sql new file mode 100644 index 0000000..826fa03 --- /dev/null +++ b/app/schemas/mysql-5/pages.sql @@ -0,0 +1,29 @@ +DROP TABLE IF EXISTS `%table_prefix%pages`; +CREATE TABLE `%table_prefix%pages` ( + `page_id` bigint(32) NOT NULL AUTO_INCREMENT, + `page_url_key` varchar(32) DEFAULT NULL, + `page_type` enum('internal','link') NOT NULL DEFAULT 'internal', + `page_file_path` varchar(255) DEFAULT NULL, + `page_link_url` mediumtext, + `page_icon` varchar(255) DEFAULT NULL, + `page_title` varchar(255) NOT NULL, + `page_description` mediumtext, + `page_keywords` mediumtext, + `page_is_active` tinyint(1) NOT NULL DEFAULT '1', + `page_is_link_visible` tinyint(1) NOT NULL DEFAULT '1', + `page_attr_target` enum('_self','_blank') DEFAULT '_self', + `page_attr_rel` varchar(255) DEFAULT NULL, + `page_sort_display` int(11) DEFAULT NULL, + `page_internal` varchar(255) DEFAULT NULL, + `page_code` mediumtext, + PRIMARY KEY (`page_id`), + UNIQUE KEY `page_internal` (`page_internal`(191)), + KEY `page_url_key` (`page_url_key`), + KEY `page_type` (`page_type`), + KEY `page_is_active` (`page_is_active`), + KEY `page_is_link_visible` (`page_is_link_visible`), + KEY `page_sort_display` (`page_sort_display`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; +INSERT INTO `%table_prefix%pages` VALUES ('1', 'tos', 'internal', null, null, 'fas fa-landmark', 'Terms of service', null, null, '1', '1', '_self', null, '1', 'tos', null); +INSERT INTO `%table_prefix%pages` VALUES ('2', 'privacy', 'internal', null, null, 'fas fa-lock', 'Privacy', null, null, '1', '1', '_self', null, '2', 'privacy', null); +INSERT INTO `%table_prefix%pages` VALUES ('3', 'contact', 'internal', null, null, 'fas fa-at', 'Contact', null, null, '1', '1', '_self', null, '3', 'contact', null); diff --git a/app/schemas/mysql-5/queues.sql b/app/schemas/mysql-5/queues.sql new file mode 100644 index 0000000..bf5e918 --- /dev/null +++ b/app/schemas/mysql-5/queues.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `%table_prefix%queues`; +CREATE TABLE `%table_prefix%queues` ( + `queue_id` bigint(32) NOT NULL AUTO_INCREMENT, + `queue_type` enum('storage-delete') NOT NULL, + `queue_date_gmt` datetime NOT NULL, + `queue_args` longtext NOT NULL, + `queue_join` bigint(32) NOT NULL, + `queue_attempts` varchar(255) DEFAULT '0', + `queue_status` enum('pending','failed') NOT NULL DEFAULT 'pending', + PRIMARY KEY (`queue_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/requests.sql b/app/schemas/mysql-5/requests.sql new file mode 100644 index 0000000..3814600 --- /dev/null +++ b/app/schemas/mysql-5/requests.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%requests`; +CREATE TABLE `%table_prefix%requests` ( + `request_id` bigint(32) NOT NULL AUTO_INCREMENT, + `request_type` enum('upload','signup','account-edit','account-password-forgot','account-password-reset','account-resend-activation','account-email-needed','account-change-email','account-activate','login', 'content-password', 'account-two-factor') NOT NULL, + `request_user_id` bigint(32) DEFAULT NULL, + `request_content_id` bigint(32) DEFAULT NULL, + `request_ip` varchar(255) NOT NULL, + `request_date` datetime NOT NULL, + `request_date_gmt` datetime NOT NULL, + `request_result` enum('success','fail') NOT NULL, + PRIMARY KEY (`request_id`), + KEY `request_type` (`request_type`), + KEY `request_user_id` (`request_user_id`), + KEY `request_content_id` (`request_content_id`), + KEY `request_ip` (`request_ip`), + KEY `request_date_gmt` (`request_date_gmt`), + KEY `request_result` (`request_result`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/settings.sql b/app/schemas/mysql-5/settings.sql new file mode 100644 index 0000000..297784a --- /dev/null +++ b/app/schemas/mysql-5/settings.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `%table_prefix%settings`; +CREATE TABLE `%table_prefix%settings` ( + `setting_id` int(11) NOT NULL AUTO_INCREMENT, + `setting_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `setting_value` mediumtext, + `setting_default` mediumtext, + `setting_typeset` enum('string','bool') DEFAULT 'string', + PRIMARY KEY (`setting_id`), + KEY `setting_name` (`setting_name`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/stats.sql b/app/schemas/mysql-5/stats.sql new file mode 100644 index 0000000..ec83675 --- /dev/null +++ b/app/schemas/mysql-5/stats.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%stats`; +CREATE TABLE `%table_prefix%stats` ( + `stat_id` bigint(32) NOT NULL AUTO_INCREMENT, + `stat_type` enum('total','date') NOT NULL, + `stat_date_gmt` date DEFAULT NULL, + `stat_users` bigint(32) NOT NULL DEFAULT '0', + `stat_images` bigint(32) NOT NULL DEFAULT '0', + `stat_albums` bigint(32) NOT NULL DEFAULT '0', + `stat_image_views` bigint(32) NOT NULL DEFAULT '0', + `stat_album_views` bigint(32) NOT NULL DEFAULT '0', + `stat_image_likes` bigint(32) NOT NULL DEFAULT '0', + `stat_album_likes` bigint(32) NOT NULL DEFAULT '0', + `stat_disk_used` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`stat_id`), + UNIQUE KEY `stat_date_gmt` (`stat_date_gmt`) USING BTREE, + KEY `stat_type` (`stat_type`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; +INSERT INTO `%table_prefix%stats` VALUES ('1', 'total', NULL, '0', '0', '0', '0', '0', '0', '0', '0'); diff --git a/app/schemas/mysql-5/storage_apis.sql b/app/schemas/mysql-5/storage_apis.sql new file mode 100644 index 0000000..b917bd4 --- /dev/null +++ b/app/schemas/mysql-5/storage_apis.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%storage_apis`; +CREATE TABLE `%table_prefix%storage_apis` ( + `storage_api_id` bigint(32) NOT NULL AUTO_INCREMENT, + `storage_api_name` varchar(255) NOT NULL, + `storage_api_type` varchar(255) NOT NULL, + PRIMARY KEY (`storage_api_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; +INSERT INTO `%table_prefix%storage_apis` VALUES ('1', 'Amazon S3', 's3'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('2', 'Google Cloud', 'gcloud'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('3', 'Microsoft Azure', 'azure'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('4', 'Chevereto Grid', 'chvgrid'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('5', 'FTP', 'ftp'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('6', 'SFTP', 'sftp'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('7', 'OpenStack', 'openstack'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('8', 'Local', 'local'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('9', 'S3 compatible', 's3compatible'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('10', 'Alibaba Cloud OSS', 'oss'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('11', 'Backblaze B2', 'b2'); diff --git a/app/schemas/mysql-5/storages.sql b/app/schemas/mysql-5/storages.sql new file mode 100644 index 0000000..f6e3c1b --- /dev/null +++ b/app/schemas/mysql-5/storages.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS `%table_prefix%storages`; +CREATE TABLE `%table_prefix%storages` ( + `storage_id` bigint(32) NOT NULL AUTO_INCREMENT, + `storage_api_id` bigint(32) NOT NULL, + `storage_name` varchar(255) NOT NULL, + `storage_service` varchar(255) DEFAULT NULL, + `storage_url` varchar(255) NOT NULL, + `storage_bucket` varchar(255) DEFAULT NULL, + `storage_region` varchar(255) DEFAULT NULL, + `storage_server` varchar(255) DEFAULT NULL, + `storage_account_id` varchar(255) DEFAULT NULL, + `storage_account_name` varchar(255) DEFAULT NULL, + `storage_key` mediumtext, + `storage_secret` mediumtext, + `storage_is_https` tinyint(1) NOT NULL DEFAULT '0', + `storage_is_active` tinyint(1) NOT NULL DEFAULT '0', + `storage_capacity` bigint(32) DEFAULT NULL, + `storage_space_used` bigint(32) DEFAULT '0', + PRIMARY KEY (`storage_id`), + KEY `storage_api_id` (`storage_api_id`), + KEY `storage_is_active` (`storage_is_active`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/two_factors.sql b/app/schemas/mysql-5/two_factors.sql new file mode 100644 index 0000000..9aab054 --- /dev/null +++ b/app/schemas/mysql-5/two_factors.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `%table_prefix%two_factors`; +CREATE TABLE `%table_prefix%two_factors` ( + `two_factor_id` bigint(32) NOT NULL AUTO_INCREMENT, + `two_factor_user_id` bigint(32) DEFAULT NULL, + `two_factor_date_gmt` datetime NOT NULL, + `two_factor_secret` mediumtext NOT NULL, + PRIMARY KEY (`two_factor_id`), + KEY `two_factor_user_id` (`two_factor_user_id`), + KEY `two_factor_date_gmt` (`two_factor_date_gmt`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-5/users.sql b/app/schemas/mysql-5/users.sql new file mode 100644 index 0000000..193c454 --- /dev/null +++ b/app/schemas/mysql-5/users.sql @@ -0,0 +1,57 @@ +DROP TABLE IF EXISTS `%table_prefix%users`; +CREATE TABLE `%table_prefix%users` ( + `user_id` bigint(32) NOT NULL AUTO_INCREMENT, + `user_name` varchar(255) DEFAULT NULL, + `user_username` varchar(255) NOT NULL, + `user_date` datetime NOT NULL, + `user_date_gmt` datetime NOT NULL, + `user_email` varchar(255) DEFAULT NULL, + `user_avatar_filename` varchar(255) DEFAULT NULL, + `user_facebook_username` varchar(255) DEFAULT NULL, + `user_twitter_username` varchar(255) DEFAULT NULL, + `user_website` varchar(255) DEFAULT NULL, + `user_background_filename` varchar(255) DEFAULT NULL, + `user_bio` varchar(255) DEFAULT NULL, + `user_timezone` varchar(255) NOT NULL, + `user_language` varchar(255) DEFAULT NULL, + `user_status` enum('valid','awaiting-confirmation','awaiting-email','banned') NOT NULL, + `user_is_admin` tinyint(1) NOT NULL DEFAULT '0', + `user_is_manager` tinyint(1) NOT NULL DEFAULT '0', + `user_is_private` tinyint(1) NOT NULL DEFAULT '0', + `user_palette_id` int(11) NOT NULL DEFAULT '0', + `user_newsletter_subscribe` tinyint(1) NOT NULL DEFAULT '1', + `user_show_nsfw_listings` tinyint(1) NOT NULL DEFAULT '0', + `user_image_count` bigint(32) NOT NULL DEFAULT '0', + `user_album_count` bigint(32) NOT NULL DEFAULT '0', + `user_image_keep_exif` tinyint(1) NOT NULL DEFAULT '1', + `user_image_expiration` varchar(255) DEFAULT NULL, + `user_registration_ip` varchar(255) NOT NULL, + `user_likes` bigint(32) NOT NULL DEFAULT '0' COMMENT 'Likes made to content owned by this user', + `user_liked` bigint(32) NOT NULL DEFAULT '0' COMMENT 'Likes made by this user', + `user_following` bigint(32) NOT NULL DEFAULT '0', + `user_followers` bigint(32) NOT NULL DEFAULT '0', + `user_content_views` bigint(32) NOT NULL DEFAULT '0', + `user_notifications_unread` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`user_id`), + UNIQUE KEY `username` (`user_username`(191)) USING BTREE, + UNIQUE KEY `email` (`user_email`(191)) USING BTREE, + KEY `user_date_gmt` (`user_date_gmt`), + KEY `user_status` (`user_status`), + KEY `user_is_admin` (`user_is_admin`), + KEY `user_is_manager` (`user_is_manager`), + KEY `user_is_private` (`user_is_private`), + KEY `user_palette_id` (`user_palette_id`), + KEY `user_newsletter_subscribe` (`user_newsletter_subscribe`), + KEY `user_show_nsfw_listings` (`user_show_nsfw_listings`), + KEY `user_image_count` (`user_image_count`), + KEY `user_album_count` (`user_album_count`), + KEY `user_image_keep_exif` (`user_image_keep_exif`), + KEY `user_image_expiration` (`user_image_expiration`), + KEY `user_registration_ip` (`user_registration_ip`), + KEY `user_likes` (`user_likes`), + KEY `user_following` (`user_following`), + KEY `user_followers` (`user_followers`), + KEY `user_liked` (`user_liked`), + KEY `user_content_views` (`user_content_views`), + FULLTEXT KEY `searchindex` (`user_name`,`user_username`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; diff --git a/app/schemas/mysql-8/albums.sql b/app/schemas/mysql-8/albums.sql new file mode 100644 index 0000000..0fa568e --- /dev/null +++ b/app/schemas/mysql-8/albums.sql @@ -0,0 +1,26 @@ +DROP TABLE IF EXISTS `%table_prefix%albums`; +CREATE TABLE `%table_prefix%albums` ( + `album_id` bigint(32) NOT NULL AUTO_INCREMENT, + `album_name` varchar(100) NOT NULL, + `album_user_id` bigint(32) DEFAULT NULL, + `album_date` datetime NOT NULL, + `album_date_gmt` datetime NOT NULL, + `album_creation_ip` varchar(255) NOT NULL, + `album_privacy` enum('public','password','private','private_but_link','custom') DEFAULT 'public', + `album_privacy_extra` mediumtext, + `album_password` mediumtext, + `album_image_count` bigint(32) NOT NULL DEFAULT '0', + `album_description` mediumtext, + `album_likes` bigint(32) NOT NULL DEFAULT '0', + `album_views` bigint(32) NOT NULL DEFAULT '0', + `album_cover_id` bigint(32) DEFAULT NULL, + `album_parent_id` bigint(32) DEFAULT NULL, + PRIMARY KEY (`album_id`), + KEY `album_name` (`album_name`), + KEY `album_user_id` (`album_user_id`), + KEY `album_date_gmt` (`album_date_gmt`), + KEY `album_privacy` (`album_privacy`), + KEY `album_image_count` (`album_image_count`), + KEY `album_creation_ip` (`album_creation_ip`), + FULLTEXT KEY `searchindex` (`album_name`,`album_description`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/api_keys.sql b/app/schemas/mysql-8/api_keys.sql new file mode 100644 index 0000000..64d3677 --- /dev/null +++ b/app/schemas/mysql-8/api_keys.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `%table_prefix%api_keys`; +CREATE TABLE `%table_prefix%api_keys` ( + `api_key_id` bigint(32) NOT NULL AUTO_INCREMENT, + `api_key_user_id` bigint(32) DEFAULT NULL, + `api_key_name` varchar(100) DEFAULT NULL, + `api_key_date_gmt` datetime NOT NULL, + `api_key_hash` mediumtext NOT NULL, + PRIMARY KEY (`api_key_id`), + KEY `api_key_user_id` (`api_key_user_id`), + KEY `api_key_name` (`api_key_name`), + KEY `api_key_date_gmt` (`api_key_date_gmt`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/assets.sql b/app/schemas/mysql-8/assets.sql new file mode 100644 index 0000000..1851dbe --- /dev/null +++ b/app/schemas/mysql-8/assets.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS `%table_prefix%assets`; +CREATE TABLE `%table_prefix%assets` ( + `asset_id` bigint(32) NOT NULL AUTO_INCREMENT, + `asset_key` varchar(255) NOT NULL, + `asset_md5` varchar(32) NOT NULL, + `asset_filename` varchar(255) NOT NULL, + `asset_file_path` varchar(255) NOT NULL, + `asset_blob` blob, + PRIMARY KEY (`asset_id`), + UNIQUE KEY `key` (`asset_key`) USING BTREE, + KEY `md5` (`asset_md5`), + KEY `filename` (`asset_filename`), + KEY `file_path` (`asset_file_path`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/categories.sql b/app/schemas/mysql-8/categories.sql new file mode 100644 index 0000000..e9cd8d8 --- /dev/null +++ b/app/schemas/mysql-8/categories.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS `%table_prefix%categories`; +CREATE TABLE `%table_prefix%categories` ( + `category_id` bigint(32) NOT NULL AUTO_INCREMENT, + `category_name` varchar(32) NOT NULL, + `category_url_key` varchar(32) NOT NULL, + `category_description` mediumtext, + PRIMARY KEY (`category_id`), + UNIQUE KEY `url_key` (`category_url_key`) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/confirmations.sql b/app/schemas/mysql-8/confirmations.sql new file mode 100644 index 0000000..db27811 --- /dev/null +++ b/app/schemas/mysql-8/confirmations.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `%table_prefix%confirmations`; +CREATE TABLE `%table_prefix%confirmations` ( + `confirmation_id` bigint(32) NOT NULL AUTO_INCREMENT, + `confirmation_user_id` bigint(32) NOT NULL, + `confirmation_type` enum('account-activate','account-change-email','account-password-forgot') NOT NULL, + `confirmation_date` datetime NOT NULL, + `confirmation_date_gmt` datetime NOT NULL, + `confirmation_token_hash` varchar(255) NOT NULL, + `confirmation_status` enum('active','valid','invalid') NOT NULL, + `confirmation_extra` mediumtext, + PRIMARY KEY (`confirmation_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/app/schemas/mysql-8/deletions.sql b/app/schemas/mysql-8/deletions.sql new file mode 100644 index 0000000..b5b732a --- /dev/null +++ b/app/schemas/mysql-8/deletions.sql @@ -0,0 +1,20 @@ +DROP TABLE IF EXISTS `%table_prefix%deletions`; +CREATE TABLE `%table_prefix%deletions` ( + `deleted_id` bigint(32) NOT NULL AUTO_INCREMENT, + `deleted_date_gmt` datetime NOT NULL, + `deleted_content_id` bigint(32) NOT NULL, + `deleted_content_date_gmt` datetime NOT NULL, + `deleted_content_user_id` bigint(32) DEFAULT NULL, + `deleted_content_ip` varchar(255) NOT NULL, + `deleted_content_md5` varchar(32) DEFAULT NULL, + `deleted_content_original_filename` varchar(255) DEFAULT NULL, + `deleted_content_views` bigint(32) NOT NULL DEFAULT '0', + `deleted_content_likes` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`deleted_id`), + KEY `deleted_content_id` (`deleted_content_id`), + KEY `deleted_content_user_id` (`deleted_content_user_id`), + KEY `deleted_content_ip` (`deleted_content_ip`), + KEY `deleted_content_md5` (`deleted_content_md5`), + KEY `deleted_content_views` (`deleted_content_views`), + KEY `deleted_content_likes` (`deleted_content_likes`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/follows.sql b/app/schemas/mysql-8/follows.sql new file mode 100644 index 0000000..211af22 --- /dev/null +++ b/app/schemas/mysql-8/follows.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS `%table_prefix%follows`; +CREATE TABLE `%table_prefix%follows` ( + `follow_id` bigint(32) NOT NULL AUTO_INCREMENT, + `follow_date` datetime NOT NULL, + `follow_date_gmt` datetime NOT NULL, + `follow_user_id` bigint(32) NOT NULL, + `follow_followed_user_id` bigint(32) NOT NULL, + `follow_ip` varchar(255) NOT NULL, + PRIMARY KEY (`follow_id`), + KEY `follow_user_id` (`follow_user_id`), + KEY `follow_followed_user_id` (`follow_followed_user_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/app/schemas/mysql-8/images.sql b/app/schemas/mysql-8/images.sql new file mode 100644 index 0000000..0c8f5f0 --- /dev/null +++ b/app/schemas/mysql-8/images.sql @@ -0,0 +1,60 @@ +DROP TABLE IF EXISTS `%table_prefix%images`; +CREATE TABLE `%table_prefix%images` ( + `image_id` bigint(32) NOT NULL AUTO_INCREMENT, + `image_name` varchar(255) NOT NULL, + `image_extension` varchar(255) NOT NULL, + `image_size` int(11) NOT NULL, + `image_width` int(11) NOT NULL, + `image_height` int(11) NOT NULL, + `image_date` datetime NOT NULL, + `image_date_gmt` datetime NOT NULL, + `image_title` varchar(100) DEFAULT NULL, + `image_description` mediumtext, + `image_nsfw` tinyint(1) NOT NULL DEFAULT '0', + `image_user_id` bigint(32) DEFAULT NULL, + `image_album_id` bigint(32) DEFAULT NULL, + `image_uploader_ip` varchar(255) NOT NULL, + `image_storage_mode` enum('datefolder','direct','old','path') NOT NULL DEFAULT 'datefolder', + `image_path` varchar(4096) DEFAULT NULL, + `image_storage_id` bigint(32) DEFAULT NULL, + `image_md5` varchar(32) NOT NULL, + `image_source_md5` varchar(32) DEFAULT NULL, + `image_original_filename` varchar(255) NOT NULL, + `image_original_exifdata` longtext, + `image_views` bigint(32) NOT NULL DEFAULT '0', + `image_category_id` bigint(32) DEFAULT NULL, + `image_chain` tinyint(128) NOT NULL, + `image_thumb_size` int(11) NOT NULL, + `image_medium_size` int(11) NOT NULL DEFAULT '0', + `image_expiration_date_gmt` datetime DEFAULT NULL, + `image_likes` bigint(32) NOT NULL DEFAULT '0', + `image_is_animated` tinyint(1) NOT NULL DEFAULT '0', + `image_is_approved` tinyint(1) NOT NULL DEFAULT '1', + `image_is_360` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`image_id`), + KEY `image_name` (`image_name`), + KEY `image_extension` (`image_extension`), + KEY `image_size` (`image_size`), + KEY `image_width` (`image_width`), + KEY `image_height` (`image_height`), + KEY `image_date_gmt` (`image_date_gmt`), + KEY `image_nsfw` (`image_nsfw`), + KEY `image_user_id` (`image_user_id`), + KEY `image_album_id` (`image_album_id`), + KEY `image_uploader_ip` (`image_uploader_ip`), + KEY `image_storage_mode` (`image_storage_mode`), + KEY `image_path` (`image_path`(255)), + KEY `image_storage_id` (`image_storage_id`), + KEY `image_md5` (`image_md5`), + KEY `image_source_md5` (`image_source_md5`), + KEY `image_views` (`image_views`), + KEY `image_category_id` (`image_category_id`), + KEY `image_chain` (`image_chain`), + KEY `image_expiration_date_gmt` (`image_expiration_date_gmt`), + KEY `image_likes` (`image_likes`), + KEY `image_is_animated` (`image_is_animated`), + KEY `image_is_approved` (`image_is_approved`), + KEY `image_is_360` (`image_is_360`), + KEY `image_album_id_image_id` (`image_album_id`, `image_id`), + FULLTEXT KEY `searchindex` (`image_name`,`image_title`,`image_description`,`image_original_filename`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/images_hash.sql b/app/schemas/mysql-8/images_hash.sql new file mode 100644 index 0000000..0ef0a0e --- /dev/null +++ b/app/schemas/mysql-8/images_hash.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS `%table_prefix%images_hash`; +CREATE TABLE `%table_prefix%images_hash` ( + `image_hash_image_id` bigint(32) NOT NULL, + `image_hash_hash` mediumtext NOT NULL, + PRIMARY KEY (`image_hash_image_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/importing.sql b/app/schemas/mysql-8/importing.sql new file mode 100644 index 0000000..c4aef57 --- /dev/null +++ b/app/schemas/mysql-8/importing.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `%table_prefix%importing`; +CREATE TABLE `%table_prefix%importing` ( + `importing_id` bigint(32) NOT NULL AUTO_INCREMENT, + `importing_import_id` bigint(32) NOT NULL, + `importing_path` varchar(4096) NOT NULL, + `importing_content_type` enum('user','album','image') NOT NULL, + `importing_content_id` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`importing_id`), + UNIQUE KEY `importing_path` (`importing_path`(767)) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/imports.sql b/app/schemas/mysql-8/imports.sql new file mode 100644 index 0000000..5745158 --- /dev/null +++ b/app/schemas/mysql-8/imports.sql @@ -0,0 +1,21 @@ +DROP TABLE IF EXISTS `%table_prefix%imports`; +CREATE TABLE `%table_prefix%imports` ( + `import_id` bigint(32) NOT NULL AUTO_INCREMENT, + `import_path` varchar(4096) NOT NULL, + `import_options` varchar(255) DEFAULT NULL, + `import_status` enum('queued','working','paused','canceled','completed') NOT NULL, + `import_users` bigint(32) NOT NULL DEFAULT '0', + `import_images` bigint(32) NOT NULL DEFAULT '0', + `import_albums` bigint(32) NOT NULL DEFAULT '0', + `import_time_created` datetime DEFAULT NULL, + `import_time_updated` datetime DEFAULT NULL, + `import_errors` tinyint(1) NOT NULL DEFAULT '0', + `import_started` tinyint(1) NOT NULL DEFAULT '0', + `import_continuous` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`import_id`), + KEY `import_path` (`import_path`(767)) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; + +INSERT INTO `%table_prefix%imports` VALUES ('1', '%rootPath%importing/no-parse', 'a:1:{s:4:"root";s:5:"plain";}', 'working', '0', '0', '0', NOW(), NOW(), '0', '1', '1'); +INSERT INTO `%table_prefix%imports` VALUES ('2', '%rootPath%importing/parse-users', 'a:1:{s:4:"root";s:5:"users";}', 'working', '0', '0', '0', NOW(), NOW(), '0', '1', '1'); +INSERT INTO `%table_prefix%imports` VALUES ('3', '%rootPath%importing/parse-albums', 'a:1:{s:4:"root";s:6:"albums";}', 'working', '0', '0', '0', NOW(), NOW(), '0', '1', '1'); \ No newline at end of file diff --git a/app/schemas/mysql-8/ip_bans.sql b/app/schemas/mysql-8/ip_bans.sql new file mode 100644 index 0000000..dd5bf2b --- /dev/null +++ b/app/schemas/mysql-8/ip_bans.sql @@ -0,0 +1,13 @@ +DROP TABLE IF EXISTS `%table_prefix%ip_bans`; +CREATE TABLE `%table_prefix%ip_bans` ( + `ip_ban_id` bigint(20) NOT NULL AUTO_INCREMENT, + `ip_ban_date` datetime NOT NULL, + `ip_ban_date_gmt` datetime NOT NULL, + `ip_ban_expires` datetime DEFAULT NULL, + `ip_ban_expires_gmt` datetime DEFAULT NULL, + `ip_ban_ip` varchar(255) NOT NULL, + `ip_ban_message` text, + PRIMARY KEY (`ip_ban_id`), + KEY `ip_ban_date_gmt` (`ip_ban_date_gmt`), + UNIQUE KEY `ip_ban_ip` (`ip_ban_ip`) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/likes.sql b/app/schemas/mysql-8/likes.sql new file mode 100644 index 0000000..8d9d4b1 --- /dev/null +++ b/app/schemas/mysql-8/likes.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%likes`; +CREATE TABLE `%table_prefix%likes` ( + `like_id` bigint(32) NOT NULL AUTO_INCREMENT, + `like_date` datetime NOT NULL, + `like_date_gmt` datetime NOT NULL, + `like_user_id` bigint(32) DEFAULT NULL, + `like_content_type` enum('image','album') DEFAULT NULL, + `like_content_id` bigint(32) NOT NULL, + `like_content_user_id` bigint(32) DEFAULT NULL, + `like_ip` varchar(255) NOT NULL, + PRIMARY KEY (`like_id`), + KEY `like_date_gmt` (`like_date_gmt`), + KEY `like_user_id` (`like_user_id`), + KEY `like_content_type` (`like_content_type`), + KEY `like_content_id` (`like_content_id`), + KEY `like_content_user_id` (`like_content_user_id`), + KEY `like_ip` (`like_ip`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; diff --git a/app/schemas/mysql-8/locks.sql b/app/schemas/mysql-8/locks.sql new file mode 100644 index 0000000..d9ba8e2 --- /dev/null +++ b/app/schemas/mysql-8/locks.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `%table_prefix%locks`; +CREATE TABLE `%table_prefix%locks` ( + `lock_id` bigint(20) NOT NULL AUTO_INCREMENT, + `lock_name` varchar(255) NOT NULL, + `lock_date_gmt` datetime NOT NULL, + `lock_expires_gmt` datetime DEFAULT NULL, + PRIMARY KEY (`lock_id`), + KEY `lock_date_gmt` (`lock_date_gmt`), + KEY `lock_expires_gmt` (`lock_expires_gmt`), + UNIQUE KEY `lock_name` (`lock_name`) USING BTREE +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/login_connections.sql b/app/schemas/mysql-8/login_connections.sql new file mode 100644 index 0000000..87c14e1 --- /dev/null +++ b/app/schemas/mysql-8/login_connections.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS `%table_prefix%login_connections`; +CREATE TABLE `%table_prefix%login_connections` ( + `login_connection_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_connection_user_id` bigint(32) NOT NULL, + `login_connection_provider_id` bigint(32) NOT NULL, + `login_connection_date_gmt` datetime NOT NULL, + `login_connection_resource_id` varchar(255) NOT NULL, + `login_connection_resource_name` mediumtext, + `login_connection_token` mediumtext NOT NULL COMMENT 'Ciphertext', + PRIMARY KEY (`login_connection_id`), + UNIQUE KEY `login_connection_unique` (`login_connection_user_id`,`login_connection_provider_id`), + KEY `login_connection_user_id` (`login_connection_user_id`), + KEY `login_connection_date_gmt` (`login_connection_date_gmt`), + KEY `login_connection_provider_id` (`login_connection_provider_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; + diff --git a/app/schemas/mysql-8/login_cookies.sql b/app/schemas/mysql-8/login_cookies.sql new file mode 100644 index 0000000..d41def3 --- /dev/null +++ b/app/schemas/mysql-8/login_cookies.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS `%table_prefix%login_cookies`; +CREATE TABLE `%table_prefix%login_cookies` ( + `login_cookie_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_cookie_user_id` bigint(32) NOT NULL, + `login_cookie_connection_id` bigint(32) DEFAULT 0, + `login_cookie_date_gmt` datetime NOT NULL, + `login_cookie_ip` varchar(255) DEFAULT NULL, + `login_cookie_user_agent` mediumtext NOT NULL, + `login_cookie_hash` mediumtext NOT NULL, + PRIMARY KEY (`login_cookie_id`), + UNIQUE KEY `login_cookie_unique` (`login_cookie_user_id`,`login_cookie_connection_id`,`login_cookie_date_gmt`), + KEY `login_cookie_user_id_date_gmt` (`login_cookie_user_id`, `login_cookie_date_gmt`), + KEY `login_cookie_user_id` (`login_cookie_user_id`), + KEY `login_cookie_ip` (`login_cookie_ip`), + KEY `login_cookie_connection_id` (`login_cookie_connection_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/login_passwords.sql b/app/schemas/mysql-8/login_passwords.sql new file mode 100644 index 0000000..91200f2 --- /dev/null +++ b/app/schemas/mysql-8/login_passwords.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS `%table_prefix%login_passwords`; +CREATE TABLE `%table_prefix%login_passwords` ( + `login_password_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_password_user_id` bigint(32) NOT NULL, + `login_password_date_gmt` datetime NOT NULL, + `login_password_hash` mediumtext NOT NULL, + PRIMARY KEY (`login_password_id`), + UNIQUE KEY `login_password_user_id` (`login_password_user_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/login_providers.sql b/app/schemas/mysql-8/login_providers.sql new file mode 100644 index 0000000..7aa3ba1 --- /dev/null +++ b/app/schemas/mysql-8/login_providers.sql @@ -0,0 +1,42 @@ +DROP TABLE IF EXISTS `%table_prefix%login_providers`; +CREATE TABLE `%table_prefix%login_providers` ( + `login_provider_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_provider_name` varchar(255) DEFAULT NULL, + `login_provider_label` varchar(255) DEFAULT NULL, + `login_provider_key_id` mediumtext DEFAULT NULL, + `login_provider_key_secret` mediumtext DEFAULT NULL, + `login_provider_is_enabled` tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (`login_provider_id`), + UNIQUE KEY `login_provider_name` (`login_provider_name`), + KEY `login_provider_is_enabled` (`login_provider_is_enabled`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; +INSERT INTO `%table_prefix%login_providers` VALUES ('1', 'facebook', 'Facebook', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('2', 'twitter', 'Twitter', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('3', 'google', 'Google', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('4', 'vkontakte', 'VK', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('5', 'apple', 'Apple', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('6', 'amazon', 'Amazon', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('7', 'bitbucket', 'BitBucket', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('8', 'discord', 'Discord', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('9', 'dribbble', 'Dribbble', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('10', 'dropbox', 'Dropbox', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('11', 'github', 'GitHub', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('12', 'gitlab', 'GitLab', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('13', 'instagram', 'Instagram', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('14', 'linkedin', 'LinkedIn', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('15', 'mailru', 'Mailru', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('16', 'medium', 'Medium', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('17', 'odnoklassniki', 'Odnoklassniki', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('18', 'orcid', 'ORCID', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('19', 'reddit', 'Reddit', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('20', 'spotify', 'Spotify', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('21', 'steam', 'Steam', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('22', 'strava', 'Strava', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('23', 'telegram', 'Telegram', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('24', 'tumblr', 'Tumblr', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('25', 'twitchtv', 'Twitch', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('26', 'wechat', 'WeChat', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('27', 'wordpress', 'WordPress', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('28', 'yandex', 'Yandex', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('29', 'yahoo', 'Yahoo', null, null, '0'); +INSERT INTO `%table_prefix%login_providers` VALUES ('30', 'qq', 'QQ', null, null, '0'); diff --git a/app/schemas/mysql-8/logins.sql b/app/schemas/mysql-8/logins.sql new file mode 100644 index 0000000..fbfe247 --- /dev/null +++ b/app/schemas/mysql-8/logins.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%logins`; +CREATE TABLE `%table_prefix%logins` ( + `login_id` bigint(32) NOT NULL AUTO_INCREMENT, + `login_user_id` bigint(32) NOT NULL, + `login_type` enum('password','session','cookie','facebook','twitter','google','vk','cookie_facebook','cookie_twitter','cookie_google','cookie_vk') NOT NULL, + `login_ip` varchar(255) DEFAULT NULL, + `login_hostname` mediumtext, + `login_date` datetime NOT NULL, + `login_date_gmt` datetime NOT NULL, + `login_resource_id` varchar(255) DEFAULT NULL, + `login_resource_name` mediumtext, + `login_resource_avatar` mediumtext, + `login_resource_url` mediumtext, + `login_secret` mediumtext DEFAULT NULL COMMENT 'The secret part', + `login_token_hash` mediumtext COMMENT 'Hashed complement to secret if needed', + PRIMARY KEY (`login_id`), + KEY `login_user_id` (`login_user_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/notifications.sql b/app/schemas/mysql-8/notifications.sql new file mode 100644 index 0000000..a5880c9 --- /dev/null +++ b/app/schemas/mysql-8/notifications.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS `%table_prefix%notifications`; +CREATE TABLE `%table_prefix%notifications` ( + `notification_id` bigint(32) NOT NULL AUTO_INCREMENT, + `notification_date_gmt` datetime NOT NULL, + `notification_user_id` bigint(32) NOT NULL, + `notification_trigger_user_id` bigint(32) DEFAULT NULL, + `notification_type` enum('follow','like') NOT NULL, + `notification_content_type` enum('user','image','album') NOT NULL, + `notification_type_id` bigint(32) NOT NULL COMMENT 'type_id based on action (type) table', + `notification_is_read` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`notification_id`), + KEY `notification_date_gmt` (`notification_date_gmt`), + KEY `notification_user_id` (`notification_user_id`), + KEY `notification_trigger_user_id` (`notification_trigger_user_id`), + KEY `notification_type` (`notification_type`), + KEY `notification_content_type` (`notification_content_type`), + KEY `notification_type_id` (`notification_type_id`), + KEY `notification_is_read` (`notification_is_read`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/app/schemas/mysql-8/pages.sql b/app/schemas/mysql-8/pages.sql new file mode 100644 index 0000000..15d0e94 --- /dev/null +++ b/app/schemas/mysql-8/pages.sql @@ -0,0 +1,29 @@ +DROP TABLE IF EXISTS `%table_prefix%pages`; +CREATE TABLE `%table_prefix%pages` ( + `page_id` bigint(32) NOT NULL AUTO_INCREMENT, + `page_url_key` varchar(32) DEFAULT NULL, + `page_type` enum('internal','link') NOT NULL DEFAULT 'internal', + `page_file_path` varchar(255) DEFAULT NULL, + `page_link_url` mediumtext, + `page_icon` varchar(255) DEFAULT NULL, + `page_title` varchar(255) NOT NULL, + `page_description` mediumtext, + `page_keywords` mediumtext, + `page_is_active` tinyint(1) NOT NULL DEFAULT '1', + `page_is_link_visible` tinyint(1) NOT NULL DEFAULT '1', + `page_attr_target` enum('_self','_blank') DEFAULT '_self', + `page_attr_rel` varchar(255) DEFAULT NULL, + `page_sort_display` int(11) DEFAULT NULL, + `page_internal` varchar(255) DEFAULT NULL, + `page_code` mediumtext, + PRIMARY KEY (`page_id`), + UNIQUE KEY `page_internal` (`page_internal`), + KEY `page_url_key` (`page_url_key`), + KEY `page_type` (`page_type`), + KEY `page_is_active` (`page_is_active`), + KEY `page_is_link_visible` (`page_is_link_visible`), + KEY `page_sort_display` (`page_sort_display`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; +INSERT INTO `%table_prefix%pages` VALUES ('1', 'tos', 'internal', null, null, 'fas fa-landmark', 'Terms of service', null, null, '1', '1', '_self', null, '1', 'tos', null); +INSERT INTO `%table_prefix%pages` VALUES ('2', 'privacy', 'internal', null, null, 'fas fa-lock', 'Privacy', null, null, '1', '1', '_self', null, '2', 'privacy', null); +INSERT INTO `%table_prefix%pages` VALUES ('3', 'contact', 'internal', null, null, 'fas fa-at', 'Contact', null, null, '1', '1', '_self', null, '3', 'contact', null); diff --git a/app/schemas/mysql-8/queues.sql b/app/schemas/mysql-8/queues.sql new file mode 100644 index 0000000..8e863e0 --- /dev/null +++ b/app/schemas/mysql-8/queues.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `%table_prefix%queues`; +CREATE TABLE `%table_prefix%queues` ( + `queue_id` bigint(32) NOT NULL AUTO_INCREMENT, + `queue_type` enum('storage-delete') NOT NULL, + `queue_date_gmt` datetime NOT NULL, + `queue_args` longtext NOT NULL, + `queue_join` bigint(32) NOT NULL, + `queue_attempts` varchar(255) DEFAULT '0', + `queue_status` enum('pending','failed') NOT NULL DEFAULT 'pending', + PRIMARY KEY (`queue_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/app/schemas/mysql-8/requests.sql b/app/schemas/mysql-8/requests.sql new file mode 100644 index 0000000..f55e6e9 --- /dev/null +++ b/app/schemas/mysql-8/requests.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%requests`; +CREATE TABLE `%table_prefix%requests` ( + `request_id` bigint(32) NOT NULL AUTO_INCREMENT, + `request_type` enum('upload','signup','account-edit','account-password-forgot','account-password-reset','account-resend-activation','account-email-needed','account-change-email','account-activate','login', 'content-password', 'account-two-factor') NOT NULL, + `request_user_id` bigint(32) DEFAULT NULL, + `request_content_id` bigint(32) DEFAULT NULL, + `request_ip` varchar(255) NOT NULL, + `request_date` datetime NOT NULL, + `request_date_gmt` datetime NOT NULL, + `request_result` enum('success','fail') NOT NULL, + PRIMARY KEY (`request_id`), + KEY `request_type` (`request_type`), + KEY `request_user_id` (`request_user_id`), + KEY `request_content_id` (`request_content_id`), + KEY `request_ip` (`request_ip`), + KEY `request_date_gmt` (`request_date_gmt`), + KEY `request_result` (`request_result`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; diff --git a/app/schemas/mysql-8/settings.sql b/app/schemas/mysql-8/settings.sql new file mode 100644 index 0000000..9c4a685 --- /dev/null +++ b/app/schemas/mysql-8/settings.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `%table_prefix%settings`; +CREATE TABLE `%table_prefix%settings` ( + `setting_id` int(11) NOT NULL AUTO_INCREMENT, + `setting_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `setting_value` mediumtext, + `setting_default` mediumtext, + `setting_typeset` enum('string','bool') DEFAULT 'string', + PRIMARY KEY (`setting_id`), + KEY `setting_name` (`setting_name`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/stats.sql b/app/schemas/mysql-8/stats.sql new file mode 100644 index 0000000..2b8beaf --- /dev/null +++ b/app/schemas/mysql-8/stats.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%stats`; +CREATE TABLE `%table_prefix%stats` ( + `stat_id` bigint(32) NOT NULL AUTO_INCREMENT, + `stat_type` enum('total','date') NOT NULL, + `stat_date_gmt` date DEFAULT NULL, + `stat_users` bigint(32) NOT NULL DEFAULT '0', + `stat_images` bigint(32) NOT NULL DEFAULT '0', + `stat_albums` bigint(32) NOT NULL DEFAULT '0', + `stat_image_views` bigint(32) NOT NULL DEFAULT '0', + `stat_album_views` bigint(32) NOT NULL DEFAULT '0', + `stat_image_likes` bigint(32) NOT NULL DEFAULT '0', + `stat_album_likes` bigint(32) NOT NULL DEFAULT '0', + `stat_disk_used` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`stat_id`), + UNIQUE KEY `stat_date_gmt` (`stat_date_gmt`) USING BTREE, + KEY `stat_type` (`stat_type`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; +INSERT INTO `%table_prefix%stats` VALUES ('1', 'total', NULL, '0', '0', '0', '0', '0', '0', '0', '0'); \ No newline at end of file diff --git a/app/schemas/mysql-8/storage_apis.sql b/app/schemas/mysql-8/storage_apis.sql new file mode 100644 index 0000000..886244c --- /dev/null +++ b/app/schemas/mysql-8/storage_apis.sql @@ -0,0 +1,18 @@ +DROP TABLE IF EXISTS `%table_prefix%storage_apis`; +CREATE TABLE `%table_prefix%storage_apis` ( + `storage_api_id` bigint(32) NOT NULL AUTO_INCREMENT, + `storage_api_name` varchar(255) NOT NULL, + `storage_api_type` varchar(255) NOT NULL, + PRIMARY KEY (`storage_api_id`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8; +INSERT INTO `%table_prefix%storage_apis` VALUES ('1', 'Amazon S3', 's3'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('2', 'Google Cloud', 'gcloud'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('3', 'Microsoft Azure', 'azure'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('4', 'Chevereto Grid', 'chvgrid'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('5', 'FTP', 'ftp'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('6', 'SFTP', 'sftp'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('7', 'OpenStack', 'openstack'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('8', 'Local', 'local'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('9', 'S3 compatible', 's3compatible'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('10', 'Alibaba Cloud OSS', 'oss'); +INSERT INTO `%table_prefix%storage_apis` VALUES ('11', 'Backblaze B2', 'b2'); \ No newline at end of file diff --git a/app/schemas/mysql-8/storages.sql b/app/schemas/mysql-8/storages.sql new file mode 100644 index 0000000..f300bbb --- /dev/null +++ b/app/schemas/mysql-8/storages.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS `%table_prefix%storages`; +CREATE TABLE `%table_prefix%storages` ( + `storage_id` bigint(32) NOT NULL AUTO_INCREMENT, + `storage_api_id` bigint(32) NOT NULL, + `storage_name` varchar(255) NOT NULL, + `storage_service` varchar(255) DEFAULT NULL, + `storage_url` varchar(255) NOT NULL, + `storage_bucket` varchar(255) DEFAULT NULL, + `storage_region` varchar(255) DEFAULT NULL, + `storage_server` varchar(255) DEFAULT NULL, + `storage_account_id` varchar(255) DEFAULT NULL, + `storage_account_name` varchar(255) DEFAULT NULL, + `storage_key` mediumtext, + `storage_secret` mediumtext, + `storage_is_https` tinyint(1) NOT NULL DEFAULT '0', + `storage_is_active` tinyint(1) NOT NULL DEFAULT '0', + `storage_capacity` bigint(32) DEFAULT NULL, + `storage_space_used` bigint(32) DEFAULT '0', + PRIMARY KEY (`storage_id`), + KEY `storage_api_id` (`storage_api_id`), + KEY `storage_is_active` (`storage_is_active`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/schemas/mysql-8/two_factors.sql b/app/schemas/mysql-8/two_factors.sql new file mode 100644 index 0000000..8234045 --- /dev/null +++ b/app/schemas/mysql-8/two_factors.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `%table_prefix%two_factors`; +CREATE TABLE `%table_prefix%two_factors` ( + `two_factor_id` bigint(32) NOT NULL AUTO_INCREMENT, + `two_factor_user_id` bigint(32) DEFAULT NULL, + `two_factor_date_gmt` datetime NOT NULL, + `two_factor_secret` mediumtext NOT NULL, + PRIMARY KEY (`two_factor_id`), + KEY `two_factor_user_id` (`two_factor_user_id`), + KEY `two_factor_date_gmt` (`two_factor_date_gmt`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; diff --git a/app/schemas/mysql-8/users.sql b/app/schemas/mysql-8/users.sql new file mode 100644 index 0000000..b18ed51 --- /dev/null +++ b/app/schemas/mysql-8/users.sql @@ -0,0 +1,57 @@ +DROP TABLE IF EXISTS `%table_prefix%users`; +CREATE TABLE `%table_prefix%users` ( + `user_id` bigint(32) NOT NULL AUTO_INCREMENT, + `user_name` varchar(255) DEFAULT NULL, + `user_username` varchar(255) NOT NULL, + `user_date` datetime NOT NULL, + `user_date_gmt` datetime NOT NULL, + `user_email` varchar(255) DEFAULT NULL, + `user_avatar_filename` varchar(255) DEFAULT NULL, + `user_facebook_username` varchar(255) DEFAULT NULL, + `user_twitter_username` varchar(255) DEFAULT NULL, + `user_website` varchar(255) DEFAULT NULL, + `user_background_filename` varchar(255) DEFAULT NULL, + `user_bio` varchar(255) DEFAULT NULL, + `user_timezone` varchar(255) NOT NULL, + `user_language` varchar(255) DEFAULT NULL, + `user_status` enum('valid','awaiting-confirmation','awaiting-email','banned') NOT NULL, + `user_is_admin` tinyint(1) NOT NULL DEFAULT '0', + `user_is_manager` tinyint(1) NOT NULL DEFAULT '0', + `user_is_private` tinyint(1) NOT NULL DEFAULT '0', + `user_palette_id` int(11) NOT NULL DEFAULT '0', + `user_newsletter_subscribe` tinyint(1) NOT NULL DEFAULT '1', + `user_show_nsfw_listings` tinyint(1) NOT NULL DEFAULT '0', + `user_image_count` bigint(32) NOT NULL DEFAULT '0', + `user_album_count` bigint(32) NOT NULL DEFAULT '0', + `user_image_keep_exif` tinyint(1) NOT NULL DEFAULT '1', + `user_image_expiration` varchar(255) DEFAULT NULL, + `user_registration_ip` varchar(255) NOT NULL, + `user_likes` bigint(32) NOT NULL DEFAULT '0' COMMENT 'Likes made to content owned by this user', + `user_liked` bigint(32) NOT NULL DEFAULT '0' COMMENT 'Likes made by this user', + `user_following` bigint(32) NOT NULL DEFAULT '0', + `user_followers` bigint(32) NOT NULL DEFAULT '0', + `user_content_views` bigint(32) NOT NULL DEFAULT '0', + `user_notifications_unread` bigint(32) NOT NULL DEFAULT '0', + PRIMARY KEY (`user_id`), + UNIQUE KEY `username` (`user_username`) USING BTREE, + UNIQUE KEY `email` (`user_email`) USING BTREE, + KEY `user_date_gmt` (`user_date_gmt`), + KEY `user_status` (`user_status`), + KEY `user_is_admin` (`user_is_admin`), + KEY `user_is_manager` (`user_is_manager`), + KEY `user_is_private` (`user_is_private`), + KEY `user_palette_id` (`user_palette_id`), + KEY `user_newsletter_subscribe` (`user_newsletter_subscribe`), + KEY `user_show_nsfw_listings` (`user_show_nsfw_listings`), + KEY `user_image_count` (`user_image_count`), + KEY `user_album_count` (`user_album_count`), + KEY `user_image_keep_exif` (`user_image_keep_exif`), + KEY `user_image_expiration` (`user_image_expiration`), + KEY `user_registration_ip` (`user_registration_ip`), + KEY `user_likes` (`user_likes`), + KEY `user_following` (`user_following`), + KEY `user_followers` (`user_followers`), + KEY `user_liked` (`user_liked`), + KEY `user_content_views` (`user_content_views`), + FULLTEXT KEY `searchindex` (`user_name`,`user_username`) +) ENGINE=%table_engine% DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/app/src/Actions/Auth/AuthVerifyCSRFTokenAction.php b/app/src/Actions/Auth/AuthVerifyCSRFTokenAction.php new file mode 100644 index 0000000..c6c041f --- /dev/null +++ b/app/src/Actions/Auth/AuthVerifyCSRFTokenAction.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Auth; + +use Chevere\Action\Action; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\String\AssertString; + +final class AuthVerifyCSRFTokenAction extends Action +{ + public function run( + #[ParameterAttribute(description: 'Token granted to the user session.')] + string $sessionValue, + #[ParameterAttribute(description: 'Token provided by the user.')] + string $userInput + ): array { + (new AssertString($sessionValue)) + ->same($userInput); + + return []; + } +} diff --git a/app/src/Actions/Auth/AuthVerifyRepositoryAccessAction.php b/app/src/Actions/Auth/AuthVerifyRepositoryAccessAction.php new file mode 100644 index 0000000..0ed0599 --- /dev/null +++ b/app/src/Actions/Auth/AuthVerifyRepositoryAccessAction.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Auth; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; + +class AuthVerifyRepositoryAccessAction extends Action +{ + public function getResponseParameters(): ParametersInterface + { + return parameters( + grant: stringParameter( + description: 'Describes the permission grantee.', + ) + ); + } + + public function run( + #[ParameterAttribute(description: 'User id for the user requesting this resource.')] + string $requesterUserId, + #[ParameterAttribute(description: 'Repository name to check access.')] + string $repository, + #[ParameterAttribute( + description: 'Permission level to check.', + regex: '/^(read|write|execute)$/' + )] + string $level + ): array { + return data( + grant: 'isAdmin' + ); + } +} diff --git a/app/src/Actions/Auth/AuthVerifyResourceAccessAction.php b/app/src/Actions/Auth/AuthVerifyResourceAccessAction.php new file mode 100644 index 0000000..4f11771 --- /dev/null +++ b/app/src/Actions/Auth/AuthVerifyResourceAccessAction.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Auth; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; + +class AuthVerifyResourceAccessAction extends Action +{ + public function run( + #[ParameterAttribute( + description: 'User id for the user requesting this resource.' + )] + int $requesterUserId, + #[ParameterAttribute( + description: 'User id for the owner of the resource.' + )] + int $ownerUserId, + #[ParameterAttribute( + description: 'Resource name to check access.' + )] + string $resource, + #[ + ParameterAttribute( + description: 'Permission level to check.', + regex: '/^(read|write|execute)$/' + )] + string $level + ): array { + return data( + grant: 'isAdmin' + ); + } + + public function getResponseParameters(): ParametersInterface + { + return parameters( + grant: stringParameter( + description: 'Describes the permission grantee.', + ) + ); + } +} diff --git a/app/src/Actions/Database/DatabaseReserveRowAction.php b/app/src/Actions/Database/DatabaseReserveRowAction.php new file mode 100644 index 0000000..d442a49 --- /dev/null +++ b/app/src/Actions/Database/DatabaseReserveRowAction.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Database; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use function Chevere\Parameter\integerParameter; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use Chevere\Parameter\Parameters; +use function Chevere\Parameter\parameters; +use Chevereto\Database\Database; + +/** + * Reserves a row in the database. + * + * Arguments: + * + * ```php + * table: string, + * ``` + * + * Response: + * + * ```php + * id: int, + * ``` + */ +class DatabaseReserveRowAction extends Action +{ + private Database $database; + + public function getContainerParameters(): ParametersInterface + { + return new Parameters( + database: objectParameter(Database::class) + ); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + id: integerParameter() + ); + } + + public function run(string $table): array + { + // $db->insert row + return data( + id: 123 + ); + } +} diff --git a/app/src/Actions/File/FileFetchSourceAction.php b/app/src/Actions/File/FileFetchSourceAction.php new file mode 100644 index 0000000..b566ed9 --- /dev/null +++ b/app/src/Actions/File/FileFetchSourceAction.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\File; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; +use Chevere\Serialize\Deserialize; +use function Chevereto\Encoding\assertBase64; +use function Chevereto\Encoding\storeDecodedBase64; +use function Chevereto\File\storeDownloadedUrl; +use Laminas\Uri\UriFactory; +use Throwable; + +final class FileFetchSourceAction extends Action +{ + public function getResponseParameters(): ParametersInterface + { + return + parameters( + filepath: stringParameter(), + ); + } + + public function run( + #[ParameterAttribute( + description: 'A binary file, base64 data, or an URL for a file.', + )] + string $source + ): array { + try { + $deserialize = new Deserialize($source); + $filepath = $deserialize->var()['tmp_name']; + } catch (Throwable) { + $filepath = tempnam(sys_get_temp_dir(), 'chv.temp'); + $uri = UriFactory::factory($source); + if ($uri->isValid()) { + storeDownloadedUrl($source, $filepath); + } else { + assertBase64($source); + storeDecodedBase64($source, $filepath); + } + } + + return data( + filepath: $filepath + ); + } +} diff --git a/app/src/Actions/File/FileNamingAction.php b/app/src/Actions/File/FileNamingAction.php new file mode 100644 index 0000000..549bf0f --- /dev/null +++ b/app/src/Actions/File/FileNamingAction.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\File; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Filesystem\Filename; +use Chevere\Filesystem\Interfaces\FilenameInterface; +use Chevere\Filesystem\Interfaces\PathInterface; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use function Chevere\String\randomString; +use Chevereto\Storage\Storage; + +/** + * Determines the best available target filename for the given storage, path and naming. + */ +class FileNamingAction extends Action +{ + public function run( + int $id, + #[ParameterAttribute( + regex: '/^.+\.[a-zA-Z]+$/' + )] + string $name, + Storage $storage, + PathInterface $path, + #[ParameterAttribute( + regex: '/^original|random|mixed|id$/' + )] + string $naming = 'original', + ): array { + $encodedId = 'encoded'; + $file = new Filename($name); + if ($naming === 'id') { + return ['filename' => new Filename($encodedId . '.' . $file->extension())]; + } + $name = $this->getName($naming, $file); + // USE OWN INDEX, REQUIRE STORAGE ID PARAM + while ($storage->adapter()->fileExists($path->getChild($name)->__toString())) { + if ($naming === 'original') { + $naming = 'mixed'; + } + $name = $this->getName($naming, $file); + } + + return data( + filename: new Filename($name) + ); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + filename: objectParameter( + className: Filename::class + ), + ); + } + + public function getName(string $naming, FilenameInterface $filename): string + { + return match ($naming) { + 'original' => $filename->__toString(), + 'random' => $this->getRandomName($filename), + 'mixed' => $this->getMixedName($filename), + }; + } + + private function getRandomName(FilenameInterface $filename): string + { + return randomString(32) . '.' . $filename->extension(); + } + + private function getMixedName(FilenameInterface $filename): string + { + $charsLength = 16; + $chars = randomString($charsLength); + $name = $filename->name(); + $nameLength = mb_strlen($name); + $withExtensionLength = mb_strlen($filename->extension()) + 1; + if ($nameLength + $charsLength > Filename::MAX_LENGTH_BYTES) { + $chop = Filename::MAX_LENGTH_BYTES - $charsLength - $nameLength - $withExtensionLength; + $name = mb_substr($name, 0, $chop); + } + + return $name . $chars . '.' . $filename->extension(); + } +} diff --git a/app/src/Actions/File/FileUploadAction.php b/app/src/Actions/File/FileUploadAction.php new file mode 100644 index 0000000..2249335 --- /dev/null +++ b/app/src/Actions/File/FileUploadAction.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\File; + +use Chevere\Action\Action; +use Chevere\Filesystem\Interfaces\FilenameInterface; +use Chevere\Filesystem\Interfaces\PathInterface; +use Chevereto\Storage\Storage; + +/** + * Upload the filename to the target storage. + * @TODO If this does storage, it should be under /Storage + */ +class FileUploadAction extends Action +{ + public function run( + string $filepath, + FilenameInterface $targetFilename, + Storage $storage, + PathInterface $path + ): array { + return []; + } +} diff --git a/app/src/Actions/File/FileValidateAction.php b/app/src/Actions/File/FileValidateAction.php new file mode 100644 index 0000000..34f0db2 --- /dev/null +++ b/app/src/Actions/File/FileValidateAction.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\File; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use function Chevere\Message\message; +use Chevere\Parameter\Attributes\ParameterAttribute; +use function Chevere\Parameter\integerParameter; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use function Safe\filesize; +use function Safe\md5_file; +use function Safe\mime_content_type; +use Throwable; + +/** + * Validate file type and its size. + */ +class FileValidateAction extends Action +{ + private array $mimes = []; + + private int $maxBytes = 0; + + private int $minBytes = 0; + + public function run( + #[ParameterAttribute( + description: 'Comma-separated list of allowed mime-types.', + regex: '/^([\w]+\/[\w\-\+\.]+)+(,([\w]+\/[\w\-\+\.]+))*$/' + )] + string $mimes, + string $filepath, + int $maxBytes = 0, + int $minBytes = 0, + ): array { + $this->mimes = explode(',', $mimes); + $this->minBytes = $minBytes; + $this->maxBytes = $maxBytes; + $bytes = $this->assertGetFileBytes($filepath); + $this->assertMaxBytes($bytes); + $this->assertMinBytes($bytes); + $mime = mime_content_type($filepath); + $this->assertMime($mime); + + return data( + bytes: $bytes, + mime: $mime, + md5: md5_file($filepath), + ); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + bytes : integerParameter(), + mime : stringParameter(), + md5 : stringParameter(), + ); + } + + /** + * @codeCoverageIgnore + */ + private function assertGetFileBytes(string $filepath): int + { + try { + return filesize($filepath); + } catch (Throwable $e) { + throw new InvalidArgumentException( + message($e->getMessage()), + 1000 + ); + } + } + + private function assertMinBytes(int $bytes): void + { + if ($this->minBytes === 0) { + return; + } + if ($bytes < $this->minBytes) { + throw new InvalidArgumentException( + message("Filesize (%fileSize%) doesn't meet the minimum bytes required (%required%)") + ->withCode('%fileSize%', (string) $bytes . ' B') + ->withCode('%required%', (string) $this->minBytes . ' B'), + 1001 + ); + } + } + + private function assertMaxBytes(int $bytes): void + { + if ($this->maxBytes === 0) { + return; + } + if ($bytes > $this->maxBytes) { + throw new InvalidArgumentException( + message('Filesize (%fileSize%) exceeds the maximum bytes allowed (%allowed%)') + ->withCode('%fileSize%', (string) $bytes . ' B') + ->withCode('%allowed%', (string) $this->maxBytes . ' B'), + 1002 + ); + } + } + + private function assertMime(string $mime): void + { + if (!in_array($mime, $this->mimes, true)) { + throw new InvalidArgumentException( + message('File mime-type %type% is not allowed (allows %allowed%)') + ->withCode('%type%', $mime) + ->withCode('%allowed%', implode(', ', $this->mimes)), + 1004 + ); + } + } +} diff --git a/app/src/Actions/File/FileVerifyNotDuplicateAction.php b/app/src/Actions/File/FileVerifyNotDuplicateAction.php new file mode 100644 index 0000000..5fe7348 --- /dev/null +++ b/app/src/Actions/File/FileVerifyNotDuplicateAction.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\File; + +use Chevere\Action\Action; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use Chevere\Parameter\Parameters; +use Chevereto\Database\Database; + +/** + * Detects file duplication based in both perceptual and file hashing, against the uploading frequency. + */ +class FileVerifyNotDuplicateAction extends Action +{ + private Database $database; + + public function getContainerParameters(): ParametersInterface + { + return new Parameters( + database: objectParameter(Database::class) + ); + } + + public function run( + #[ParameterAttribute(regex: '/^[a-f0-9]{32}$/')] + string $md5, + #[ParameterAttribute(regex: '/^[0-9A-F]+$/i')] + string $perceptual, + #[ParameterAttribute(regex: '/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/')] + string $ip, + #[ParameterAttribute(regex: '/^[4|6]$/')] + string $ipVersion + ): array { + // $db->query hash, rate, HALT if dupe + + return []; + } +} diff --git a/app/src/Actions/Image/ImageFetchMetaAction.php b/app/src/Actions/Image/ImageFetchMetaAction.php new file mode 100644 index 0000000..a85e2ed --- /dev/null +++ b/app/src/Actions/Image/ImageFetchMetaAction.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Image; + +use Chevere\Action\Action; +use Chevere\Parameter\ArrayParameter; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use Intervention\Image\Image; +use JeroenDesloovere\XmpMetadataExtractor\XmpMetadataExtractor; + +/** + * Fetch image metadata. + * + * Response parameters: + * + * ```php + * exif: array, + * iptc: array, + * xmp: array, + * ``` + */ +class ImageFetchMetaAction extends Action +{ + public function run(Image $image): array + { + $data = array_fill_keys(['exif', 'iptc', 'xmp'], []); + $data['exif'] = $image->exif() ?? []; + $data['iptc'] = $image->iptc() ?? []; + $xmpDataExtractor = new XmpMetadataExtractor(); + $data['xmp'] = $xmpDataExtractor->extractFromFile($image->basePath()); + + return $data; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + exif: new ArrayParameter(), + iptc: new ArrayParameter(), + xmp: new ArrayParameter(), + ); + } +} diff --git a/app/src/Actions/Image/ImageFixOrientationAction.php b/app/src/Actions/Image/ImageFixOrientationAction.php new file mode 100644 index 0000000..029cd63 --- /dev/null +++ b/app/src/Actions/Image/ImageFixOrientationAction.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Image; + +use Chevere\Action\Action; +use Intervention\Image\Image; + +/** + * Fix the image orientation based on Exif Orientation (if any, if needed). + */ +class ImageFixOrientationAction extends Action +{ + public function run(Image $image): array + { + $image->orientate()->save(); + + return []; + } +} diff --git a/app/src/Actions/Image/ImageInsertAction.php b/app/src/Actions/Image/ImageInsertAction.php new file mode 100644 index 0000000..95bad4e --- /dev/null +++ b/app/src/Actions/Image/ImageInsertAction.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Image; + +use Chevere\Action\Action; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use Chevere\Parameter\Parameters; +use Chevereto\Database\Database; + +/** + * Insert the image in the database. + */ +class ImageInsertAction extends Action +{ + private Database $database; + + public function getContainerParameters(): ParametersInterface + { + return new Parameters( + database: objectParameter(Database::class) + ); + } + + public function run( + int $id, + int $expires, + int $userId, + int $albumId, + ): array { + // TODO: DB inserting + return []; + } +} diff --git a/app/src/Actions/Image/ImageStripMetaAction.php b/app/src/Actions/Image/ImageStripMetaAction.php new file mode 100644 index 0000000..6c6def4 --- /dev/null +++ b/app/src/Actions/Image/ImageStripMetaAction.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Image; + +use Chevere\Action\Action; +use Imagick; +use Intervention\Image\Image; + +/** + * Strip image metadata. + */ +class ImageStripMetaAction extends Action +{ + public function run(Image $image): array + { + /** @var Imagick $imagick */ + $imagick = $image->getCore(); + if (!($imagick instanceof Imagick)) { + return []; + } + $profiles = $imagick->getImageProfiles('icc', true); + $imagick->stripImage(); + // @codeCoverageIgnoreStart + if (!empty($profiles)) { + $imagick->profileImage('icc', $profiles['icc']); + } + // @codeCoverageIgnoreEnd + $image->save(); + + return []; + } +} diff --git a/app/src/Actions/Image/ImageVerifyMediaAction.php b/app/src/Actions/Image/ImageVerifyMediaAction.php new file mode 100644 index 0000000..313050d --- /dev/null +++ b/app/src/Actions/Image/ImageVerifyMediaAction.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Image; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Message\Interfaces\MessageInterface; +use function Chevere\Message\message; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use function Chevereto\Image\imageHash; +use function Chevereto\Image\imageManager; +use Intervention\Image\Image; +use Intervention\Image\ImageManager; +use Throwable; + +/** + * Validates an image against the image processing and image dimensions. + * + * Response parameters: + * + * ```php + * image: \Intervention\Image\Image, + * perceptual: string, + * ``` + */ +class ImageVerifyMediaAction extends Action +{ + private int $width = 0; + + private int $height = 0; + + private int $maxWidth = 0; + + private int $maxHeight = 0; + + private int $minWidth = 0; + + private int $minHeight = 0; + + public function run( + string $filepath, + int $maxHeight, + int $maxWidth, + int $minHeight, + int $minWidth, + ): array { + $image = $this->assertGetImage($filepath); + $this->width = $image->width(); + $this->height = $image->height(); + $this->maxWidth = $maxWidth; + $this->maxHeight = $maxHeight; + $this->minWidth = $minWidth; + $this->minHeight = $minHeight; + $this->assertMinHeight(); + $this->assertMaxHeight(); + $this->assertMinWidth(); + $this->assertMaxWidth(); + + return data( + image: $image, + perceptual: imageHash()->hash($filepath) + ); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + image: objectParameter( + className: Image::class + ), + perceptual: stringParameter(), + ); + } + + private function assertGetImage(string $filepath): Image + { + try { + return imageManager()->make($filepath); + } catch (Throwable $e) { + throw new InvalidArgumentException( + previous: $e, + code: 1000, + message: message("Filepath %filepath% provided can't be handled by %manager%") + ->withCode('%filepath%', $filepath) + ->withCode('%manager%', ImageManager::class) + ); + } + } + + private function assertMinHeight(): void + { + if ($this->height < $this->minHeight) { + throw new InvalidArgumentException( + $this->getMinExceptionMessage('height', $this->height), + 1001 + ); + } + } + + private function assertMaxHeight(): void + { + if ($this->height > $this->maxHeight) { + throw new InvalidArgumentException( + $this->getMaxExceptionMessage('height', $this->height), + 1002 + ); + } + } + + private function assertMinWidth(): void + { + if ($this->width < $this->minWidth) { + throw new InvalidArgumentException( + $this->getMinExceptionMessage('width', $this->width), + 1003 + ); + } + } + + private function assertMaxWidth(): void + { + if ($this->width > $this->maxWidth) { + throw new InvalidArgumentException( + $this->getMaxExceptionMessage('width', $this->width), + 1004 + ); + } + } + + private function getMinExceptionMessage(string $dimension, int $provided): MessageInterface + { + return message("Image %dimension% %provided% doesn't meet the the minimum required (%required%)") + ->withCode('%dimension%', $dimension) + ->withCode('%provided%', (string) $provided) + ->withCode('%required%', $this->getMinRequired()); + } + + private function getMinRequired(): string + { + return (string) $this->minWidth . 'x' . (string) $this->minHeight; + } + + private function getMaxExceptionMessage(string $dimension, int $provided): MessageInterface + { + return message('Image %dimension% %provided% exceeds the maximum allowed (%allowed%)') + ->withCode('%dimension%', $dimension) + ->withCode('%provided%', (string) $provided) + ->withCode('%allowed%', $this->getMaxAllowed()); + } + + private function getMaxAllowed(): string + { + return (string) $this->maxWidth . 'x' . (string) $this->maxHeight; + } +} diff --git a/app/src/Actions/Legacy/Api/V1/ImageInsertAction.php b/app/src/Actions/Legacy/Api/V1/ImageInsertAction.php new file mode 100644 index 0000000..b26d561 --- /dev/null +++ b/app/src/Actions/Legacy/Api/V1/ImageInsertAction.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Legacy\Api\V1; + +use Chevere\Action\Action; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use Chevere\Parameter\Parameters; +use Chevereto\Database\Database; + +class ImageInsertAction extends Action +{ + private Database $database; + + public function getContainerParameters(): ParametersInterface + { + return new Parameters( + database: objectParameter(Database::class) + ); + } + + public function run(int $id): array + { + // TODO: DB inserting + return []; + } +} diff --git a/app/src/Actions/Legacy/Api/V1/LegacyApiV1OutputAction.php b/app/src/Actions/Legacy/Api/V1/LegacyApiV1OutputAction.php new file mode 100644 index 0000000..4511488 --- /dev/null +++ b/app/src/Actions/Legacy/Api/V1/LegacyApiV1OutputAction.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Legacy\Api\V1; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; + +class LegacyApiV1OutputAction extends Action +{ + public function run( + #[ParameterAttribute(regex: '/^(json|txt|redirect)$/')] + string $format + ): array { + return data( + document: 'formatted_document' + ); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + document: stringParameter() + ); + } +} diff --git a/app/src/Actions/Legacy/Api/V1/LegacyApiV1VerifyKeyAction.php b/app/src/Actions/Legacy/Api/V1/LegacyApiV1VerifyKeyAction.php new file mode 100644 index 0000000..2e82b62 --- /dev/null +++ b/app/src/Actions/Legacy/Api/V1/LegacyApiV1VerifyKeyAction.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Legacy\Api\V1; + +use Chevere\Action\Action; +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\InvalidArgumentException; + +class LegacyApiV1VerifyKeyAction extends Action +{ + public function run( + string $key, + string $apiV1Key + ): array { + if ($key !== $apiV1Key) { + throw new InvalidArgumentException( + message: message('Invalid API V1 key provided'), + code: 100 + ); + } + + return []; + } +} diff --git a/app/src/Actions/Storage/StorageGetForAssetAction.php b/app/src/Actions/Storage/StorageGetForAssetAction.php new file mode 100644 index 0000000..446d19f --- /dev/null +++ b/app/src/Actions/Storage/StorageGetForAssetAction.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Storage; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use Chevere\Parameter\Parameters; +use function Chevere\Parameter\parameters; +use Chevereto\Database\Database; +use Chevereto\Storage\Storage; +use League\Flysystem\Local\LocalFilesystemAdapter; + +/** + * Finds a valid storage to allocate the bytes required. + * + * Response parameters: + * + * ```php + * storage: \Chevereto\Interfaces\Storage\StorageInterface, + * ``` + */ +class StorageGetForAssetAction extends Action +{ + public function getContainerParameters(): ParametersInterface + { + return new Parameters( + database: objectParameter(Database::class) + ); + } + + public function run(int $userId, int $bytesRequired): array + { + // $adapter = db->query storage for user assert; + $adapter = new LocalFilesystemAdapter(__DIR__); + + return data( + storage: new Storage(__DIR__) + ); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + storage: objectParameter( + className: Storage::class + ) + ); + } +} diff --git a/app/src/Actions/Storage/StorageGetForUserAction.php b/app/src/Actions/Storage/StorageGetForUserAction.php new file mode 100644 index 0000000..ddaba37 --- /dev/null +++ b/app/src/Actions/Storage/StorageGetForUserAction.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Actions\Storage; + +use Chevere\Action\Action; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use Chevere\Parameter\Parameters; +use function Chevere\Parameter\parameters; +use Chevereto\Database\Database; +use Chevereto\Storage\Storage; + +/** + * Finds a valid storage to allocate the bytes required. + * + * Response parameters: + * + * ```php + * storage: \Chevereto\Interfaces\Storage\StorageInterface, + * ``` + */ +class StorageGetForUserAction extends Action +{ + public function getContainerParameters(): ParametersInterface + { + return new Parameters( + database: objectParameter(Database::class) + ); + } + + public function run(int $userId, int $bytesRequired): array + { + // $adapter = db->query storage for user; + + return data( + storage: new Storage(__DIR__) + ); + } + + public function getResponseParameters(): ParametersInterface + { + return parameters( + storage: objectParameter( + className: Storage::class + ) + ); + } +} diff --git a/app/src/Config/AssetConfig.php b/app/src/Config/AssetConfig.php new file mode 100644 index 0000000..6c867a4 --- /dev/null +++ b/app/src/Config/AssetConfig.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Config; + +final class AssetConfig +{ + public function __construct( + private string $accountId = '', + private string $accountName = '', + private string $bucket = '', + private string $key = '', + private string $region = '', + private string $secret = '', + private string $server = '', + private string $service = '', + private string $url = '', + private string $type = 'local', + private string $name = 'assets', + ) { + } + + public function export() + { + return [ + 'accountId' => $this->accountId, + 'accountName' => $this->accountName, + 'bucket' => $this->bucket, + 'key' => $this->key, + 'region' => $this->region, + 'secret' => $this->secret, + 'server' => $this->server, + 'service' => $this->service, + 'url' => $this->url, + 'type' => $this->type, + 'name' => $this->name, + ]; + } + + public function accountId(): string + { + return $this->accountId; + } + + public function accountName(): string + { + return $this->accountName; + } + + public function bucket(): string + { + return $this->bucket; + } + + public function key(): string + { + return $this->key; + } + + public function name(): string + { + return $this->name; + } + + public function region(): string + { + return $this->region; + } + + public function secret(): string + { + return $this->secret; + } + + public function server(): string + { + return $this->server; + } + + public function service(): string + { + return $this->service; + } + + public function type(): string + { + return $this->type; + } + + public function url(): string + { + return $this->url; + } +} diff --git a/app/src/Config/Config.php b/app/src/Config/Config.php new file mode 100644 index 0000000..89a44b8 --- /dev/null +++ b/app/src/Config/Config.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Config; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Chevereto\Traits\Instance\AssertStaticInstanceTrait; + +final class Config +{ + use AssertStaticInstanceTrait; + + private static AssetConfig $asset; + + private static EnabledConfig $enabled; + + private static HostConfig $host; + + private static SystemConfig $system; + + private static LimitConfig $limit; + + public function __construct( + AssetConfig $asset, + EnabledConfig $enabled, + HostConfig $host, + SystemConfig $system, + LimitConfig $limit, + ) { + if (isset(static::$asset)) { + throw new LogicException( + message('An instance of %type% has been already created.') + ->withCode('%type%', static::class), + 600 + ); + } + static::$asset = $asset; + static::$enabled = $enabled; + static::$host = $host; + static::$system = $system; + static::$limit = $limit; + } + + public static function asset(): AssetConfig + { + return static::$asset; + } + + public static function enabled(): EnabledConfig + { + return static::$enabled; + } + + public static function host(): HostConfig + { + return static::$host; + } + + public static function system(): SystemConfig + { + return static::$system; + } + + public static function limit(): LimitConfig + { + return static::$limit; + } +} diff --git a/app/src/Config/DatabaseConfig.php b/app/src/Config/DatabaseConfig.php new file mode 100644 index 0000000..ddf2106 --- /dev/null +++ b/app/src/Config/DatabaseConfig.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Config; + +final class DatabaseConfig +{ + public function __construct( + private string $driver = 'mysql', + private string $host = 'localhost', + private string $name = 'chevereto', + private string $user = 'chevereto', + private string $pass = 'user_database_password', + private string $tablePrefix = 'chv_', + private array $pdoAttrs = [], + private int $port = 3306, + ) { + } + + public function export(): array + { + return [ + 'driver' => $this->driver, + 'host' => $this->host, + 'name' => $this->name, + 'user' => $this->user, + 'pass' => $this->pass, + 'tablePrefix' => $this->tablePrefix, + 'pdoAttrs' => $this->pdoAttrs, + 'port' => $this->port, + ]; + } + + public function driver(): string + { + return $this->driver; + } + + public function host(): string + { + return $this->host; + } + + public function name(): string + { + return $this->name; + } + + public function pass(): string + { + return $this->pass; + } + + public function pdoAttrs(): array + { + return $this->pdoAttrs; + } + + public function port(): int + { + return $this->port; + } + + public function tablePrefix(): string + { + return $this->tablePrefix; + } + + public function user(): string + { + return $this->user; + } +} diff --git a/app/src/Config/EnabledConfig.php b/app/src/Config/EnabledConfig.php new file mode 100644 index 0000000..6e922cc --- /dev/null +++ b/app/src/Config/EnabledConfig.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Config; + +final class EnabledConfig +{ + public function __construct( + private bool $phpPages = false, + private bool $updateCli = true, + private bool $updateHttp = true, + private bool $htaccessCheck = true, + ) { + } + + public function phpPages(): bool + { + return $this->phpPages; + } + + public function updateCli(): bool + { + return $this->updateCli; + } + + public function updateHttp(): bool + { + return $this->updateHttp; + } + + public function htaccessCheck(): bool + { + return $this->htaccessCheck; + } +} diff --git a/app/src/Config/HostConfig.php b/app/src/Config/HostConfig.php new file mode 100644 index 0000000..e343ce7 --- /dev/null +++ b/app/src/Config/HostConfig.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Config; + +final class HostConfig +{ + public function __construct( + private string $hostnamePath = '/', + private string $hostname = 'localhost', + private bool $isHttps = false, + ) { + } + + public function hostnamePath(): string + { + return $this->hostnamePath; + } + + public function hostname(): string + { + return $this->hostname; + } + + public function isHttps(): bool + { + return $this->isHttps; + } +} diff --git a/app/src/Config/LimitConfig.php b/app/src/Config/LimitConfig.php new file mode 100644 index 0000000..68632b7 --- /dev/null +++ b/app/src/Config/LimitConfig.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace Chevereto\Config; + +final class LimitConfig +{ + public function __construct( + private int $invalidRequestsPerDay = 25, + ) { + } + + public function invalidRequestsPerDay(): int + { + return $this->invalidRequestsPerDay; + } +} diff --git a/app/src/Config/SystemConfig.php b/app/src/Config/SystemConfig.php new file mode 100644 index 0000000..68b93d1 --- /dev/null +++ b/app/src/Config/SystemConfig.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Config; + +final class SystemConfig +{ + public function __construct( + private int $debugLevel = 1, + private string $errorLog = 'php://stderr', + private array $imageFormatsAvailable = ['PNG', 'GIF', 'JPEG', 'BMP', 'WEBP'], + private string $imageLibrary = 'imagick', + private string $sessionSaveHandler = 'files', + private string $sessionSavePath = '/tmp', + ) { + } + + public function debugLevel(): int + { + return $this->debugLevel; + } + + public function errorLog(): string + { + return $this->errorLog; + } + + public function imageFormatsAvailable(): array + { + return $this->imageFormatsAvailable; + } + + public function imageLibrary(): string + { + return $this->imageLibrary; + } + + public function sessionSaveHandler(): string + { + return $this->sessionSaveHandler; + } + + public function sessionSavePath(): string + { + return $this->sessionSavePath; + } +} diff --git a/app/src/Controllers/Api/V1/Upload/UploadPostController.php b/app/src/Controllers/Api/V1/Upload/UploadPostController.php new file mode 100644 index 0000000..b1313b2 --- /dev/null +++ b/app/src/Controllers/Api/V1/Upload/UploadPostController.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V1\Upload; + +use function Chevere\DataStructure\data; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; +use Chevere\Workflow\Attributes\Provider; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Legacy\LegacyUploadPostWorkflow; + +#[Provider(LegacyUploadPostWorkflow::class)] +final class UploadPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Uploads an image resource.'; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + document: stringParameter() + ); + } + + public function run( + #[ParameterAttribute(description: 'A binary file, base64 data, or an URL for an image.')] + string $source, // try: files + #[ParameterAttribute(description: 'API V1 key.')] + string $key, + #[ParameterAttribute( + description: 'Response document output format.', + regex: '/^(json|txt|redirect)$/' + )] + string $format = 'json' + ): array { + return data(); + } +} diff --git a/app/src/Controllers/Api/V4/Album/AlbumDeleteController.php b/app/src/Controllers/Api/V4/Album/AlbumDeleteController.php new file mode 100644 index 0000000..786b37e --- /dev/null +++ b/app/src/Controllers/Api/V4/Album/AlbumDeleteController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Album; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Album\AlbumDeleteWorkflow; + +#[RelationWorkflow(AlbumDeleteWorkflow::class)] +final class AlbumDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Delete an album identified by its id.'; + } + + public function run( + #[ParameterAttribute( + description: 'The identifier.', + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Album/AlbumGetController.php b/app/src/Controllers/Api/V4/Album/AlbumGetController.php new file mode 100644 index 0000000..1b540e2 --- /dev/null +++ b/app/src/Controllers/Api/V4/Album/AlbumGetController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Album; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Album\AlbumGetWorkflow; + +#[RelationWorkflow(AlbumGetWorkflow::class)] +final class AlbumGetController extends WorkflowController +{ + public function getDescription(): string + { + return 'Get an album identified by its id.'; + } + + public function run( + #[ParameterAttribute( + description: 'The identifier.', + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Album/AlbumPatchController.php b/app/src/Controllers/Api/V4/Album/AlbumPatchController.php new file mode 100644 index 0000000..62d6704 --- /dev/null +++ b/app/src/Controllers/Api/V4/Album/AlbumPatchController.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Album; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Album\AlbumPatchWorkflow; + +#[RelationWorkflow(AlbumPatchWorkflow::class)] +final class AlbumPatchController extends WorkflowController +{ + public function getDescription(): string + { + return 'Updates the album.'; + } + + public function run( + #[ParameterAttribute( + description: 'The identifier.', + regex: '/\w+/' + )] + string $id, + #[ParameterAttribute( + description: 'The image identifier.', + regex: '/\w+/' + )] + string $cover_id + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + image: objectParameter(Image::class) + ); + } +} diff --git a/app/src/Controllers/Api/V4/Album/AlbumPostController.php b/app/src/Controllers/Api/V4/Album/AlbumPostController.php new file mode 100644 index 0000000..9287ced --- /dev/null +++ b/app/src/Controllers/Api/V4/Album/AlbumPostController.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Album; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Album\AlbumPostWorkflow; + +#[RelationWorkflow(AlbumPostWorkflow::class)] +final class AlbumPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Creates an album.'; + } + + public function run( + string $description, + string $name, + string $parent_id, + string $password, + string $privacy, + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + album: objectParameter( + className: Album::class + ), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Album/Like/AlbumLikeDeleteController.php b/app/src/Controllers/Api/V4/Album/Like/AlbumLikeDeleteController.php new file mode 100644 index 0000000..985467d --- /dev/null +++ b/app/src/Controllers/Api/V4/Album/Like/AlbumLikeDeleteController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Album\Like; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Album\Like\AlbumLikeDeleteWorkflow; + +#[RelationWorkflow(AlbumLikeDeleteWorkflow::class)] +final class AlbumLikeDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Delete album like.'; + } + + public function run( + #[ParameterAttribute( + description: 'The identifier.', + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Album/Like/AlbumLikePostController.php b/app/src/Controllers/Api/V4/Album/Like/AlbumLikePostController.php new file mode 100644 index 0000000..440a1d7 --- /dev/null +++ b/app/src/Controllers/Api/V4/Album/Like/AlbumLikePostController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Album\Like; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Album\Like\AlbumLikePostWorkflow; + +#[RelationWorkflow(AlbumLikePostWorkflow::class)] +final class AlbumLikePostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Like the album.'; + } + + public function run( + #[ParameterAttribute( + description: 'The identifier.', + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Ban/Ip/BanIpDeleteController.php b/app/src/Controllers/Api/V4/Ban/Ip/BanIpDeleteController.php new file mode 100644 index 0000000..1857ad9 --- /dev/null +++ b/app/src/Controllers/Api/V4/Ban/Ip/BanIpDeleteController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Ban\Ip; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Ban\Ip\BanIpDeleteWorkflow; + +#[RelationWorkflow(BanIpDeleteWorkflow::class)] +final class BanIpDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Delete an IP ban identified by its id.'; + } + + public function run( + #[ParameterAttribute( + description: 'The identifier.', + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Ban/Ip/BanIpPatchController.php b/app/src/Controllers/Api/V4/Ban/Ip/BanIpPatchController.php new file mode 100644 index 0000000..3567677 --- /dev/null +++ b/app/src/Controllers/Api/V4/Ban/Ip/BanIpPatchController.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Ban\Ip; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Ban\Ip\BanIpPatchWorkflow; + +#[RelationWorkflow(BanIpPatchWorkflow::class)] +final class BanIpPatchController extends WorkflowController +{ + public function getDescription(): string + { + return 'Updates the album.'; + } + + public function run( + #[ParameterAttribute( + description: 'The album identifier.', + regex: '/\w+/' + )] + string $id, + #[ParameterAttribute( + description: 'The image identifier.', + regex: '/\w+/' + )] + string $cover_id = '' + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + image: objectParameter(Image::class) + ); + } +} diff --git a/app/src/Controllers/Api/V4/Ban/Ip/BanIpPostController.php b/app/src/Controllers/Api/V4/Ban/Ip/BanIpPostController.php new file mode 100644 index 0000000..0f15b02 --- /dev/null +++ b/app/src/Controllers/Api/V4/Ban/Ip/BanIpPostController.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Ban\Ip; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; + +final class BanIpPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Creates a IP ban.'; + } + + public function run( + string $ip, + #[ParameterAttribute( + regex: '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/' + )] + string $expires, + string $message + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + ban_ip: objectParameter( + className: BanIp::class + ), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Category/CategoryPostController.php b/app/src/Controllers/Api/V4/Category/CategoryPostController.php new file mode 100644 index 0000000..4a66c46 --- /dev/null +++ b/app/src/Controllers/Api/V4/Category/CategoryPostController.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Category; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; + +final class CategoryPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Creates a category.'; + } + + public function run( + string $name, + #[ParameterAttribute( + description: 'Category URL key (slug)', + regex: '/^[-\w]+$/', + )] + string $url_key, + string $description + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + category: objectParameter( + className: Category::class + ), + ); + } +} diff --git a/app/src/Controllers/Api/V4/File/FilePostController.php b/app/src/Controllers/Api/V4/File/FilePostController.php new file mode 100644 index 0000000..23e82fb --- /dev/null +++ b/app/src/Controllers/Api/V4/File/FilePostController.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\File; + +use Chevere\Controller\Controller; +use function Chevere\DataStructure\data; + +abstract class FilePostController extends Controller +{ + public function run( + string $source + ): array { + return data(); + } +} diff --git a/app/src/Controllers/Api/V4/Image/Bulk/ImageBulkPatchController.php b/app/src/Controllers/Api/V4/Image/Bulk/ImageBulkPatchController.php new file mode 100644 index 0000000..240461a --- /dev/null +++ b/app/src/Controllers/Api/V4/Image/Bulk/ImageBulkPatchController.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Image\Bulk; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\ArrayParameter; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Image\Bulk\ImageBulkPatchWorkflow; + +#[RelationWorkflow(ImageBulkPatchWorkflow::class)] +class ImageBulkPatchController extends WorkflowController +{ + public function getDescription(): string + { + return 'Bulk image edit.'; + } + + public function run( + #[ParameterAttribute( + description: 'Comma-separated list of images to edit.', + regex: '/^\w+(,+\w+)*$/' + )] + string $image_ids, + string $category_id = '', + string $is_approved = '', + string $is_nsfw = '', + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + edited: new ArrayParameter(), + failed: new ArrayParameter(), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Image/ImageGetController.php b/app/src/Controllers/Api/V4/Image/ImageGetController.php new file mode 100644 index 0000000..fd57f3a --- /dev/null +++ b/app/src/Controllers/Api/V4/Image/ImageGetController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Image; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Image\ImageGetWorkflow; + +#[RelationWorkflow(ImageGetWorkflow::class)] +final class ImageGetController extends WorkflowController +{ + public function getDescription(): string + { + return 'Get the image identified by its id.'; + } + + public function run( + #[ParameterAttribute( + description: 'The image identifier.', + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Image/ImagePatchController.php b/app/src/Controllers/Api/V4/Image/ImagePatchController.php new file mode 100644 index 0000000..37ee74a --- /dev/null +++ b/app/src/Controllers/Api/V4/Image/ImagePatchController.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Image; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\Image\ImagePatchWorkflow; + +#[RelationWorkflow(ImagePatchWorkflow::class)] +class ImagePatchController extends WorkflowController +{ + public function getDescription(): string + { + return 'Edit the image resource.'; + } + + public function run( + #[ParameterAttribute( + description: 'The image identifier.', + regex: '/\w+/' + )] + string $id, + string $category_id, + #[ParameterAttribute( + regex: '/^(0|1)$/' + )] + string $is_approved, + #[ParameterAttribute( + regex: '/^(0|1)$/' + )] + string $is_nsfw + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + image: objectParameter(stdClass::class, Image::class), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Image/ImagePostController.php b/app/src/Controllers/Api/V4/Image/ImagePostController.php new file mode 100644 index 0000000..508af7b --- /dev/null +++ b/app/src/Controllers/Api/V4/Image/ImagePostController.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Image; + +use Chevere\Controller\Controller; +use function Chevere\DataStructure\data; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Workflows\Image\ImagePostWorkflow; + +final class ImagePostController extends Controller +{ + public function run( + #[ParameterAttribute( + description: 'A binary file, base64 data, or an URL for an image.', + // try: 'files' + )] + string $image, + string $album_id = '', + ): array { + $workflow = (new ImagePostWorkflow())->getWorkflow(); + // $source + // $mimes + // $max_bytes + // $min_bytes + // $max_width + // $max_height + // $min_width + // $min_height + // $ip + // $ip_version + // $user_id + // $table + // $name + // $naming + // $path + // $upload_filepath + // $expires + // $album_id + return data(); + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + image: objectParameter( + className: Image::class + ) + ); + } +} diff --git a/app/src/Controllers/Api/V4/Image/Like/ImageLikeDeleteController.php b/app/src/Controllers/Api/V4/Image/Like/ImageLikeDeleteController.php new file mode 100644 index 0000000..10c5ef8 --- /dev/null +++ b/app/src/Controllers/Api/V4/Image/Like/ImageLikeDeleteController.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Image\Like; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; + +class ImageLikeDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Delete image like.'; + } + + public function run( + #[ParameterAttribute( + description: 'The image identifier.', + regex: '/\w+/' + )] + string $id, + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Image/Like/ImageLikePostController.php b/app/src/Controllers/Api/V4/Image/Like/ImageLikePostController.php new file mode 100644 index 0000000..3a3c7c6 --- /dev/null +++ b/app/src/Controllers/Api/V4/Image/Like/ImageLikePostController.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Image\Like; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; + +class ImageLikePostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Like the image.'; + } + + public function run( + #[ParameterAttribute( + description: ('The image identifier.'), + regex: '/\w+/' + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Stat/Rebuild/StatRebuildPostController.php b/app/src/Controllers/Api/V4/Stat/Rebuild/StatRebuildPostController.php new file mode 100644 index 0000000..22aae27 --- /dev/null +++ b/app/src/Controllers/Api/V4/Stat/Rebuild/StatRebuildPostController.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Stat\Rebuild; + +use Chevereto\Controllers\WorkflowController; + +class StatRebuildPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Rebuild stats.'; + } +} diff --git a/app/src/Controllers/Api/V4/Storage/Migrate/StorageMigratePostController.php b/app/src/Controllers/Api/V4/Storage/Migrate/StorageMigratePostController.php new file mode 100644 index 0000000..209b34c --- /dev/null +++ b/app/src/Controllers/Api/V4/Storage/Migrate/StorageMigratePostController.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Storage\Migrate; + +use Chevereto\Controllers\WorkflowController; + +class StorageMigratePostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Migrate all stored content to another storage.'; + } + + public function run(string $storage_id): array + { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/Storage/Stat/Regen/StorageStatRegenPostController.php b/app/src/Controllers/Api/V4/Storage/Stat/Regen/StorageStatRegenPostController.php new file mode 100644 index 0000000..56dc12d --- /dev/null +++ b/app/src/Controllers/Api/V4/Storage/Stat/Regen/StorageStatRegenPostController.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Storage\Stat\Regen; + +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use stdClass; + +class StorageStatRegenPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Regenerate storage stats.'; + } + + public function run(string $storage_id): array + { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + storage: objectParameter(stdClass::class, Storage::class), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Storage/StoragePostController.php b/app/src/Controllers/Api/V4/Storage/StoragePostController.php new file mode 100644 index 0000000..d5468bb --- /dev/null +++ b/app/src/Controllers/Api/V4/Storage/StoragePostController.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Storage; + +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; + +class StoragePostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Creates a storage.'; + } + + public function run( + string $account_id, + string $account_name, + string $api_id, + string $bucket, + string $capacity, + string $id, + string $key, + string $name, + string $region, + string $secret, + string $server, + string $service, + string $url, + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + storage: objectParameter( + className: Storage::class + ), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Tool/Id/Decode/ToolDecodeIdGetController.php b/app/src/Controllers/Api/V4/Tool/Id/Decode/ToolDecodeIdGetController.php new file mode 100644 index 0000000..f4de8e3 --- /dev/null +++ b/app/src/Controllers/Api/V4/Tool/Id/Decode/ToolDecodeIdGetController.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Tool\Id\Decode; + +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; +use Chevereto\Controllers\WorkflowController; + +class ToolDecodeIdGetController extends WorkflowController +{ + public function getDescription(): string + { + return 'Retrieve a decoded representation of the Id.'; + } + + public function run(string $id): array + { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + data: stringParameter(), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Tool/Id/Encode/ToolEncodeIdGetController.php b/app/src/Controllers/Api/V4/Tool/Id/Encode/ToolEncodeIdGetController.php new file mode 100644 index 0000000..1a7e66a --- /dev/null +++ b/app/src/Controllers/Api/V4/Tool/Id/Encode/ToolEncodeIdGetController.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Tool\Id\Encode; + +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\parameters; +use function Chevere\Parameter\stringParameter; +use Chevereto\Controllers\WorkflowController; + +class ToolEncodeIdGetController extends WorkflowController +{ + public function getDescription(): string + { + return 'Retrieve an encoded representation of the Id.'; + } + + public function run(string $id): array + { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + data: stringParameter(), + ); + } +} diff --git a/app/src/Controllers/Api/V4/Tool/Probe/Email/ToolProbeEmailPostController.php b/app/src/Controllers/Api/V4/Tool/Probe/Email/ToolProbeEmailPostController.php new file mode 100644 index 0000000..b81569f --- /dev/null +++ b/app/src/Controllers/Api/V4/Tool/Probe/Email/ToolProbeEmailPostController.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\Tool\Probe\Email; + +use Chevereto\Controllers\WorkflowController; + +class ToolProbeEmailPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Probe email delivery.'; + } + + public function run(string $email): array + { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/User/Asset/Avatar/UserAssetAvatarDeleteController.php b/app/src/Controllers/Api/V4/User/Asset/Avatar/UserAssetAvatarDeleteController.php new file mode 100644 index 0000000..a8fa2b8 --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Asset/Avatar/UserAssetAvatarDeleteController.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Asset\Avatar; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\User\Asset\Avatar\UserAssetAvatarDeleteWorkflow; + +#[RelationWorkflow(UserAssetAvatarDeleteWorkflow::class)] +final class UserAssetAvatarDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Delete the user avatar image resource.'; + } + + public function run( + #[ParameterAttribute( + description: 'The username.', + regex: '/\w+/' + )] + string $username + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/User/Asset/Avatar/UserAssetAvatarPostController.php b/app/src/Controllers/Api/V4/User/Asset/Avatar/UserAssetAvatarPostController.php new file mode 100644 index 0000000..5229cc2 --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Asset/Avatar/UserAssetAvatarPostController.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Asset\Avatar; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\Api\V4\File\FilePostController; +use Chevereto\Workflows\User\Asset\Avatar\UserAssetAvatarPostWorkflow; + +#[RelationWorkflow(UserAssetAvatarPostWorkflow::class)] +final class UserAssetAvatarPostController extends FilePostController +{ + public function getDescription(): string + { + return 'Uploads an image resource to be used as user avatar'; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + file_info: objectParameter( + className: FileInfo::class + ) + ); + } +} diff --git a/app/src/Controllers/Api/V4/User/Asset/Background/UserAssetBackgroundDeleteController.php b/app/src/Controllers/Api/V4/User/Asset/Background/UserAssetBackgroundDeleteController.php new file mode 100644 index 0000000..f592ce7 --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Asset/Background/UserAssetBackgroundDeleteController.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Asset\Background; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\User\Asset\Background\UserAssetBackgroundDeleteWorkflow; + +#[RelationWorkflow(UserAssetBackgroundDeleteWorkflow::class)] +final class UserAssetBackgroundDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Delete the user background image resource.'; + } +} diff --git a/app/src/Controllers/Api/V4/User/Asset/Background/UserAssetBackgroundPostController.php b/app/src/Controllers/Api/V4/User/Asset/Background/UserAssetBackgroundPostController.php new file mode 100644 index 0000000..ba7f5ae --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Asset/Background/UserAssetBackgroundPostController.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Asset\Background; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\Api\V4\File\FilePostController; +use Chevereto\Workflows\User\Asset\Background\UserAssetBackgroundPostWorkflow; + +#[RelationWorkflow(UserAssetBackgroundPostWorkflow::class)] +final class UserAssetBackgroundPostController extends FilePostController +{ + public function getDescription(): string + { + return 'Uploads an image resource to be used as user background.'; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + file_info: objectParameter( + className: FileInfo::class + ) + ); + } +} diff --git a/app/src/Controllers/Api/V4/User/Export/UserExportGetController.php b/app/src/Controllers/Api/V4/User/Export/UserExportGetController.php new file mode 100644 index 0000000..59516fa --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Export/UserExportGetController.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Export; + +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use stdClass; + +class UserExportGetController extends WorkflowController +{ + public function getDescription(): string + { + return 'Exports the user.'; + } + + public function run(string $username): array + { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + key: objectParameter(stdClass::class, 'className'), + ); + } +} diff --git a/app/src/Controllers/Api/V4/User/Follow/UserFollowDeleteController.php b/app/src/Controllers/Api/V4/User/Follow/UserFollowDeleteController.php new file mode 100644 index 0000000..4042a73 --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Follow/UserFollowDeleteController.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Follow; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; + +class UserFollowDeleteController extends WorkflowController +{ + public function getDescription(): string + { + return 'Unfollow the user.'; + } + + public function run( + #[ParameterAttribute( + description: 'The username.', + regex: '/\w+/' + )] + string $username + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/User/Follow/UserFollowPostController.php b/app/src/Controllers/Api/V4/User/Follow/UserFollowPostController.php new file mode 100644 index 0000000..7514dcb --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Follow/UserFollowPostController.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Follow; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; + +class UserFollowPostController extends WorkflowController +{ + public function getDescription(): string + { + return 'Follows the user.'; + } + + public function run( + #[ParameterAttribute( + description: 'The username.', + regex: '/\w+/' + )] + string $username + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/User/Setting/UserSettingPatchController.php b/app/src/Controllers/Api/V4/User/Setting/UserSettingPatchController.php new file mode 100644 index 0000000..873075a --- /dev/null +++ b/app/src/Controllers/Api/V4/User/Setting/UserSettingPatchController.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User\Setting; + +use Chevere\Controller\Attributes\RelationWorkflow; +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use stdClass; + +#[RelationWorkflow('')] +class UserSettingPatchController extends WorkflowController +{ + public function getDescription(): string + { + return 'Updates user settings.'; + } + + public function run( + #[ParameterAttribute( + description: 'The user identifier.' + )] + string $userId + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + key: objectParameter(stdClass::class, 'className'), + ); + } +} diff --git a/app/src/Controllers/Api/V4/User/UserGetController.php b/app/src/Controllers/Api/V4/User/UserGetController.php new file mode 100644 index 0000000..47cb91b --- /dev/null +++ b/app/src/Controllers/Api/V4/User/UserGetController.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevereto\Controllers\WorkflowController; + +final class UserGetController extends WorkflowController +{ + public function getDescription(): string + { + return 'Get an user identified by its id.'; + } + + public function run( + #[ParameterAttribute( + description: 'The user identifier.', + regex: '/\w+/', + )] + string $id + ): array { + return []; + } +} diff --git a/app/src/Controllers/Api/V4/User/UserPostController.php b/app/src/Controllers/Api/V4/User/UserPostController.php new file mode 100644 index 0000000..df86346 --- /dev/null +++ b/app/src/Controllers/Api/V4/User/UserPostController.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers\Api\V4\User; + +use Chevere\Parameter\Attributes\ParameterAttribute; +use Chevere\Parameter\Interfaces\ParametersInterface; +use function Chevere\Parameter\objectParameter; +use function Chevere\Parameter\parameters; +use Chevereto\Controllers\WorkflowController; +use Chevereto\Workflows\User\UserPostWorkflow; + +final class UserPostController extends WorkflowController +{ + public function getWorkflowName(): string + { + return UserPostWorkflow::class; + } + + public function getDescription(): string + { + return 'Creates an user.'; + } + + public function run( + #[ParameterAttribute( + regex: '/^[\w]{3,16}$/' + )] + string $username, + string $email, + #[ParameterAttribute( + regex: '/^.{6,128}$/' + )] + string $password, + #[ParameterAttribute( + regex: '/^(user|manager|admin)$/', + )] + string $role = 'user' + ): array { + return []; + } + + public function getResponseParameters(): ParametersInterface + { + return + parameters( + user: objectParameter( + className: User::class + ) + ); + } +} diff --git a/app/src/Controllers/LegacyController.php b/app/src/Controllers/LegacyController.php new file mode 100644 index 0000000..b9fbc1b --- /dev/null +++ b/app/src/Controllers/LegacyController.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers; + +use Chevere\Controller\Controller; +use Chevere\Parameter\Interfaces\ArgumentsInterface; +use Chevere\Response\Interfaces\ResponseInterface; + +final class LegacyController extends Controller +{ + final public function run(ArgumentsInterface $arguments): ResponseInterface + { + return $this + ->getResponse( + document: $arguments->getString('document'), + ); + } +} diff --git a/app/src/Controllers/WorkflowController.php b/app/src/Controllers/WorkflowController.php new file mode 100644 index 0000000..b472518 --- /dev/null +++ b/app/src/Controllers/WorkflowController.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Controllers; + +use Chevere\Controller\Controller; +use function Chevere\Message\message; +use Chevere\Workflow\Interfaces\WorkflowInterface; +use Chevere\Workflow\Interfaces\WorkflowProviderInterface; +use LogicException; + +abstract class WorkflowController extends Controller +{ + final public function getWorkflow(): WorkflowInterface + { + $relation = $this->relation(); + if ($relation === '') { + throw new LogicException( + message: message('Missing workflow provider relationship') + ); + } + if (!is_subclass_of($relation, WorkflowProviderInterface::class, true)) { + throw new LogicException( + message: message('Relation %relation% is not of type %type%') + ->withCode('%relation%', $relation) + ->withCode('%type%', WorkflowProviderInterface::class) + ); + } + /** @var WorkflowProviderInterface $workflowProvider */ + $workflowProvider = new $relation(); + + return $workflowProvider->getWorkflow(); + // $this->hook('getWorkflow:after', $workflow); + } +} diff --git a/app/src/Controllers/functions.php b/app/src/Controllers/functions.php new file mode 100644 index 0000000..97831c6 --- /dev/null +++ b/app/src/Controllers/functions.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace App\Controllers; + +use Chevereto\Controllers\LegacyController; + +/** + * @var String $route `name.php` file. + */ +function legacyController(string $route) +{ + return new LegacyController(dispatch: $route); +} diff --git a/app/src/Database/Database.php b/app/src/Database/Database.php new file mode 100644 index 0000000..b7ef8f1 --- /dev/null +++ b/app/src/Database/Database.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace Chevereto\Database; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Query\QueryBuilder; + +final class Database +{ + private Connection $connection; + + private QueryBuilder $queryBuilder; + + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + public function getQueryBuilder(): QueryBuilder + { + return new QueryBuilder($this->connection); + } +} diff --git a/app/src/Database/EntitiesIo.php b/app/src/Database/EntitiesIo.php new file mode 100644 index 0000000..48e82ce --- /dev/null +++ b/app/src/Database/EntitiesIo.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Database; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\OutOfBoundsException; +use Chevereto\Database\Traits\GetWhereEqualsTrait; +use Doctrine\DBAL\Result; + +/** + * Provides database I/O for the X entities. + */ +abstract class EntitiesIo implements EntitiesIoInterface +{ + use GetWhereEqualsTrait; + + protected Database $database; + + public function __construct(Database $database) + { + $this->database = $database; + } + + abstract public function table(): string; + + public function selectWhereAllValues(array $columns = ['*'], string ...$values): array + { + return $this->selectWhereValues($columns, ...$values); + } + + public function selectWhereAnyValues(array $columns = ['*'], string ...$values): array + { + return $this->selectWhereValues($columns, ...$values); + } + + protected function selectWhereValues(array $columns = ['*'], string ...$values): array + { + $all = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] === 'selectWhereAllValues'; + $queryBuilder = $this->database->getQueryBuilder() + ->select(...$columns) + ->from($this->table()); + foreach ($values as $column => $value) { + $column = (string) $column; + $where = $this->getWhereEquals($column); + if ($all) { + $queryBuilder->andWhere($where); + } else { + $queryBuilder->orWhere($where); + } + $queryBuilder->setParameter($column, $value); + } + /** @var Result $result */ + $result = $queryBuilder->execute(); + $fetch = $result->fetchAllAssociative(); + if ($fetch === false) { + throw new OutOfBoundsException( + message: message('No record exists for values provided') + ); + } + + return $fetch; + } +} diff --git a/app/src/Database/EntitiesIoInterface.php b/app/src/Database/EntitiesIoInterface.php new file mode 100644 index 0000000..bb46d98 --- /dev/null +++ b/app/src/Database/EntitiesIoInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Database; + +/** + * Describes the component in charge of providing multiple entity I/O interaction. + */ +interface EntitiesIoInterface +{ + public function __construct(Database $database); + + /** + * Defines the table name. + */ + public function table(): string; + + /** + * Select the entities for the given values (all). + * + * @return array Raw associative result. + */ + public function selectWhereAllValues(array $columns = ['*'], string ...$values): array; + + /** + * Select the entities for the given values (any). + * + * @return array Raw associative result. + */ + public function selectWhereAnyValues(array $columns = ['*'], string ...$values): array; +} diff --git a/app/src/Database/EntityIo.php b/app/src/Database/EntityIo.php new file mode 100644 index 0000000..3bcf887 --- /dev/null +++ b/app/src/Database/EntityIo.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Database; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\OutOfBoundsException; +use Chevereto\Database\Traits\GetWhereEqualsTrait; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Result; + +/** + * Provides database I/O for the X entity. + */ +abstract class EntityIo implements EntityIoInterface +{ + use GetWhereEqualsTrait; + + protected Database $database; + + protected string $whereIdClause; + + public function __construct(Database $database) + { + $this->database = $database; + $this->whereIdClause = $this->getWhereEquals($this->id()); + } + + abstract public function table(): string; + + abstract public function id(): string; + + public function select(int $id, string ...$columns): array + { + $args = empty($columns) ? ['*'] : $columns; + $queryBuilder = $this->database->getQueryBuilder() + ->select(...$args) + ->from($this->table()) + ->where($this->whereIdClause) + ->setParameter($this->id(), $id, ParameterType::INTEGER); + /** @var Result $result */ + $result = $queryBuilder->execute(); + $fetch = $result->fetchAssociative(); + if ($fetch === false) { + throw new OutOfBoundsException( + message('No record exists for id %id%') + ->withCode('%id%', (string) $id) + ); + } + + return $fetch; + } + + public function delete(int $id): int + { + return $this->database->getQueryBuilder() + ->delete($this->table()) + ->where($this->whereIdClause) + ->setParameter($this->id(), $id, ParameterType::INTEGER) + ->execute(); + } + + public function update(int $id, string ...$values): int + { + $queryBuilder = $this->database->getQueryBuilder() + ->update($this->table()); + foreach ($values as $column => $value) { + $column = (string) $column; + $queryBuilder + ->set($column, ":${column}") + ->setParameter($column, $value); + } + + return $queryBuilder + ->where($this->whereIdClause) + ->setParameter($this->id(), $id, ParameterType::INTEGER) + ->execute(); + } + + public function insert(string ...$values): int + { + $queryBuilder = $this->database->getQueryBuilder() + ->insert($this->table()); + foreach ($values as $column => $value) { + $column = (string) $column; + $queryBuilder + ->setValue($column, ":${column}") + ->setParameter($column, $value); + } + $result = $queryBuilder->execute(); + if ($result === 1) { + return (int) $queryBuilder->getConnection()->lastInsertId(); + } + + return 0; + } +} diff --git a/app/src/Database/EntityIoInterface.php b/app/src/Database/EntityIoInterface.php new file mode 100644 index 0000000..a4646d2 --- /dev/null +++ b/app/src/Database/EntityIoInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Database; + +/** + * Describes the component in charge of providing entity I/O interaction. + */ +interface EntityIoInterface +{ + public function __construct(Database $database); + + /** + * Defines the table name. + */ + public function table(): string; + + /** + * Defines the column id name. + */ + public function id(): string; + + /** + * Select the entity columns identified by its id. + * + * @return array Raw associative result. + */ + public function select(int $id, string ...$columns): array; + + /** + * @return int Number of deleted rows `0`, `1`. + */ + public function delete(int $id): int; + + /** + * @return int Number of updated rows. + */ + public function update(int $id, string ...$values): int; + + /** + * @return int Last inserted Id. + */ + public function insert(string ...$values): int; +} diff --git a/app/src/Database/Traits/GetWhereEqualsTrait.php b/app/src/Database/Traits/GetWhereEqualsTrait.php new file mode 100644 index 0000000..13f5ec2 --- /dev/null +++ b/app/src/Database/Traits/GetWhereEqualsTrait.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Database\Traits; + +trait GetWhereEqualsTrait +{ + private function getWhereEquals(string $column): string + { + return str_replace('%s', $column, '%s = :%s'); + } +} diff --git a/app/src/Encoding/functions.php b/app/src/Encoding/functions.php new file mode 100644 index 0000000..7f30fd9 --- /dev/null +++ b/app/src/Encoding/functions.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encoding; + +use function Chevere\Message\message; +use Chevere\Regex\Interfaces\RegexInterface; +use Chevere\Regex\Regex; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use Chevere\Throwable\Exceptions\RuntimeException; +use Safe\Exceptions\FilesystemException; +use Safe\Exceptions\StreamException; +use function Safe\fclose; +use function Safe\fopen; +use function Safe\fwrite; +use function Safe\stream_filter_append; + +/** + * @throws InvalidArgumentException + */ +function assertBase64(string $string): void +{ + $double = base64_encode(base64_decode($string, true)); + if ($string !== $double) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( + message('Invalid base64 formatting'), + 600 + ); + // @codeCoverageIgnoreEnd + } + unset($double); +} + +/** + * @param string $base64 A base64 encoded string + * @param string $filepath Filename or stream to store decoded base64 + * + * @throws FilesystemException + * @throws StreamException + * @throws RuntimeException + */ +function storeDecodedBase64(string $base64, string $filepath): void +{ + $filter = 'convert.base64-decode'; + $fh = fopen($filepath, 'w'); + stream_filter_append($fh, $filter, STREAM_FILTER_WRITE); + if (fwrite($fh, $base64) === 0) { + // @codeCoverageIgnoreStart + throw new RuntimeException( + message('Unable to write %filter% provided string') + ->withCode('%filter%', $filter), + 1200 + ); + // @codeCoverageIgnoreEnd + } + fclose($fh); +} + +function getBase64Regex(): RegexInterface +{ + return new Regex('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/'); +} diff --git a/app/src/Encryption/Decode.php b/app/src/Encryption/Decode.php new file mode 100644 index 0000000..7d4241c --- /dev/null +++ b/app/src/Encryption/Decode.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use Chevereto\Encryption\Interfaces\DecodeInterface; +use Chevereto\Encryption\Interfaces\EncryptionInterface; + +final class Decode implements DecodeInterface +{ + private string $decoded; + + private string $nonce; + + private string $cipherText; + + public function __construct(string $encoded) + { + $this->decoded = base64_decode($encoded, true); + $this->nonce = mb_substr( + $this->decoded, + 0, + EncryptionInterface::NONCE_LENGTH, + self::ENCODING + ); + assertNonce($this->nonce); + $this->cipherText = mb_substr( + $this->decoded, + EncryptionInterface::NONCE_LENGTH, + null, + self::ENCODING + ); + } + + public function __toString(): string + { + return $this->decoded; + } + + public function nonce(): string + { + return $this->nonce; + } + + public function cipherText(): string + { + return $this->cipherText; + } +} diff --git a/app/src/Encryption/Encode.php b/app/src/Encryption/Encode.php new file mode 100644 index 0000000..296194c --- /dev/null +++ b/app/src/Encryption/Encode.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use Chevereto\Encryption\Interfaces\EncodeInterface; +use Chevereto\Encryption\Interfaces\EncryptionInterface; + +final class Encode implements EncodeInterface +{ + public function __construct( + private EncryptionInterface $encryption + ) { + } + + public function encrypt(string $text): string + { + return base64_encode( + $this->encryption->nonce() . $this->encryption->encrypt($text) + ); + } +} diff --git a/app/src/Encryption/Encryption.php b/app/src/Encryption/Encryption.php new file mode 100644 index 0000000..275dbd8 --- /dev/null +++ b/app/src/Encryption/Encryption.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use function Chevere\VariableSupport\deepCopy; +use Chevereto\Encryption\Interfaces\EncryptionInterface; +use Chevereto\Encryption\Interfaces\KeyInterface; +use phpseclib3\Crypt\ChaCha20; + +final class Encryption implements EncryptionInterface +{ + private ChaCha20 $cipher; + + private string $nonce; + + public function __construct(KeyInterface $key) + { + $this->nonce = randomNonce(); + $this->cipher = new ChaCha20(); + $this->cipher->setNonce($this->nonce); + $this->cipher->setKey((string) $key); + } + + public function __clone() + { + $this->cipher = deepCopy($this->cipher); + } + + public function nonce(): string + { + return $this->nonce; + } + + public function withNonce(string $nonce): self + { + assertNonce($nonce); + $new = clone $this; + $new->nonce = $nonce; + $new->cipher->setNonce($new->nonce); + + return $new; + } + + public function withRandomNonce(): self + { + $new = clone $this; + $new->nonce = randomNonce(); + $new->cipher->setNonce($new->nonce); + + return $new; + } + + public function encrypt(string $plainText): string + { + return $this->cipher->encrypt($plainText); + } + + public function decrypt(string $cipherText): string + { + return $this->cipher->decrypt($cipherText); + } +} diff --git a/app/src/Encryption/EncryptionInstance.php b/app/src/Encryption/EncryptionInstance.php new file mode 100644 index 0000000..c3d4e3c --- /dev/null +++ b/app/src/Encryption/EncryptionInstance.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Chevereto\Encryption\Interfaces\EncryptionInterface; + +final class EncryptionInstance +{ + private static ?EncryptionInterface $instance; + + public function __construct(EncryptionInterface $encryption) + { + self::$instance = $encryption; + } + + public static function get(): EncryptionInterface + { + if (!isset(self::$instance)) { + throw new LogicException( + message('No Encryption instance present') + ); + } + + return self::$instance; + } +} diff --git a/app/src/Encryption/Interfaces/DecodeInterface.php b/app/src/Encryption/Interfaces/DecodeInterface.php new file mode 100644 index 0000000..726d9e2 --- /dev/null +++ b/app/src/Encryption/Interfaces/DecodeInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption\Interfaces; + +use Stringable; + +/** + * Describes the component in charge of defining the encoded string stored in database. + */ +interface DecodeInterface extends Stringable +{ + public const ENCODING = '8bit'; + + public function nonce(): string; + + public function cipherText(): string; +} diff --git a/app/src/Encryption/Interfaces/EncodeInterface.php b/app/src/Encryption/Interfaces/EncodeInterface.php new file mode 100644 index 0000000..2493430 --- /dev/null +++ b/app/src/Encryption/Interfaces/EncodeInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption\Interfaces; + +/** + * Describes the component in charge of encoding nonce and cipher text for database storage. + */ +interface EncodeInterface +{ + public function encrypt(string $text): string; +} diff --git a/app/src/Encryption/Interfaces/EncryptionInterface.php b/app/src/Encryption/Interfaces/EncryptionInterface.php new file mode 100644 index 0000000..250da72 --- /dev/null +++ b/app/src/Encryption/Interfaces/EncryptionInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption\Interfaces; + +interface EncryptionInterface +{ + public const NONCE_LENGTH = 12; + + public const KEY_LENGTH = 32; + + public function withNonce(string $nonce): self; + + public function withRandomNonce(): self; + + public function nonce(): string; + + public function encrypt(string $plainText): string; + + public function decrypt(string $cipherText): string; +} diff --git a/app/src/Encryption/Interfaces/KeyInterface.php b/app/src/Encryption/Interfaces/KeyInterface.php new file mode 100644 index 0000000..753fe56 --- /dev/null +++ b/app/src/Encryption/Interfaces/KeyInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption\Interfaces; + +use Stringable; + +/** + * @method string __toString() The key as string + */ +interface KeyInterface extends Stringable +{ + public function base64(): string; +} diff --git a/app/src/Encryption/Key.php b/app/src/Encryption/Key.php new file mode 100644 index 0000000..013f6d5 --- /dev/null +++ b/app/src/Encryption/Key.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use Chevereto\Encryption\Interfaces\EncryptionInterface; +use Chevereto\Encryption\Interfaces\KeyInterface; +use function Safe\base64_decode; + +final class Key implements KeyInterface +{ + private string $key; + + public function __construct(private string $base64) + { + $this->key = base64_decode($base64); + if (strlen($this->key) !== EncryptionInterface::KEY_LENGTH) { + throw new InvalidArgumentException( + message('Requires a key size of %s') + ->withStrtr('%s', strval(EncryptionInterface::KEY_LENGTH)) + ); + } + } + + public function __toString() + { + return $this->key; + } + + public function base64(): string + { + return $this->base64; + } +} diff --git a/app/src/Encryption/NullEncryption.php b/app/src/Encryption/NullEncryption.php new file mode 100644 index 0000000..5a34bca --- /dev/null +++ b/app/src/Encryption/NullEncryption.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use Chevereto\Encryption\Interfaces\EncryptionInterface; + +final class NullEncryption implements EncryptionInterface +{ + public function withNonce(string $nonce): self + { + return clone $this; + } + + public function withRandomNonce(): self + { + return clone $this; + } + + public function nonce(): string + { + return ''; + } + + public function encrypt(string $plainText): string + { + return $plainText; + } + + public function decrypt(string $cipherText): string + { + return $cipherText; + } +} diff --git a/app/src/Encryption/functions.php b/app/src/Encryption/functions.php new file mode 100644 index 0000000..328016b --- /dev/null +++ b/app/src/Encryption/functions.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Encryption; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use Chevere\Throwable\Exceptions\LogicException; +use Chevereto\Encryption\Interfaces\EncryptionInterface; +use Chevereto\Encryption\Interfaces\KeyInterface; +use function Chevereto\Vars\env; +use phpseclib3\Crypt\Random; +use Throwable; + +function assertNonce(string $nonce): void +{ + if (!isValidNonce($nonce)) { + throw new InvalidArgumentException( + message('Requires a nonce size of %s') + ->withStrtr('%s', strval(EncryptionInterface::NONCE_LENGTH)) + ); + } +} + +function isValidNonce(string $nonce): bool +{ + return strlen($nonce) === EncryptionInterface::NONCE_LENGTH; +} + +function randomNonce(): string +{ + return Random::string(EncryptionInterface::NONCE_LENGTH); +} + +function randomKey(): KeyInterface +{ + return new Key( + base64_encode( + Random::string(EncryptionInterface::KEY_LENGTH) + ) + ); +} + +function encryption(): EncryptionInterface +{ + try { + return EncryptionInstance::get(); + } catch (Throwable) { + $base64 = env()['CHEVERETO_ENCRYPTION_KEY'] ?? ''; + new EncryptionInstance( + $base64 === '' + ? new NullEncryption() + : new Encryption( + new Key($base64) + ) + ); + + return EncryptionInstance::get(); + } +} + +function assertEncryption(): void +{ + if (!hasEncryption()) { + throw new LogicException( + message('Encryption is not enabled, set the %s environment variable to use encryption.') + ->withStrong('%s', 'CHEVERETO_ENCRYPTION_KEY') + ); + } +} + +function hasEncryption(): bool +{ + return !(encryption() instanceof NullEncryption); +} + +/** + * @return string A base64 encoded encrypted string with a nonce. + */ +function encrypt(string $plainText): string +{ + assertEncryption(); + $encode = new Encode(encryption()->withRandomNonce()); + + return $encode->encrypt($plainText); +} + +function decrypt(string $base64NonceCipherText): string +{ + assertEncryption(); + $decode = new Decode($base64NonceCipherText); + + return encryption() + ->withNonce($decode->nonce()) + ->decrypt($decode->cipherText()); +} + +function decryptValues(array $encryptedKeys, array $keyValues): array +{ + return mb_convert_encoding( + cipherValues($encryptedKeys, $keyValues, function (string $text) { + return decrypt($text); + }), + 'UTF-8' + ) ?: []; +} + +function encryptValues(array $encryptedKeys, array $keyValues): array +{ + return cipherValues($encryptedKeys, $keyValues, function (string $text) { + return encrypt($text); + }); +} + +function cipherValues(array $encryptedKeys, array $keyValues, callable $fn): array +{ + foreach ($encryptedKeys as $key) { + $value = $keyValues[$key] ?? ''; + if ($value !== '') { + try { + $cipher = $fn($value); + $keyValues[$key] = $cipher; + } catch (Throwable) { + } + } + } + + return $keyValues; +} diff --git a/app/src/File/functions.php b/app/src/File/functions.php new file mode 100644 index 0000000..3b3c127 --- /dev/null +++ b/app/src/File/functions.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace Chevereto\File; + +use Chevere\Throwable\Exceptions\LogicException; +use GuzzleHttp\Client; +use function Safe\file_put_contents; +use Throwable; + +function storeDownloadedUrl(string $url, string $filepath) +{ + $clientArgs = [ + 'base_uri' => $url, + 'timeout' => $_ENV['CHEVERETO_HTTP_TIMEOUT'] ?? 30, + ]; + // @codeCoverageIgnoreStart + if (isset($_ENV['CHEVERETO_HTTP_PROXY'])) { + $clientArgs['proxy'] = $_ENV['CHEVERETO_HTTP_PROXY']; + } + // @codeCoverageIgnoreEnd + try { + $httpClient = new Client($clientArgs); + $response = $httpClient->request('GET'); + } catch (Throwable $e) { + throw new LogicException(previous: $e); + } + file_put_contents($filepath, $response->getBody()); +} diff --git a/app/src/HashId/HashId.php b/app/src/HashId/HashId.php new file mode 100644 index 0000000..8de290d --- /dev/null +++ b/app/src/HashId/HashId.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\HashId; + +use function Chevere\Message\message; +use Chevere\String\AssertString; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use Throwable; + +/** + * Provides encoding/decoding for integer IDs. + */ +final class HashId +{ + private string $alphabet; + + private string $salt; + + private int $padding; + + private string $hash; + + private string $index; + + private array $table; + + private int $base; + + private string $baseString; + + public function __construct(string $salt) + { + $this->assertSalt($salt); + $this->salt = $salt; + $this->padding = 0; + $this->alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $index = str_split($this->alphabet, 1); + $this->hash = hash('sha256', $this->salt); + $this->table = []; + for ($n = 0; $n < strlen($this->alphabet); ++$n) { + $this->table[] = substr($this->hash, $n, 1); + } + array_multisort($this->table, SORT_DESC, $index); + $this->index = implode($index); + $this->base = strlen($this->index); + $this->baseString = (string) $this->base; + } + + public function withPadding(int $padding): self + { + $new = clone $this; + $this->assertPadding($padding); + $new->padding = $padding; + + return $new; + } + + public function decode(string $alpha): int + { + $out = 0; + $len = strlen($alpha) - 1; + for ($i = 0; $i <= $len; ++$i) { + $bcpow = bcpow($this->baseString, (string) ($len - $i)); + $out = $out + strpos($this->index, substr($alpha, $i, 1)) * $bcpow; + } + if ($this->padding > 0) { + $out = $out / $this->padding; + } + + return (int) $out; + } + + public function encode(int $id): string + { + if ($this->padding > 0) { + $id = $id * $this->padding; + } + $out = ''; + for ($i = floor(log((float) $id, $this->base)); $i >= 0; --$i) { + $bcpow = bcpow($this->baseString, (string) $i); + $start = floor($id / $bcpow) % $this->base; + $out = $out . substr($this->index, $start, 1); + $id = $id - ($start * $bcpow); + } + + return $out; + } + + private function assertSalt(string $salt): void + { + try { + (new AssertString($salt)) + ->notEmpty() + ->notCtypeSpace(); + } catch (Throwable) { + throw new InvalidArgumentException( + message('Invalid salt provided'), + ); + } + } + + private function assertPadding(int $padding): void + { + if ($padding < 0) { + throw new InvalidArgumentException( + message('Padding must be greater than zero'), + ); + } + } +} diff --git a/app/src/Image/ImageHashInstance.php b/app/src/Image/ImageHashInstance.php new file mode 100644 index 0000000..fbb305d --- /dev/null +++ b/app/src/Image/ImageHashInstance.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Image; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Jenssegers\ImageHash\ImageHash; + +/** + * @codeCoverageIgnore + */ +final class ImageHashInstance +{ + private static ?ImageHash $instance; + + public function __construct(ImageHash $imageHash) + { + self::$instance = $imageHash; + } + + public static function get(): ImageHash + { + if (!isset(self::$instance)) { + throw new LogicException( + message('No %instance% instance present') + ->withCode('%instance%', ImageHash::class) + ); + } + + return self::$instance; + } +} diff --git a/app/src/Image/ImageManagerInstance.php b/app/src/Image/ImageManagerInstance.php new file mode 100644 index 0000000..81a2f63 --- /dev/null +++ b/app/src/Image/ImageManagerInstance.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Image; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Intervention\Image\ImageManager; + +/** + * @codeCoverageIgnore + */ +final class ImageManagerInstance +{ + private static ?ImageManager $instance; + + public function __construct(ImageManager $imageManager) + { + self::$instance = $imageManager; + } + + public static function get(): ImageManager + { + if (!isset(self::$instance)) { + throw new LogicException( + message('No ImageManager instance present') + ); + } + + return self::$instance; + } +} diff --git a/app/src/Image/functions.php b/app/src/Image/functions.php new file mode 100644 index 0000000..973355f --- /dev/null +++ b/app/src/Image/functions.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Image; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\RuntimeException; +use Intervention\Image\ImageManager; +use Jenssegers\ImageHash\ImageHash; +use Jenssegers\ImageHash\Implementations\DifferenceHash; +use Throwable; + +function hasExtGd(): bool +{ + return extension_loaded('gd') && function_exists('gd_info'); +} + +function hasExtImagick(): bool +{ + return extension_loaded('imagick') && class_exists('Imagick'); +} + +function imageManager(): ImageManager +{ + try { + return ImageManagerInstance::get(); + } catch (Throwable) { + $driver = match (true) { + hasExtImagick() => 'Imagick', + hasExtGd() => 'Gd', + default => '', + }; + if ($driver === '') { + throw new RuntimeException( + message: message('No image driver available') + ); + } + $manager = new ImageManager(['driver' => $driver]); + new ImageManagerInstance($manager); + + return ImageManagerInstance::get(); + } +} + +function imageHash(): ImageHash +{ + try { + return ImageHashInstance::get(); + } catch (Throwable) { + new ImageHashInstance( + new ImageHash(new DifferenceHash(16)) + ); + + return ImageHashInstance::get(); + } +} diff --git a/app/src/Job/Job.php b/app/src/Job/Job.php new file mode 100644 index 0000000..4191aee --- /dev/null +++ b/app/src/Job/Job.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Job; + +use Chevere\Parameter\Interfaces\ArgumentsInterface; +use Chevereto\Controllers\WorkflowController; +use Ramsey\Uuid\Uuid; + +final class Job +{ + private string $id; + + private string $document; + + public function __construct( + private WorkflowController $controller, + private ArgumentsInterface $arguments + ) { + $this->id = Uuid::uuid4()->toString(); + } + + public function withDocument(string $document): self + { + $new = clone $this; + $new->document = $document; + + return $new; + } + + public function id(): string + { + return $this->id; + } + + public function document(): string + { + return $this->document ??= 'document'; + } +} diff --git a/app/src/Legacy/Classes/Album.php b/app/src/Legacy/Classes/Album.php new file mode 100644 index 0000000..17ad19f --- /dev/null +++ b/app/src/Legacy/Classes/Album.php @@ -0,0 +1,579 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\assertNotStopWords; +use function Chevereto\Legacy\encodeID; +use function Chevereto\Legacy\G\check_value; +use function Chevereto\Legacy\G\datetime; +use function Chevereto\Legacy\G\datetimegmt; +use function Chevereto\Legacy\G\datetimegmt_convert_tz; +use function Chevereto\Legacy\G\get_base_url; +use function Chevereto\Legacy\G\get_client_ip; +use function Chevereto\Legacy\G\get_public_url; +use function Chevereto\Legacy\G\nullify_string; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\seoUrlfy; +use function Chevereto\Legacy\G\truncate; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\send_mail; +use function Chevereto\Legacy\time_elapsed_string; +use function Chevereto\Vars\session; +use function Chevereto\Vars\sessionVar; +use Exception; + +class Album +{ + public const HASHED_NAMES = [ + 'password', + ]; + + public static function getSingle( + int $id, + bool $sumview = false, + bool $pretty = true, + array $requester = [] + ): array { + $tables = DB::getTables(); + $query = 'SELECT * FROM ' . $tables['albums'] . "\n"; + $joins = [ + 'LEFT JOIN ' . $tables['users'] . ' ON ' . $tables['albums'] . '.album_user_id = ' . $tables['users'] . '.user_id' + ]; + if ($requester !== []) { + if (version_compare(Settings::get('chevereto_version_installed'), '3.9.0', '>=')) { + $joins[] = 'LEFT JOIN ' . $tables['likes'] . ' ON ' . $tables['likes'] . '.like_content_type = "album" AND ' . $tables['albums'] . '.album_id = ' . $tables['likes'] . '.like_content_id AND ' . $tables['likes'] . '.like_user_id = ' . $requester['id']; + } + } + $query .= implode("\n", $joins) . "\n"; + $query .= 'WHERE album_id=:album_id;' . "\n"; + if ($sumview) { + $query .= 'UPDATE ' . $tables['albums'] . ' SET album_views = album_views + 1 WHERE album_id=:album_id'; + } + $db = DB::getInstance(); + $db->query($query); + $db->bind(':album_id', $id); + $album_db = $db->fetchSingle(); + if (!isset($album_db) + || !is_array($album_db) + || !$album_db) { + return []; + } + if ($sumview) { + $album_db['album_views'] ??= 0; + $album_db['album_views'] += 1; + Stat::track([ + 'action' => 'update', + 'table' => 'albums', + 'value' => '+1', + 'user_id' => $album_db['album_user_id'], + ]); + } + if ($requester !== []) { + $album_db['album_liked'] = (bool) $album_db['like_user_id']; + } + $return = $album_db; + + return $pretty + ? self::formatArray($return) + : $return; + } + + public static function getMultiple(array $ids, bool $pretty = false): array + { + if ($ids === []) { + throw new Exception('Empty ids provided', 600); + } + $tables = DB::getTables(); + $query = 'SELECT * FROM ' . $tables['albums'] . "\n"; + $joins = [ + 'LEFT JOIN ' . $tables['users'] . ' ON ' . $tables['albums'] . '.album_user_id = ' . $tables['users'] . '.user_id' + ]; + $query .= implode("\n", $joins) . "\n"; + $query .= 'WHERE album_id IN (' . implode(',', $ids) . ')' . "\n"; + $db = DB::getInstance(); + $db->query($query); + $db_rows = $db->fetchAll(); + if ($pretty) { + $return = []; + foreach ($db_rows as $k => $v) { + $return[$k] = self::formatArray($v); + } + + return $return; + } + + return $db_rows; + } + + public static function sumView(int $id, array $album = []): void + { + if (($album['id'] ?? 0) !== $id) { + $album = self::getSingle($id); + if ($album === []) { + throw new Exception(sprintf('Invalid album id %s', $id), 600); + } + } + $increment = '+1'; + DB::increment('albums', ['views' => $increment], ['id' => $id]); + Stat::track([ + 'action' => 'update', + 'table' => 'albums', + 'value' => $increment, + 'user_id' => $album['album_user_id'], + ]); + $addValue = session()['album_view_stock']; + $addValue[] = $id; + sessionVar()->put('album_view_stock', $id); + } + + public static function getUrl(string $id_encoded, string $title = null): string + { + $seo = seoUrlfy($title ?? ''); + $url = $seo == '' + ? $id_encoded + : ($seo . '.' . $id_encoded); + + return get_base_url( + (getSetting('root_route') === 'album' + ? '' + : getSetting('route_album') . '/') + . $url + ); + } + + public static function insert(array $values): int + { + Stat::assertMax('albums'); + if (!isset($values['user_id'])) { + $values['user_id'] = null; + } + if (!isset($values['description'])) { + $values['description'] = ''; + } + if (($values['privacy'] ?? null) == 'password') { + if (!check_value($values['password'])) { + throw new Exception('Missing album password', 100); + } + $values['password'] = password_hash($values['password'], PASSWORD_BCRYPT); + } + $flood = self::handleFlood(); + if ($flood !== []) { + throw new Exception( + _s( + 'Flooding detected. You can only upload %limit% %content% per %time%', + [ + '%content%' => _n('album', 'albums', $flood['limit']), + '%limit%' => $flood['limit'], + '%time%' => $flood['by'] + ] + ), + 130 + ); + } + if (!isset($values['name'])) { + $values['name'] = _s('Untitled') . ' ' . datetime(); + } + $privacyOpts = ['public', 'password', 'private_but_link']; + if (Login::isLoggedUser()) { + $privacyOpts[] = 'private'; + } + if (in_array($values['privacy'], $privacyOpts) == false) { + $values['privacy'] = 'public'; + } + nullify_string($values['description']); + if (empty($values['creation_ip'])) { + $values['creation_ip'] = get_client_ip(); + } + assertNotStopWords($values['name'] ?? '', $values['description'] ?? ''); + $album_array = [ + 'name' => $values['name'], + 'user_id' => $values['user_id'], + 'date' => datetime(), + 'date_gmt' => datetimegmt(), + 'privacy' => $values['privacy'], + 'password' => $values['privacy'] == 'password' ? $values['password'] : null, + 'description' => $values['description'], + 'creation_ip' => $values['creation_ip'], + 'parent_id' => $values['parent_id'] ?? null + ]; + $insert = DB::insert('albums', $album_array); + if (Login::isLoggedUser()) { + DB::increment('users', ['album_count' => '+1'], ['id' => $values['user_id']]); + } else { + $addValue = session()['guest_albums'] ?? []; + $addValue[] = $insert; + sessionVar()->put('guest_albums', $addValue); + } + Stat::track([ + 'action' => 'insert', + 'table' => 'albums', + 'value' => '+1', + 'date_gmt' => $album_array['date_gmt'] + ]); + + return $insert; + } + + public static function moveContents(int|string|array $from, ?int $to = null): bool + { + $ids = is_array($from) ? $from : [$from]; + $db = DB::getInstance(); + $db->query('UPDATE ' . DB::getTable('albums') . ' SET album_parent_id=:album_parent_id WHERE album_id IN (' . implode(',', $ids) . ')'); + $db->bind(':album_parent_id', $to); + + return $db->exec(); + } + + public static function addImage(int $album_id, int $id) + { + return self::addImages($album_id, [$id]); + } + + public static function addImages(?int $album_id, array $ids) + { + if ($ids === []) { + throw new Exception('Empty ids provided', 600); + } + $images = Image::getMultiple($ids, true); + $albums = []; + foreach ($images as $k => $v) { + if (isset($v['album']['id']) && $v['album']['id'] != $album_id) { + $album_k = $v['album']['id']; + if (!array_key_exists($album_k, $albums)) { + $albums[$album_k] = []; + } + $albums[$album_k][] = $v['id']; + } + } + $db = DB::getInstance(); + $db->query('UPDATE `' . DB::getTable('images') . '` SET `image_album_id`=:image_album_id WHERE `image_id` IN (' . implode(',', $ids) . ')'); + $db->bind(':image_album_id', $album_id); + $exec = $db->exec(); + if ($exec && $db->rowCount() > 0) { + if (!is_null($album_id)) { + self::updateImageCount($album_id, $db->rowCount()); + } + if ($albums !== []) { + $album_query = ''; + $album_query_tpl = 'UPDATE `' . DB::getTable('albums') . '` SET `album_image_count` = GREATEST(`album_image_count` - :counter, 0) WHERE `album_id` = :album_id;'; + foreach ($albums as $k => $v) { + $album_query .= strtr($album_query_tpl, [':counter' => count($v), ':album_id' => $k]); + } + $db = DB::getInstance(); + $db->query($album_query); + $db->exec(); + } + } + $db = DB::getInstance(); + $db->query('UPDATE `' . DB::getTable('albums') . '` SET `album_cover_id` = NULL WHERE `album_cover_id` IN(' . implode(',', $ids) . ');'); + $db->exec(); + $album = Album::getSingle((int) $album_id); + if (!isset($album['cover_id']) && is_int($album_id)) { + self::populateCover($album_id); + } + + return $exec; + } + + public static function update(int $id, array $values) + { + if (array_key_exists('description', $values)) { + nullify_string($values['description']); + } + assertNotStopWords($values['name'] ?? '', $values['description'] ?? ''); + if (($values['privacy'] ?? null) !== 'password') { + $values['password'] = null; + } + if (isset($values['password'])) { + $values['password'] = password_hash($values['password'], PASSWORD_BCRYPT); + } + + return DB::update('albums', $values, ['id' => $id]); + } + + public static function populateCover(int $id) + { + $db = DB::getInstance(); + $db->query('UPDATE `' . DB::getTable('albums') . '` + SET album_cover_id = (SELECT image_id FROM `' . DB::getTable('images') . '` WHERE image_album_id = album_id AND image_is_approved = 1 ORDER BY image_id DESC LIMIT 1) + WHERE album_id = :album_id;'); + $db->bind(':album_id', $id); + $db->exec(); + } + + public static function delete(int $id) + { + $images_deleted = 0; + $user_id = DB::get('albums', ['id' => $id])[0]['album_user_id'] ?? null; + $album = self::getSingle($id); + if ($album === []) { + return false; + } + foreach (DB::get('albums', ['parent_id' => $id]) as $child) { + $images_deleted += static::delete((int) $child['album_id']); + } + $delete = DB::delete('albums', ['id' => $id]); + if ($delete === 0) { + return false; + } + $db = DB::getInstance(); + $db->query('SELECT image_id FROM ' . DB::getTable('images') . ' WHERE image_album_id=:image_album_id'); + $db->bind(':image_album_id', $id); + $album_image_ids = $db->fetchAll(); + foreach ($album_image_ids as $k => $v) { + if (Image::delete((int) $v['image_id'], false) !== 0) { // We will update the user counts (image + album) at once + $images_deleted++; + } + } + if (isset($user_id)) { + $user_updated_counts = [ + 'album_count' => '-1', + 'image_count' => '-' . $images_deleted + ]; + DB::increment('users', $user_updated_counts, ['id' => $user_id]); + } + Stat::track([ + 'action' => 'delete', + 'table' => 'albums', + 'value' => '-1', + 'date_gmt' => $album['date_gmt'] + ]); + + return $images_deleted; + } + + public static function deleteMultiple(array $ids) + { + $affected = 0; + foreach ($ids as $id) { + $affected += self::delete((int) $id); + } + + return $affected; + } + + public static function updateImageCount(int $id, int $counter = 1, string $operator = '+') + { + $query = 'UPDATE `' . DB::getTable('albums') . '` SET `album_image_count` = '; + if (in_array($operator, ['+', '-'])) { + $query .= 'GREATEST(`album_image_count` ' . $operator . ' ' . $counter . ', 0)'; + } else { + $query .= $counter; + } + $query .= ' WHERE `album_id` = :album_id'; + $db = DB::getInstance(); + $db->query($query); + $db->bind(':album_id', $id); + + return $db->exec(); + } + + public static function fill(array &$album, array &$user = []) + { + $album['id_encoded'] = isset($album['id']) + ? encodeID((int) $album['id']) + : null; + if (!isset($album['name']) && isset($user['id'])) { + $album['name'] = _s("%s's images", $user['username']); + } + if (!isset($album['id'])) { + $album['url'] = $user !== [] ? User::getUrl($user['username']) : null; + $album['url_short'] = $album['url']; + } else { + $album['url'] = self::getUrl($album['id_encoded'], getSetting('seo_album_urls') ? $album['name'] : ''); + $album['url_short'] = self::getUrl($album['id_encoded'], ''); + } + $album['name_html'] = safe_html($album['name'] ?? ''); + if (!isset($album['privacy'])) { + $album['privacy'] = "public"; + } + switch ($album['privacy']) { + case 'private_but_link': + $album['privacy_notes'] = _s('Note: This content is private but anyone with the link will be able to see this.'); + + break; + case 'password': + $album['privacy_notes'] = _s('Note: This content is password protected. Remember to pass the content password to share.'); + + break; + case 'private': + $album['privacy_notes'] = _s('Note: This content is private. Change privacy to "public" to share.'); + + break; + default: + $album['privacy_notes'] = null; + + break; + } + $private_str = _s('Private'); + $privacy_to_label = [ + 'public' => _s('Public'), + 'private' => $private_str . '/' . _s('Me'), + 'private_but_link' => $private_str . '/' . _s('Link'), + 'password' => $private_str . '/' . _s('Password'), + ]; + $album['privacy_readable'] = $privacy_to_label[$album['privacy']]; + $album['name_with_privacy_readable'] = ($album['name'] ?? '') . ' (' . $album['privacy_readable'] . ')'; + $album['name_with_privacy_readable_html'] = safe_html($album['name_with_privacy_readable']); + $album['name_truncated'] = truncate($album['name'] ?? '', 28); + $album['name_truncated_html'] = safe_html($album['name_truncated']); + if (!empty($user)) { + User::fill($user); + } + $display_url = ''; + $display_width = ''; + $display_height = ''; + if (!empty($album['cover_id'])) { + $image = Image::getSingle((int) $album['cover_id']); + if ($image !== []) { + $image = DB::formatRow($image); + unset($image['album']); + Image::fill($image); + $display_url = $image['display_url']; + $display_width = $image['display_width']; + $display_height = $image['display_height']; + } + $album['cover_id_encoded'] = encodeID((int) $album['cover_id']); + } + if (!empty($album['parent_id'])) { + $album['parent_id_encoded'] = encodeID((int) $album['parent_id']); + } + $album['display_url'] = $display_url; + $album['display_width'] = $display_width; + $album['display_height'] = $display_height; + if (!isset($album['date_gmt'])) { + $album['date_gmt'] = $user['date_gmt'] ?? datetimegmt(); + } + $album['date_fixed_peer'] = Login::isLoggedUser() + ? datetimegmt_convert_tz($album['date_gmt'], Login::getUser()['timezone']) + : $album['date_gmt']; + } + + public static function formatArray(array $dbrow, bool $safe = false): array + { + $output = DB::formatRow($dbrow); + if (!isset($output['user'])) { + $output['user'] = []; + } + self::fill($output, $output['user']); + $output['views_label'] = _n('view', 'views', $output['views'] ?? 0); + $output['how_long_ago'] = time_elapsed_string($output['date_gmt'] ?? ''); + if (isset($output['images_slice'])) { + foreach ($output['images_slice'] as &$v) { + $v = Image::formatArray($v, $safe); + $v['flag'] = $v['nsfw'] ? 'unsafe' : 'safe'; + } + } + if ($safe) { + unset( + $output['id'], $output['privacy_extra'], $output['cover_id'], $output['parent_id'], + $output['user']['id'], + ); + } + + return $output; + } + + public static function checkPassword(string $hash, string $user_password): bool + { + return password_verify($user_password, $hash); + } + + public static function storeUserPasswordHash($album_id, $user_password): void + { + $addValue = session()['password']; + $addValue['album'][$album_id] = $user_password; + sessionVar()->put('password', $addValue); + } + + public static function checkSessionPassword($album = []): bool + { + $user_password_hash = session()['password']['album'][$album['id']] ?? null; + if (!isset($user_password_hash) || !password_verify($user_password_hash, $album['password'])) { + $removeValue = session()['password'] ?? null; + unset($removeValue['album'][$album['id']]); + sessionVar()->put('password', $removeValue); + + return false; + } + + return true; + } + + protected static function handleFlood(): array + { + if (!getSetting('flood_uploads_protection') || Login::isAdmin()) { + return []; + } + $flood_limit = [ + 'minute' => 20, + 'hour' => 200, + 'day' => 400, + 'week' => 2000, + 'month' => 10000 + ]; + + try { + $db = DB::getInstance(); + $flood_db = $db->queryFetchSingle( + "SELECT + COUNT(IF(album_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MINUTE), 1, NULL)) AS minute, + COUNT(IF(album_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 HOUR), 1, NULL)) AS hour, + COUNT(IF(album_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 DAY), 1, NULL)) AS day, + COUNT(IF(album_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 WEEK), 1, NULL)) AS week, + COUNT(IF(album_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MONTH), 1, NULL)) AS month + FROM " . DB::getTable('albums') . " WHERE album_creation_ip='" . get_client_ip() . "' AND album_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MONTH)" + ); + } catch (Exception $e) { + $flood_db = false; + } // Silence + if ($flood_db === false) { + return []; + } + $is_flood = false; + $flood_by = ''; + foreach (['minute', 'hour', 'day', 'week', 'month'] as $v) { + if ($flood_db[$v] >= $flood_limit[$v]) { + $flood_by = $v; + $is_flood = true; + + break; + } + } + if ($is_flood) { + if (!isset(session()['flood_albums_notify'], session()['flood_albums_notify'][$flood_by])) { + try { + $logged_user = Login::getUser(); + $message_report = '' . "\n"; + $message_report .= strtr('Flooding IP %ip', ['%ip' => get_client_ip()]) . '
'; + $message_report .= 'User ' . $logged_user['name'] . '
'; + $message_report .= '
'; + $message_report .= 'Albums per time period
'; + $message_report .= 'Minute: ' . $flood_db['minute'] . "
"; + $message_report .= 'Hour: ' . $flood_db['hour'] . "
"; + $message_report .= 'Week: ' . $flood_db['day'] . "
"; + $message_report .= 'Month: ' . $flood_db['week'] . "
"; + $message_report .= ''; + send_mail(getSetting('email_incoming_email'), 'Flood report user ID ' . $logged_user['id'], $message_report); + $addValue = session()['flood_albums_notify']; + $addValue[$flood_by] = true; + sessionVar()->put('flood_albums_notify', $addValue); + } catch (Exception $e) { + } // Silence + } + + return ['flood' => true, 'limit' => $flood_limit[$flood_by], 'count' => $flood_db[$flood_by], 'by' => $flood_by]; + } + + return []; + } +} diff --git a/app/src/Legacy/Classes/ApiKey.php b/app/src/Legacy/Classes/ApiKey.php new file mode 100644 index 0000000..49186b8 --- /dev/null +++ b/app/src/Legacy/Classes/ApiKey.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use function Chevere\String\randomString; +use Chevere\Throwable\Exceptions\OutOfRangeException; +use function Chevereto\Legacy\decodeID; +use function Chevereto\Legacy\encodeID; +use function Chevereto\Legacy\G\datetimegmt; +use Throwable; + +class ApiKey +{ + public static function generate(int $id): string + { + return 'chv_' . encodeID($id) . '_' . randomString(128); + } + + public static function hash(string $key): string + { + return password_hash($key, PASSWORD_BCRYPT); + } + + public static function verify(string $key): array + { + $explode = explode('_', $key); + $idEncoded = $explode[1] ?? null; + if ($idEncoded === null) { + return []; + } + $id = decodeID($idEncoded); + $get = self::get($id); + if ($get === []) { + return []; + } + $verify = password_verify($key, $get['hash']); + if ($verify === false) { + return []; + } + + return [ + 'id' => $get['id'], + 'user_id' => $get['user_id'], + 'date_gmt' => $get['date_gmt'], + ]; + } + + public static function insert(int $userId): string + { + $values = [ + 'user_id' => $userId, + 'date_gmt' => datetimegmt(), + 'hash' => '', + ]; + $insert = DB::insert('api_keys', $values); + $key = self::generate($insert); + $hash = self::hash($key); + DB::update('api_keys', ['hash' => $hash], ['id' => $insert]); + + return $key; + } + + public static function remove(int $id): void + { + DB::delete('api_keys', ['id' => $id]); + } + + public static function has(int $userId): bool + { + return self::getUserKey($userId) !== []; + } + + public static function getUserPublic(int $userId): array + { + $get = self::getUserKey($userId); + if ($get === []) { + throw new OutOfRangeException( + message('The user does not have an API key') + ); + } + + return [ + 'public' => 'chv_' . encodeID($get['id']) . '_***', + 'date_gmt' => $get['date_gmt'], + ]; + } + + public static function get(int $id): array + { + try { + $get = DB::get('api_keys', ['id' => $id], 'AND', ['field' => 'id', 'order' => 'desc'])[0] ?? null; + } catch (Throwable) { + return []; + } + + return DB::formatRow($get, 'api_key') ?? []; + } + + public static function getUserKey(int $userId): array + { + try { + $get = DB::get('api_keys', ['user_id' => $userId], 'AND', ['field' => 'id', 'order' => 'desc'])[0] ?? null; + } catch (Throwable) { + return []; + } + + return DB::formatRow($get, 'api_key') ?? []; + } +} diff --git a/app/src/Legacy/Classes/AssetStorage.php b/app/src/Legacy/Classes/AssetStorage.php new file mode 100644 index 0000000..0a5ae5a --- /dev/null +++ b/app/src/Legacy/Classes/AssetStorage.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use Chevereto\Traits\Instance\AssertNoInstanceTrait; + +final class AssetStorage +{ + use AssertNoInstanceTrait; + + protected static array $storage = []; + + protected static ?LocalStorage $localStorage = null; + + protected static bool $isLocalLegacy; + + public function __construct(array $storage) + { + $this->assertNoInstance(); + self::$storage = $storage; + self::$isLocalLegacy = StorageApis::getApiType((int) $storage['api_id']) == 'local' + && PATH_PUBLIC === $storage['bucket']; + if (($storage['api_id'] ?? false) === 8) { + self::$localStorage = new LocalStorage($storage); + } + } + + public static function getStorage(): array + { + return self::$storage; + } + + public static function isLocalLegacy(): bool + { + return self::$isLocalLegacy; + } + + public static function uploadFiles(array $targets, array $options): void + { + Storage::uploadFiles($targets, self::getStorage(), $options); + } + + public static function deleteFiles(array $targets): void + { + Storage::deleteFiles($targets, self::getStorage()); + } +} diff --git a/app/src/Legacy/Classes/Confirmation.php b/app/src/Legacy/Classes/Confirmation.php new file mode 100644 index 0000000..8582a62 --- /dev/null +++ b/app/src/Legacy/Classes/Confirmation.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\G\datetime; +use function Chevereto\Legacy\G\datetimegmt; + +class Confirmation +{ + public static function get( + array|string $values, + array $sort = [], + int $limit = 1 + ): array|bool { + return DB::get('confirmations', $values, 'AND', $sort, $limit); + } + + public static function insert(array $values): int + { + if (!isset($values['status'])) { + $values['status'] = 'active'; + } + $values['date'] = datetime(); + $values['date_gmt'] = datetimegmt(); + + return DB::insert('confirmations', $values); + } + + public static function update($id, $values): bool + { + return DB::update('confirmations', $values, ['id' => $id]) > 0; + } + + public static function delete($values, $clause = 'AND'): int + { + return DB::delete('confirmations', $values, $clause); + } +} diff --git a/app/src/Legacy/Classes/DB.php b/app/src/Legacy/Classes/DB.php new file mode 100644 index 0000000..ea1f82c --- /dev/null +++ b/app/src/Legacy/Classes/DB.php @@ -0,0 +1,231 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use Chevereto\Legacy\G\DB as GDB; +use function Chevereto\Legacy\G\starts_with; +use function Chevereto\Vars\env; +use PDO; + +class DB extends GDB +{ + public const TABLES = [ + 'albums', + 'api_keys', + 'categories', + 'confirmations', + 'deletions', + 'follows', + 'images_hash', + 'images', + 'import', + 'importing', + 'ip_bans', + 'likes', + 'logins', + 'login_connections', + 'login_cookies', + 'login_passwords', + 'login_providers', + 'notifications', + 'pages', + 'queue', + 'requests', + 'settings', + 'stats', + 'storage_apis', + 'storages', + 'two_factors', + 'users', + ]; + + public const PREFIX_TO_TABLE = [ + 'category' => 'categories', + 'deleted' => 'deletions', + 'image_hash' => 'images_hash', + ]; + + public const TABLES_TO_PREFIX = [ + 'categories' => 'category', + 'deletions' => 'deleted', + 'images_hash' => 'image_hash', + ]; + + public static function getTable(string $table): string + { + return env()['CHEVERETO_DB_TABLE_PREFIX'] . $table; + } + + public static function getTables(): array + { + $return = []; + foreach (self::TABLES as $table) { + $return[$table] = self::getTable($table); + } + + return $return; + } + + public static function get( + array|string $table, + array|string $values, + string $clause = 'AND', + array $sort = [], + int $limit = null, + int $fetch_style = PDO::FETCH_ASSOC + ): mixed { + $prefix = self::getFieldPrefix($table); + $values = self::getPrefixedValues($prefix, $values); + $sort = self::getPrefixedSort($prefix, $sort); + + return GDB::get($table, $values, $clause, $sort, $limit, $fetch_style); + } + + public static function update( + string $table, + array $values, + array $wheres, + string $clause = 'AND' + ): int { + $prefix = self::getFieldPrefix($table); + $values = self::getPrefixedValues($prefix, $values); + $wheres = self::getPrefixedValues($prefix, $wheres); + + return GDB::update($table, $values, $wheres, $clause); + } + + public static function insert($table, $values): int + { + $prefix = self::getFieldPrefix($table); + $values = self::getPrefixedValues($prefix, $values); + + return GDB::insert($table, $values); + } + + public static function increment( + string $table, + array $values, + array $wheres, + string $clause = 'AND' + ): int|false { + $prefix = self::getFieldPrefix($table); + $values = self::getPrefixedValues($prefix, $values); + $wheres = self::getPrefixedValues($prefix, $wheres); + + return GDB::increment($table, $values, $wheres, $clause); + } + + public static function delete( + string $table, + array $values, + string $clause = 'AND' + ): int { + $prefix = self::getFieldPrefix($table); + $values = self::getPrefixedValues($prefix, $values); + + return GDB::delete($table, $values, $clause); + } + + public static function formatRow(mixed $row, string $prefix = ''): mixed + { + if (!is_array($row)) { + return $row; + } + if ($prefix == '') { + $array = $row; + reset($array); + preg_match('/^([a-z0-9]+)_{1}/', (string) key($array), $match); + $prefix = $match[1] ?? ''; + } + $output = []; + foreach ($row as $k => $v) { + $k = (string) $k; + if (!starts_with($prefix, $k)) { + $new_key = preg_match('/^([a-z0-9]+)_/i', (string) $k, $new_key_match); + $new_key = $new_key_match[1] ?? null; + if ($new_key === null) { + continue; + } + $output[$new_key][str_replace($new_key . '_', '', $k)] = $v; + unset($output[$k]); + } else { + $output[str_replace($prefix . '_', '', $k)] = $v; + } + } + + return $output; + } + + public static function formatRows($get, string $prefix = '') + { + if (isset($get[0]) && is_array($get[0])) { + foreach ($get as $k => $v) { + self::formatRowValues($get[$k], $v, $prefix); + } + } elseif (!empty($get)) { + self::formatRowValues(values: $get, prefix: $prefix); + } + + return $get; + } + + public static function formatRowValues(array|string &$values, array|string $row = [], string $prefix = ''): void + { + $values = self::formatRow($row !== [] ? $row : $values, $prefix); + } + + public static function getTableFromFieldPrefix(string $prefix, bool $db_table_prefix = true): string + { + $table = array_key_exists($prefix, self::PREFIX_TO_TABLE) + ? self::PREFIX_TO_TABLE[$prefix] + : $prefix . 's'; + + return $db_table_prefix ? self::getTable($table) : $table; + } + + public static function getFieldPrefix(array|string $table): string + { + if (is_array($table)) { + $array = $table; + $table = $array['table']; + } + if (array_key_exists($table, self::TABLES_TO_PREFIX)) { + return self::TABLES_TO_PREFIX[$table]; + } else { + return rtrim($table, 's'); + } + } + + protected static function getPrefixedValues(string $prefix, array|string $values): array|string + { + if (!is_array($values)) { + return $values; + } + $values_prefix = []; + if (is_array($values)) { + foreach ($values as $k => $v) { + $values_prefix[$prefix . '_' . $k] = $v; + } + } + + return $values_prefix; + } + + protected static function getPrefixedSort(string $prefix, array $sort): array + { + if ($sort !== [] && !empty($sort['field'])) { + $sort['field'] = $prefix . '_' . $sort['field']; + } + + return $sort; + } +} diff --git a/app/src/Legacy/Classes/Image.php b/app/src/Legacy/Classes/Image.php new file mode 100644 index 0000000..f8846d4 --- /dev/null +++ b/app/src/Legacy/Classes/Image.php @@ -0,0 +1,1534 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use function Chevere\String\randomString; +use Chevere\Throwable\Exceptions\LogicException; +use function Chevereto\Legacy\assertNotStopWords; +use function Chevereto\Legacy\decodeID; +use function Chevereto\Legacy\encodeID; +use function Chevereto\Legacy\G\add_ending_slash; +use function Chevereto\Legacy\G\array_filter_array; +use function Chevereto\Legacy\G\array_utf8encode; +use function Chevereto\Legacy\G\datetime; +use function Chevereto\Legacy\G\datetime_add; +use function Chevereto\Legacy\G\datetime_diff; +use function Chevereto\Legacy\G\datetime_modify; +use function Chevereto\Legacy\G\datetime_sub; +use function Chevereto\Legacy\G\datetimegmt; +use function Chevereto\Legacy\G\datetimegmt_convert_tz; +use function Chevereto\Legacy\G\format_bytes; +use function Chevereto\Legacy\G\get_basename_without_extension; +use function Chevereto\Legacy\G\get_bytes; +use function Chevereto\Legacy\G\get_client_ip; +use function Chevereto\Legacy\G\get_filename; +use function Chevereto\Legacy\G\get_image_fileinfo as GGet_image_fileinfo; +use function Chevereto\Legacy\G\get_public_url; +use function Chevereto\Legacy\G\is_animated_image; +use function Chevereto\Legacy\G\name_unique_file; +use function Chevereto\Legacy\G\nullify_string; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\seoUrlfy; +use function Chevereto\Legacy\G\starts_with; +use function Chevereto\Legacy\G\truncate; +use function Chevereto\Legacy\G\unlinkIfExists; +use function Chevereto\Legacy\G\url_to_relative; +use function Chevereto\Legacy\get_image_fileinfo; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\system_notification_email; +use function Chevereto\Legacy\time_elapsed_string; +use function Chevereto\Vars\env; +use function Chevereto\Vars\session; +use function Chevereto\Vars\sessionVar; +use DateTimeZone; +use Exception; +use Intervention\Image\ImageManagerStatic; +use PHPExif\Exif; +use function Safe\password_hash; +use Throwable; + +class Image +{ + public static array $table_chv_image = [ + 'name', + 'extension', + 'album_id', + 'size', + 'width', + 'height', + 'date', + 'date_gmt', + 'nsfw', + 'user_id', + 'uploader_ip', + 'storage_mode', + 'storage_id', + 'md5', + 'source_md5', + 'original_filename', + 'original_exifdata', + 'category_id', + 'description', + 'chain', + 'thumb_size', + 'medium_size', + 'title', + 'expiration_date_gmt', + 'likes', + 'is_animated', + 'is_approved', + 'is_360', + ]; + + protected static array $expirations = [ + ['minute', 5, 300], + ['minute', 15, 900], + ['minute', 30, 1800], + ['hour', 1, 3600], + ['hour', 3, 10800], + ['hour', 6, 21600], + ['hour', 12, 43200], + ['day', 1, 86400], + ['day', 2, 172800], + ['day', 3, 259200], + ['day', 4, 345600], + ['day', 5, 432000], + ['day', 6, 518400], + ['week', 1, 604800], + ['week', 2, 1209600], + ['week', 3, 1814400], + ['month', 1, 2630000], + ['month', 2, 5260000], + ['month', 3, 7890000], + ['month', 4, 10520000], + ['month', 5, 13150000], + ['month', 6, 15780000], + ['year', 1, 31536000], + ]; + + public static array $chain_sizes = ['original', 'image', 'medium', 'thumb']; + + public static function getSingle( + int $id, + bool $sumView = false, + bool $pretty = false, + array $requester = [] + ): array { + $tables = DB::getTables(); + $query = 'SELECT * FROM ' . $tables['images'] . "\n"; + $joins = [ + 'LEFT JOIN ' . $tables['storages'] . ' ON ' . $tables['images'] . '.image_storage_id = ' . $tables['storages'] . '.storage_id', + 'LEFT JOIN ' . $tables['storage_apis'] . ' ON ' . $tables['storages'] . '.storage_api_id = ' . $tables['storage_apis'] . '.storage_api_id', + 'LEFT JOIN ' . $tables['users'] . ' ON ' . $tables['images'] . '.image_user_id = ' . $tables['users'] . '.user_id', + 'LEFT JOIN ' . $tables['albums'] . ' ON ' . $tables['images'] . '.image_album_id = ' . $tables['albums'] . '.album_id' + ]; + if ($requester !== []) { + if (version_compare(Settings::get('chevereto_version_installed'), '3.7.0', '>=')) { + $joins[] = 'LEFT JOIN ' . $tables['likes'] . ' ON ' . $tables['likes'] . '.like_content_type = "image" AND ' . $tables['images'] . '.image_id = ' . $tables['likes'] . '.like_content_id AND ' . $tables['likes'] . '.like_user_id = ' . $requester['id']; + } + } + $query .= implode("\n", $joins) . "\n"; + $query .= 'WHERE image_id=:image_id;' . "\n"; + if ($sumView) { + $query .= 'UPDATE ' . $tables['images'] . ' SET image_views = image_views + 1 WHERE image_id=:image_id'; + } + $db = DB::getInstance(); + $db->query($query); + $db->bind(':image_id', $id); + $image_db = $db->fetchSingle(); + if (empty($image_db)) { + return []; + } + if ($sumView) { + $image_db['image_views'] += 1; + Stat::track([ + 'action' => 'update', + 'table' => 'images', + 'value' => '+1', + 'user_id' => $image_db['image_user_id'], + ]); + } + if ($requester !== []) { + $image_db['image_liked'] = (bool) $image_db['like_user_id']; + } + $return = $image_db; + $return = $pretty ? self::formatArray($return) : $return; + if (!isset($return['file_resource'])) { + $return['file_resource'] = self::getSrcTargetSingle($image_db); + } + + return $return; + } + + public static function getMultiple(array $ids, bool $pretty = false): array + { + if ($ids === []) { + throw new Exception('Null $ids provided in Image::get_multiple', 600); + } + $tables = DB::getTables(); + $query = 'SELECT * FROM ' . $tables['images'] . "\n"; + $joins = [ + 'LEFT JOIN ' . $tables['users'] . ' ON ' . $tables['images'] . '.image_user_id = ' . $tables['users'] . '.user_id', + 'LEFT JOIN ' . $tables['albums'] . ' ON ' . $tables['images'] . '.image_album_id = ' . $tables['albums'] . '.album_id' + ]; + $query .= implode("\n", $joins) . "\n"; + $query .= 'WHERE image_id IN (' . implode(',', $ids) . ')' . "\n"; + $db = DB::getInstance(); + $db->query($query); + $images_db = $db->fetchAll(); + if (!empty($images_db)) { + foreach ($images_db as $k => $v) { + $images_db[$k] = array_merge($v, self::getSrcTargetSingle($v, true)); // todo + } + } + if ($pretty) { + $return = []; + foreach ($images_db as $k => $v) { + $return[] = self::formatArray($v); + } + + return $return; + } + + return $images_db; + } + + public static function getAlbumSlice( + int $image_id, + int $album_id = null, + int $padding = 2 + ): array { + $tables = DB::getTables(); + if (!isset($album_id)) { + $db = DB::getInstance(); + $db->query('SELECT image_album_id FROM ' . $tables['images'] . ' WHERE image_id=:image_id'); + $db->bind(':image_id', $image_id); + $image_album_db = $db->fetchSingle(); + $album_id = $image_album_db['image_album_id']; + if (!isset($album_id)) { + return []; + } + } + if (!is_numeric($padding)) { + $padding = 2; + } + $prevListing = new Listing(); + $prevListing->setType('images'); + $prevListing->setLimit(($padding * 2) + 1); + $prevListing->setSortType('date'); + $prevListing->setSortOrder('desc'); + $prevListing->setRequester(Login::getUser()); + $prevListing->setWhere('WHERE image_album_id=' . $album_id . ' AND image_id <= ' . $image_id); + $prevListing->exec(); + $nextListing = new Listing(); + $nextListing->setType('images'); + $nextListing->setLimit($padding * 2); + $nextListing->setSortType('date'); + $nextListing->setSortOrder('asc'); + $nextListing->setRequester(Login::getUser()); + $nextListing->setWhere('WHERE image_album_id=' . $album_id . ' AND image_id > ' . $image_id); + $nextListing->exec(); + if (is_array($prevListing->output)) { + $prevListing->output = array_reverse($prevListing->output); + } + $list = array_merge($prevListing->output, $nextListing->output); + $album_offset = [ + 'top' => $prevListing->count - 1, + 'bottom' => $nextListing->count + ]; + $album_chop_count = count($list); + $album_iteration_times = $album_chop_count - ($padding * 2 + 1); + if ($album_chop_count > ($padding * 2 + 1)) { + if ($album_offset['top'] > $padding && $album_offset['bottom'] > $padding) { + for ($i = 0; $i < $album_offset['top'] - $padding; $i++) { + unset($list[$i]); + } + for ($i = 1; $i <= $album_offset['bottom'] - $padding; $i++) { + unset($list[$album_chop_count - $i]); + } + } elseif ($album_offset['top'] <= $padding) { + for ($i = 0; $i < $album_iteration_times; $i++) { + unset($list[$album_chop_count - 1 - $i]); + } + } elseif ($album_offset['bottom'] <= $padding) { + for ($i = 0; $i < $album_iteration_times; $i++) { + unset($list[$i]); + } + } + $list = array_values($list); + } + $images = []; + foreach ($list as $v) { + $format = self::formatArray($v); + $images[$format['id']] = $format; + } + if (is_array($prevListing->output) && $prevListing->count > 1) { + $prevLastKey = $prevListing->count - 2; + $prevLastId = $prevListing->output[$prevLastKey]['image_id']; + $slice['prev'] = $images[$prevLastId]; + } + if ($nextListing->output) { + $slice['next'] = $images[$nextListing->output[0]['image_id']]; + } + $slice['images'] = $images; + + return $slice; + } + + public static function getSrcTargetSingle(array $filearray, bool $prefix = true): array + { + $prefix = $prefix ? 'image_' : null; + $folder = CHV_PATH_IMAGES; + $pretty = !isset($filearray['image_id']); + $mode = $filearray[$prefix . 'storage_mode']; + $chain_mask = str_split( + (string) str_pad( + decbin((int) ($filearray[$pretty ? 'chain' : 'image_chain'])), + 4, + '0', + STR_PAD_LEFT + ) + ); + $chain_to_sufix = [ + 'image' => '.', + 'thumb' => '.th.', + 'medium' => '.md.' + ]; + if ($pretty) { + $type = isset($filearray['storage']['id']) ? 'url' : 'path'; + } else { + $type = isset($filearray['storage_id']) ? 'url' : 'path'; + } + if ($type == 'url') { // URL resource folder + $folder = add_ending_slash($pretty ? $filearray['storage']['url'] : $filearray['storage_url']); + } + switch ($mode) { + case 'datefolder': + $datetime = $filearray[$prefix . 'date']; + $datefolder = preg_replace('/(.*)(\s.*)/', '$1', str_replace('-', '/', $datetime)); + $folder .= add_ending_slash($datefolder); // Y/m/d/ + + break; + case 'old': + $folder .= 'old/'; + + break; + case 'direct': + // use direct $folder + break; + case 'path': + $folder = add_ending_slash($filearray['path']); + + break; + } + $targets = [ + 'type' => $type, + 'chain' => [ + 'image' => null, + 'thumb' => null, + 'medium' => null + ] + ]; + foreach (array_keys($targets['chain']) as $k) { + $targets['chain'][$k] = $folder . $filearray[$prefix . 'name'] . $chain_to_sufix[$k] . $filearray[$prefix . 'extension']; + } + if ($type == 'path') { + foreach ($targets['chain'] as $k => $v) { + if (!is_readable($v)) { + unset($targets['chain'][$k]); + } + } + } else { + foreach ($chain_mask as $k => $v) { + if (!(bool) $v) { + unset($targets['chain'][self::$chain_sizes[$k]]); + } + } + } + + return $targets; + } + + public static function getUrlViewer(string $id_encoded, string $title = ''): string + { + $seo = seoUrlfy($title); + $url = $seo == '' + ? $id_encoded + : ($seo . '.' . $id_encoded); + + return get_public_url( + (getSetting('root_route') === 'image' + ? '' + : getSetting('route_image') . '/') + . $url, + ); + } + + public static function getDeleteUrl(string $idEncoded, string $password): string + { + return self::getUrlViewer($idEncoded) . '/delete/' . $password; + } + + public static function getAvailableExpirations(): array + { + $string = _s('After %n %t'); + $translate = [ + 'minute' => _n('minute', 'minutes', 1), + 'hour' => _n('hour', 'hours', 1), + 'day' => _n('day', 'days', 1), + 'week' => _n('week', 'weeks', 1), + 'month' => _n('month', 'months', 1), + 'year' => _n('year', 'years', 1), + ]; + $return = [ + null => _s("Don't autodelete"), + ]; + $table = self::$expirations; + foreach ($table as $expire) { + $unit = $expire[0]; + $interval_spec = self::getPastTimeSpec($unit, $expire[1]); + $return[$interval_spec] = strtr($string, ['%n' => $expire[1], '%t' => _n($unit, $unit . 's', $expire[1])]); + } + + return $return; + } + + protected static function getPastTimeSpec(string $unit, string $value): string + { + return 'P' . + (in_array($unit, ['second', 'minute', 'hour']) + ? 'T' + : '') + . $value . strtoupper($unit[0]); + } + + public static function getExpirationFromSeconds(int $seconds): string + { + if ($seconds <= 0) { + return ''; + } + $previous = array_values(self::$expirations)[0]; + foreach (self::$expirations as $expires) { + if ($seconds < $expires[2]) { + return self::getPastTimeSpec(...$previous); + } + $previous = [strval($expires[0]), strval($expires[1])]; + } + $maxLimit = self::$expirations[count(self::$expirations) - 1]; + + return self::getPastTimeSpec(...$maxLimit); + } + + public static function watermarkFromDb(): void + { + $file = PATH_PUBLIC_CONTENT_IMAGES_SYSTEM . getSetting('watermark_image'); + $assetsDb = DB::get('assets', ['key' => 'watermark_image'], 'AND', [], 1); + if ($assetsDb === false) { + return; + } + if (file_exists($file) + && md5_file($file) != $assetsDb['asset_md5'] + && !starts_with('default/', getSetting('watermark_image')) + ) { + unlinkIfExists($file); + } + if (!file_exists($file)) { + $fh = fopen($file, 'w'); + $st = !$fh || fwrite($fh, $assetsDb['asset_blob']) === false ? false : true; + fclose($fh); + if (!$st) { + throw new LogicException( + message(_s("Can't open %s for writing", $file)), + 600 + ); + } + } + } + + public static function watermark(string $image_path, array $options = []): bool + { + $options = array_merge([ + 'ratio' => getSetting('watermark_percentage') / 100, + 'position' => explode(' ', getSetting('watermark_position')), + 'file' => PATH_PUBLIC_CONTENT_IMAGES_SYSTEM . getSetting('watermark_image') + ], $options); + self::watermarkFromDb(); + if (!is_readable($options['file'])) { + throw new Exception("Can't read watermark file at " . $options['file'], 600); + } + $image = ImageManagerStatic::make($image_path); + $options['ratio'] = min(1, (is_numeric($options['ratio']) ? max(0.01, $options['ratio']) : 0.01)); + if (!in_array($options['position'][0], ['left', 'center', 'right'])) { + $options['position'][0] = 'right'; + } + if (!in_array($options['position'][1], ['top', 'center', 'bottom'])) { + $options['position'][0] = 'bottom'; + } + $watermarkPos = []; + if ($options['position'][1] !== 'center') { + $watermarkPos[] = $options['position'][1]; + } + if ($options['position'][0] !== 'center') { + $watermarkPos[] = $options['position'][0]; + } + $watermark = ImageManagerStatic::make($options['file']); + $watermark_area = $image->getWidth() * $image->getHeight() * $options['ratio']; + $watermark_image_ratio = $watermark->getWidth() / $watermark->getHeight(); + $watermark_new_height = round(sqrt($watermark_area / $watermark_image_ratio), 0); + if ($watermark_new_height > $image->getHeight()) { + $watermark_new_height = $image->getHeight(); + } + if (getSetting('watermark_margin') && $options['position'][1] !== 'center' && $watermark_new_height + getSetting('watermark_margin') > $image->getHeight()) { + $watermark_new_height -= $watermark_new_height + 2 * getSetting('watermark_margin') - $image->getHeight(); + } + $watermark_new_width = round($watermark_image_ratio * $watermark_new_height, 0); + if ($watermark_new_width > $image->getWidth()) { + $watermark_new_width = $image->getWidth(); + } + if (getSetting('watermark_margin') && $options['position'][0] !== 'center' && $watermark_new_width + getSetting('watermark_margin') > $image->getWidth()) { + $watermark_new_width -= $watermark_new_width + 2 * getSetting('watermark_margin') - $image->getWidth(); + $watermark_new_height = $watermark_new_width / $watermark_image_ratio; + } + if ($watermark_new_width !== $watermark->getWidth()) { + $watermark->resize($watermark_new_width, null, function ($constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + }); + } + $watermark->opacity(getSetting('watermark_opacity')); + $image + ->insert( + $watermark, + $watermarkPos === [] + ? 'center' + : implode('-', $watermarkPos), + getSetting('watermark_margin'), + getSetting('watermark_margin') + ) + ->save(); + + return true; + } + + public static function upload( + array|string $source, + string $destination, + string|null $filename = null, + array $options = [], + int|null $storage_id = null, + bool $guestSessionHandle = true + ): array { + $default_options = Upload::getDefaultOptions(); + $options = array_merge($default_options, $options); + if (!is_null($filename) && !$options['filenaming']) { + $options['filenaming'] = 'original'; + } + $upload = new Upload(); + $upload->setSource($source); + $upload->setDestination($destination); + $upload->setOptions($options); + if (!is_null($storage_id)) { + $upload->setStorageId($storage_id); + } + if (!is_null($filename)) { + $upload->setFilename($filename); + } + if ($guestSessionHandle == false) { + $upload->detectFlood = false; + } + $upload->exec(); + $return = [ + 'uploaded' => $upload->uploaded(), + 'source' => $upload->source(), + ]; + if (property_exists($upload, 'moderation') && $upload->moderation() !== null) { + $return['moderation'] = $upload->moderation(); + } + + return $return; + } + + // Mostly for people uploading two times the same image to test or just bug you + // $mixed => $_FILES or md5 string + public static function isDuplicatedUpload(array|string $source, string $time_frame = 'P1D'): bool + { + if (is_array($source) && isset($source['tmp_name'])) { + $filename = $source['tmp_name']; + if (stream_resolve_include_path($filename) == false) { + throw new Exception("Concurrency: $filename is gone", 666); + } + $md5_file = md5_file($filename); + } else { + $filename = $source; + $md5_file = $filename; + } + if ($md5_file === false) { + throw new Exception('Unable to process md5_file', 600); + } + $db = DB::getInstance(); + $db->query('SELECT * FROM ' . DB::getTable('images') . ' WHERE (image_md5=:md5 OR image_source_md5=:md5) AND image_uploader_ip=:ip AND image_date_gmt > :date_gmt'); + $db->bind(':md5', $md5_file); + $db->bind(':ip', get_client_ip()); + $db->bind(':date_gmt', datetime_sub(datetimegmt(), $time_frame)); + $db->exec(); + + return (bool) $db->fetchColumn(); + } + + public static function uploadToWebsite( + array|string $source, + array $user = [], + array $params = [], + bool $guestSessionHandle = true, + string|null $ip = null + ): array { + $params['use_file_date'] = $params['use_file_date'] ?? false; + nullify_string($params['album_id']); + $datefolder = ''; + + try { + if ($user !== [] + && getSetting('upload_max_filesize_mb_bak') !== null + && getSetting('upload_max_filesize_mb') == getSetting('upload_max_filesize_mb_guest') + ) { + Settings::setValue('upload_max_filesize_mb', getSetting('upload_max_filesize_mb_bak')); + } + $do_dupe_check = !getSetting('enable_duplicate_uploads') && !($user['is_admin'] ?? false); + if ($do_dupe_check && self::isDuplicatedUpload($source)) { + throw new Exception(_s('Duplicated upload'), 101); + } + $storage_id = null; + $get_active_storages = env()['CHEVERETO_ENABLE_EXTERNAL_STORAGE'] + ? Storage::get(['is_active' => 1]) + : []; + if ($get_active_storages !== []) { + if (count($get_active_storages) > 1) { + $last_used_storage = (int) getSetting('last_used_storage'); + } else { + $last_used_storage = null; + $storage_id = (int) $get_active_storages[0]['id']; + } + $last_used_storage_is_active = false; + $active_storages = []; + foreach ($get_active_storages as $i => $get_active_storage) { + $pointer = $get_active_storage['id']; + $active_storages[$pointer] = $get_active_storage; + if ($pointer === $last_used_storage) { + $last_used_storage_is_active = true; + } + } + if (!$last_used_storage_is_active) { + $storage_id = $get_active_storages[0]['id']; + } else { + unset($active_storages[$last_used_storage]); + $storage_keys = array_keys($active_storages); + shuffle($storage_keys); + $storage_id = $storage_keys[0]; + } + $storage = $active_storages[$storage_id]; + } + $storage_mode = getSetting('upload_storage_mode'); + $upload_path = ''; + switch ($storage_mode) { + case 'direct': + $upload_path = CHV_PATH_IMAGES; + + break; + case 'datefolder': + $stockDate = datetime(); + $stockDateGmt = datetimegmt(); + if (is_array($source) && $params['use_file_date'] && $source['type'] === 'image/jpeg') { + try { + $exifSource = \exif_read_data($source['tmp_name']); + } catch (Throwable $e) { + } + if (isset($exifSource['DateTime'])) { + $stockDateGmt = date_create_from_format("Y:m:d H:i:s", $exifSource['DateTime'], new DateTimeZone('UTC')); + $stockDateGmt = $stockDateGmt->format('Y-m-d H:i:s'); + $stockDate = datetimegmt_convert_tz($stockDateGmt, getSetting('default_timezone')); + } + } + $datefolder_stock = [ + 'date' => $stockDate, + 'date_gmt' => $stockDateGmt, + ]; + $datefolder = date('Y/m/d/', strtotime($datefolder_stock['date'])); + $upload_path = CHV_PATH_IMAGES . $datefolder; + + break; + } + $filenaming = getSetting('upload_filenaming'); + if ($filenaming !== 'id' && in_array($params['privacy'] ?? '', ['password', 'private', 'private_but_link'])) { + $filenaming = 'random'; + } + $upload_options = [ + 'max_size' => get_bytes(getSetting('upload_max_filesize_mb') . ' MB'), + 'exif' => (getSetting('upload_image_exif_user_setting') && $user !== []) + ? $user['image_keep_exif'] + : getSetting('upload_image_exif'), + ]; + if ($filenaming == 'id') { + try { + $dummy = [ + 'name' => '', + 'extension' => '', + 'size' => 0, + 'width' => 0, + 'height' => 0, + 'date' => '0000-01-01 00:00:00', + 'date_gmt' => '0000-01-01 00:00:00', + 'nsfw' => 0, + 'uploader_ip' => '', + 'md5' => '', + 'original_filename' => '', + 'chain' => 0, + 'thumb_size' => 0, + 'medium_size' => 0, + ]; + $dummy_insert = DB::insert('images', $dummy); + DB::delete('images', ['id' => $dummy_insert]); + $target_id = $dummy_insert; + } catch (Throwable $e) { + $filenaming = 'original'; + } + } + $upload_options['filenaming'] = $filenaming; + $upload_options['allowed_formats'] = self::getEnabledImageFormats(); + $image_upload = self::upload( + $source, + $upload_path, + ($filenaming == 'id' && isset($target_id)) + ? encodeID((int) $target_id) + : null, + $upload_options, + $storage_id, + $guestSessionHandle + ); + $chain_mask = [0, 1, 0, 1]; // original image medium thumb + if ($do_dupe_check && self::isDuplicatedUpload($image_upload['uploaded']['fileinfo']['md5'])) { + throw new Exception(_s('Duplicated upload'), 102); + } + $image_ratio = $image_upload['uploaded']['fileinfo']['width'] / $image_upload['uploaded']['fileinfo']['height']; + $must_resize = false; + $image_max_size_cfg = [ + 'width' => Settings::get('upload_max_image_width') ?: $image_upload['uploaded']['fileinfo']['width'], + 'height' => Settings::get('upload_max_image_height') ?: $image_upload['uploaded']['fileinfo']['height'], + ]; + if ($image_max_size_cfg['width'] < $image_upload['uploaded']['fileinfo']['width'] || $image_max_size_cfg['height'] < $image_upload['uploaded']['fileinfo']['height']) { + $image_max = $image_max_size_cfg; + $image_max['width'] = (int) round($image_max_size_cfg['height'] * $image_ratio); + $image_max['height'] = (int) round($image_max_size_cfg['width'] / $image_ratio); + if ($image_max['height'] > $image_max_size_cfg['height']) { + $image_max['height'] = $image_max_size_cfg['height']; + $image_max['width'] = (int) round($image_max['height'] * $image_ratio); + } + if ($image_max['width'] > $image_max_size_cfg['width']) { + $image_max['width'] = $image_max_size_cfg['width']; + $image_max['height'] = (int) round($image_max['width'] / $image_ratio); + } + if ($image_max !== ['width' => $image_upload['uploaded']['fileinfo']['width'], 'height' => $image_max_size_cfg['height']]) { // loose just in case.. + $must_resize = true; + $params['width'] = $image_max['width']; + $params['height'] = $image_max['height']; + } + } + foreach (['width', 'height'] as $k) { + if (!isset($params[$k]) || !is_numeric($params[$k])) { + continue; + } + if ($params[$k] != $image_upload['uploaded']['fileinfo'][$k]) { + $must_resize = true; + } + } + $is_360 = (bool) $image_upload['uploaded']['fileinfo']['is_360']; + if (is_animated_image($image_upload['uploaded']['file'])) { + $must_resize = false; + } + if ($must_resize) { + $source_md5 = $image_upload['uploaded']['fileinfo']['md5']; + if ($do_dupe_check && self::isDuplicatedUpload($source_md5)) { + throw new Exception(_s('Duplicated upload'), 103); + } + $image_ratio = $image_upload['uploaded']['fileinfo']['ratio']; + if (isset($params['width'], $params['height'])) { + $image_resize_options = [ + 'width' => $params['width'], + 'height' => $params['height'], + ]; + } else { + $image_resize_options = ['width' => $params['width']]; + } + $image_upload['uploaded'] = self::resize( + $image_upload['uploaded']['file'], + dirname($image_upload['uploaded']['file']), + null, + $image_resize_options + ); + $image_upload['uploaded']['fileinfo']['is_360'] = $is_360; + } + $image_thumb_options = [ + 'forced' => true, + 'over_resize' => true, + 'fitted' => true, + 'width' => getSetting('upload_thumb_width'), + 'height' => getSetting('upload_thumb_height'), + ]; + $medium_size = getSetting('upload_medium_size'); + $medium_fixed_dimension = getSetting('upload_medium_fixed_dimension'); + $is_animated_image = is_animated_image($image_upload['uploaded']['file']); + $image_thumb = self::resize( + source: $image_upload['uploaded']['file'], + destination: dirname($image_upload['uploaded']['file']), + filename: $image_upload['uploaded']['name'] . '.th', + options: $image_thumb_options + ); + $original_md5 = $image_upload['source']['fileinfo']['md5']; + $watermark_enable = getSetting('watermark_enable'); + if ($watermark_enable) { + $watermark_user = $user !== [] + ? ($user['is_admin'] ? 'admin' : 'user') + : 'guest'; + $watermark_enable = getSetting('watermark_enable_' . $watermark_user); + } + $watermark_gif = (bool) getSetting('watermark_enable_file_gif'); + $apply_watermark = $watermark_enable; + if ($is_animated_image || $image_upload['uploaded']['fileinfo']['is_360']) { + $apply_watermark = false; + } + if ($apply_watermark) { + foreach (['width', 'height'] as $k) { + $min_value = getSetting('watermark_target_min_' . $k); + if ($min_value == 0) { // Skip on zero + continue; + } + $apply_watermark = $image_upload['uploaded']['fileinfo'][$k] >= $min_value; + } + if ($apply_watermark && $image_upload['uploaded']['fileinfo']['extension'] == 'gif' && !$watermark_gif) { + $apply_watermark = false; + } + } + if ($apply_watermark && self::watermark($image_upload['uploaded']['file'])) { + $image_upload['uploaded']['fileinfo'] = GGet_image_fileinfo($image_upload['uploaded']['file']); // Remake the fileinfo array, new full array file info (todo: faster!) + $image_upload['uploaded']['fileinfo']['md5'] = $original_md5; // Preserve original MD5 for watermarked images + } + if ($image_upload['uploaded']['fileinfo'][$medium_fixed_dimension] > $medium_size || $is_animated_image) { + $image_medium_options = []; + $image_medium_options[$medium_fixed_dimension] = $medium_size; + if ($is_animated_image) { + $image_medium_options['forced'] = true; + $image_medium_options[$medium_fixed_dimension] = min($image_medium_options[$medium_fixed_dimension], $image_upload['uploaded']['fileinfo'][$medium_fixed_dimension]); + } + $image_medium = self::resize( + $image_upload['uploaded']['file'], + dirname($image_upload['uploaded']['file']), + $image_upload['uploaded']['name'] . '.md', + $image_medium_options + ); + $chain_mask[2] = 1; + } + $chain_value = bindec((string) implode('', $chain_mask)); + $disk_space_needed = $image_upload['uploaded']['fileinfo']['size']; + if (isset($image_thumb['fileinfo']['size'])) { + $disk_space_needed += $image_thumb['fileinfo']['size']; + } + if (isset($image_medium['fileinfo']['size'])) { + $disk_space_needed += $image_medium['fileinfo']['size']; + } + $switch_to_local = false; + if (isset($storage_id) + && !empty($storage['capacity']) + && $disk_space_needed > ($storage['capacity'] - $storage['space_used']) + ) { + if (isset($active_storages) && $active_storages !== []) { + $capable_storages = []; + foreach ($active_storages as $k => $v) { + if ($v['id'] == $storage_id || $disk_space_needed > ($v['capacity'] - $v['space_used'])) { + continue; + } + $capable_storages[] = $v['id']; + } + if (count($capable_storages) == 0) { + $switch_to_local = true; + } else { + $storage_id = (int) $capable_storages[0]; + $storage = $active_storages[$storage_id]; + } + } else { + $switch_to_local = true; + } + if ($switch_to_local) { + $storage_id = 0; + $downstream = $image_upload['uploaded']['file']; + $fixed_filename = $image_upload['uploaded']['filename']; + $uploaded_file = name_unique_file( + $upload_path, + $fixed_filename, + $upload_options['filenaming'] + ); + + try { + $renamed_uploaded = rename($downstream, $uploaded_file); + } catch (Throwable $e) { + $renamed_uploaded = file_exists($uploaded_file); + } + if (!$renamed_uploaded) { + throw new Exception("Can't re-allocate image to local storage", 600); + } + $image_upload['uploaded'] = [ + 'file' => $uploaded_file, + 'filename' => get_filename($uploaded_file), + 'name' => get_basename_without_extension($uploaded_file), + 'fileinfo' => GGet_image_fileinfo($uploaded_file) + ]; + $chain_props = [ + 'thumb' => ['suffix' => 'th'], + 'medium' => ['suffix' => 'md'] + ]; + if (!($image_medium ?? false)) { + unset($chain_props['medium']); + } + foreach ($chain_props as $k => $v) { + $chain_file = add_ending_slash(dirname($image_upload['uploaded']['file'])) . $image_upload['uploaded']['name'] . '.' . $v['suffix'] . '.' . ${"image_$k"}['fileinfo']['extension']; + + try { + $renamed_chain = rename(${"image_$k"}['file'], $chain_file); + } catch (Throwable $e) { + $renamed_chain = file_exists($chain_file); + } + if (!$renamed_chain) { + throw new Exception("Can't re-allocate image " . $k . " to local storage", 601); + } + ${"image_$k"} = [ + 'file' => $chain_file, + 'filename' => get_filename($chain_file), + 'name' => get_basename_without_extension($chain_file), + 'fileinfo' => GGet_image_fileinfo($chain_file) + ]; + } + } + } + $image_insert_values = [ + 'storage_mode' => $storage_mode, + 'storage_id' => $storage_id ?? null, + 'user_id' => $user['id'] ?? null, + 'album_id' => $params['album_id'] ?? null, + 'nsfw' => $params['nsfw'] ?? null, + 'category_id' => $params['category_id'] ?? null, + 'title' => $params['title'] ?? null, + 'description' => $params['description'] ?? null, + 'chain' => $chain_value, + 'thumb_size' => $image_thumb['fileinfo']['size'] ?? 0, + 'medium_size' => $image_medium['fileinfo']['size'] ?? 0, + 'is_animated' => $is_animated_image, + 'source_md5' => $source_md5 ?? null, + 'is_360' => $is_360 + ]; + if (isset($datefolder_stock)) { + foreach ($datefolder_stock as $k => $v) { + $image_insert_values[$k] = $v; + } + } + if (getSetting('enable_expirable_uploads')) { + if ($user === [] && getSetting('auto_delete_guest_uploads') !== null) { + $params['expiration'] = getSetting('auto_delete_guest_uploads'); + } + if (!isset($params['expiration']) && isset($user['image_expiration'])) { + $params['expiration'] = $user['image_expiration']; + } + + try { + if (!empty($params['expiration']) && array_key_exists($params['expiration'], self::getAvailableExpirations())) { + $params['expiration_date_gmt'] = datetime_add(datetimegmt(), strtoupper($params['expiration'])); + } + if (!empty($params['expiration_date_gmt'])) { + $expirable_diff = datetime_diff(datetimegmt(), $params['expiration_date_gmt'], 'm'); + $image_insert_values['expiration_date_gmt'] = $expirable_diff < 5 ? datetime_modify(datetimegmt(), '+5 minutes') : $params['expiration_date_gmt']; + } + } catch (Exception $e) { + } // Silence + } + if (isset($storage_id, $storage)) { + $toStorage = []; + foreach (self::$chain_sizes as $k => $v) { + if (!(bool) $chain_mask[$k]) { + continue; + } + switch ($v) { + case 'image': + $prop = $image_upload['uploaded']; + + break; + default: + $prop = ${"image_$v"}; + + break; + } + $toStorage[$v] = [ + 'file' => $prop['file'], + 'filename' => $prop['filename'], + 'mime' => $prop['fileinfo']['mime'], + ]; + } + Storage::uploadFiles($toStorage, $storage, [ + 'keyprefix' => $storage_mode == 'datefolder' + ? $datefolder + : null + ]); + } + /** @var ?Exif */ + $exifRead = $image_upload['source']['image_exif']; + if ($exifRead instanceof Exif) { + if (!array_key_exists('title', $params)) { + $title_from_exif = null; + if ($exifRead->getTitle() !== false) { + $title_from_exif = trim($exifRead->getTitle()); + } + if ($title_from_exif !== null) { + $title_from_exif = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $title_from_exif); + $image_title = $title_from_exif; + } else { + $title_from_filename = preg_replace('/[-_\s]+/', ' ', trim($image_upload['source']['name'])); + $image_title = $title_from_filename; + } + $image_insert_values['title'] = $image_title; + } + if (!array_key_exists('description', $params)) { + $description_from_exif = null; + if ($exifRead->getDescription() !== false) { + $description_from_exif = trim($exifRead->getDescription()); + } + if ($description_from_exif !== null) { + $description_from_exif = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $description_from_exif); + $image_insert_values['description'] = $description_from_exif; + } + } + } + if ($filenaming == 'id' && isset($target_id)) { // Insert as a reserved ID + $image_insert_values['id'] = $target_id; + } + $image_insert_values['title'] = mb_substr($image_insert_values['title'] ?? '', 0, 100, 'UTF-8'); + if ($user !== [] && isset($image_insert_values['album_id'])) { + $album = Album::getSingle((int) $image_insert_values['album_id']); + if (($album['user']['id'] ?? 0) != $user['id']) { + unset($image_insert_values['album_id'], $album); + } + } + if (isset($ip)) { + $image_insert_values['uploader_ip'] = $ip; + } + $uploaded_id = self::insert($image_upload, $user, $image_insert_values); + $deletePassword = randomString(48); + $deleteHash = password_hash($deletePassword, PASSWORD_BCRYPT); + DB::insert('images_hash', ['image_id' => $uploaded_id, 'hash' => $deleteHash]); + if (isset($toStorage)) { + foreach ($toStorage as $k => $v) { + unlinkIfExists($v['file']); // Remove the source image + } + } + $privacyTargets = ['private', 'private_but_link']; + if (in_array($params['privacy'] ?? '', $privacyTargets) + && (!in_array($album['privacy'] ?? '', $privacyTargets)) + ) { + $upload_timestamp = $params['timestamp'] ?? time(); + $session_handle = 'upload_' . $upload_timestamp; + $album = isset(session()[$session_handle]) + ? Album::getSingle(decodeID(session()[$session_handle])) + : null; + if (!empty($album) || !in_array($album['privacy'], $privacyTargets)) { + $inserted_album = Album::insert([ + 'name' => _s('Private upload') . ' ' . datetime('Y-m-d'), + 'user_id' => $user['id'], + 'privacy' => $params['privacy'] + ]); + sessionVar()->put($session_handle, $inserted_album); + $image_insert_values['album_id'] = $inserted_album; + } else { + $image_insert_values['album_id'] = $album['id']; + } + } + if (isset($image_insert_values['album_id'])) { + Album::addImage($image_insert_values['album_id'], $uploaded_id); + } + if ($user !== []) { + DB::increment('users', ['image_count' => '+1'], ['id' => $user['id']]); + } elseif ($guestSessionHandle == true) { + $addValue = session()['guest_images'] ?? []; + $addValue[] = $uploaded_id; + sessionVar()->put('guest_images', $addValue); + } + if ($switch_to_local) { + $image_viewer = self::getUrlViewer(encodeID((int) $uploaded_id)); + system_notification_email(['subject' => 'Upload switched to local storage', 'message' => strtr('System has switched to local storage due to not enough disk capacity (%c) in the external storage server(s). The image %s has been allocated to local storage.', ['%c' => $disk_space_needed . ' B', '%s' => '' . $image_viewer . ''])]); + } + + return [$uploaded_id, $deletePassword]; + } catch (Exception $e) { + if (isset($image_upload['uploaded'], $image_upload['uploaded']['file'])) { + unlinkIfExists($image_upload['uploaded']['file']); + } + if (isset($image_medium['file'])) { + unlinkIfExists($image_medium['file']); + } + if (isset($image_thumb['file'])) { + unlinkIfExists($image_thumb['file']); + } + + throw $e; + } + } + + public static function getEnabledImageFormats(): array + { + $formats = explode(',', Settings::get('upload_enabled_image_formats')); + if (in_array('jpg', $formats)) { + $formats[] = 'jpeg'; + } + + return $formats; + } + + public static function resize( + string $source, + ?string $destination, + ?string $filename = null, + array $options = [] + ): array { + $resize = new ImageResize($source); + $resize->setDestination($destination ?? ''); + if ($filename) { + $resize->setFilename($filename); + } + $resize->setOptions($options); + if (isset($options['width'])) { + $resize->setWidth((int) $options['width']); + } + if (isset($options['height'])) { + $resize->setHeight((int) $options['height']); + } + if (isset($options['forced']) && $options['forced'] === true) { + $resize->setOption('forced', true); + } + $resize->exec(); + + return $resize->resized(); + } + + protected static function insert(array $image_upload, array $user = [], array $values = []): int + { + Stat::assertMax('images'); + $table_chv_image = self::$table_chv_image; + foreach ($table_chv_image as $k => $v) { + $table_chv_image[$k] = 'image_' . $v; + } + if (empty($values['uploader_ip'])) { + $values['uploader_ip'] = get_client_ip(); + } + /** @var ?Exif $exifRead */ + $exifRead = $image_upload['source']['image_exif']; + $exifRaw = null; + if ($exifRead instanceof Exif) { + $exifRaw = $exifRead->getRawData(); + unset($exifRaw['MakerNote']); + } + $original_exifdata = $exifRaw !== null + ? json_encode(array_utf8encode($exifRaw)) + : null; + $values['nsfw'] = in_array(strval($values['nsfw']), ['0', '1']) ? $values['nsfw'] : 0; + if (Settings::get('moderatecontent') + && $values['nsfw'] == 0 + && Settings::get('moderatecontent_flag_nsfw') + && is_object($image_upload['moderation']) + ) { + switch ($image_upload['moderation']->rating_letter) { + case 'a': + $values['nsfw'] = '1'; + + break; + case 't': + if (Settings::get('moderatecontent_flag_nsfw') == 't') { + $values['nsfw'] = 1; + } + + break; + } + } + $is360 = false; + if (isset($image_upload['uploaded']['fileinfo']['is_360'])) { + $is360 = (bool) $image_upload['uploaded']['fileinfo']['is_360']; + } + $populate_values = [ + 'uploader_ip' => $values['uploader_ip'], + 'md5' => $image_upload['uploaded']['fileinfo']['md5'], + 'original_filename' => $image_upload['source']['filename'], + 'original_exifdata' => $original_exifdata, + 'is_360' => $is360, + ]; + if (!isset($values['date'])) { + $populate_values = array_merge($populate_values, [ + 'date' => datetime(), + 'date_gmt' => datetimegmt(), + ]); + } + $values = array_merge($image_upload['uploaded']['fileinfo'], $populate_values, $values); + assertNotStopWords( + $values['name'] ?? '', + $values['original_filename'] ?? '', + $values['title'] ?? '', + $values['description'] ?? '' + ); + foreach (['title', 'description', 'category_id', 'album_id'] as $v) { + nullify_string($values[$v]); + } + foreach (array_keys($values) as $k) { + if (!in_array('image_' . $k, $table_chv_image) && $k !== 'id') { + unset($values[$k]); + } + } + $values['is_approved'] = 1; + switch (Settings::get('moderate_uploads')) { + case 'all': + $values['is_approved'] = (int) (($user['is_admin'] ?? 0) || ($user['is_manager'] ?? 0)); + + break; + case 'guest': + $values['is_approved'] = (int) isset($values['user_id']); + + break; + } + if (Settings::get('moderatecontent_auto_approve') + && isset($image_upload['moderation']) + ) { + $values['is_approved'] = 1; + } + $insert = DB::insert('images', $values); + $disk_space_used = $values['size'] + $values['thumb_size'] + $values['medium_size']; + Stat::track([ + 'action' => 'insert', + 'table' => 'images', + 'value' => '+1', + 'date_gmt' => $values['date_gmt'], + 'disk_sum' => $disk_space_used, + ]); + if (!is_null($values['album_id']) && $insert) { + Album::updateImageCount((int) $values['album_id'], 1); + } + + return $insert; + } + + public static function update(int $id, array $values): int + { + $values = array_filter_array($values, self::$table_chv_image, 'exclusion'); + assertNotStopWords($values['title'] ?? '', $values['description'] ?? ''); + foreach (['title', 'description', 'category_id', 'album_id'] as $v) { + if (!array_key_exists($v, $values)) { + continue; + } + nullify_string($values[$v]); + } + if (isset($values['album_id'])) { + $image_db = self::getSingle($id); + $old_album = $image_db['image_album_id']; + $update = DB::update('images', $values, ['id' => $id]); + if ($update && $old_album !== $values['album_id']) { + if (!is_null($old_album)) { // Update the old album + Album::updateImageCount((int) $old_album, 1, '-'); + } + Album::updateImageCount((int) $values['album_id'], 1); + } + + return $update; + } else { + return DB::update('images', $values, ['id' => $id]); + } + } + + public static function delete(int $id, bool $update_user = true): int + { + $image = self::getSingle(id: $id, pretty: true); + $disk_space_used = $image['size'] + ($image['thumb']['size'] ?? 0) + ($image['medium']['size'] ?? 0); + if ($image['file_resource']['type'] == 'path') { + foreach ($image['file_resource']['chain'] as $file_delete) { + if (file_exists($file_delete) && !unlinkIfExists($file_delete)) { + throw new Exception("Can't delete file", 600); + } + } + } else { + $targets = []; + foreach ($image['file_resource']['chain'] as $k => $v) { + $targets[$k] = [ + 'key' => preg_replace('#' . add_ending_slash($image['storage']['url']) . '#', '', $v), + 'size' => $image[$k]['size'], + ]; + } + Storage::deleteFiles($targets, $image['storage']); + } + if ($update_user && isset($image['user']['id'])) { + DB::increment('users', ['image_count' => '-1'], ['id' => $image['user']['id']]); + } + if (isset($image['album']['id']) && $image['album']['id'] > 0) { + Album::updateImageCount((int) $image['album']['id'], 1, '-'); + } + if (isset($image['album']['cover_id']) && $image['album']['cover_id'] === $image['id']) { + Album::populateCover((int) $image['album']['id']); + } + Stat::track([ + 'action' => 'delete', + 'table' => 'images', + 'value' => '-1', + 'date_gmt' => $image['date_gmt'], + 'disk_sum' => $disk_space_used, + 'likes' => $image['likes'], + ]); + DB::queryExecute('UPDATE ' . DB::getTable('users') . ' INNER JOIN ' . DB::getTable('likes') . ' ON user_id = like_user_id AND like_content_type = "image" AND like_content_id = ' . $image['id'] . ' SET user_liked = GREATEST(cast(user_liked AS SIGNED) - 1, 0);'); + if (isset($image['user']['id'])) { + $autoliked = DB::get('likes', ['user_id' => $image['user']['id'], 'content_type' => 'image', 'content_id' => $image['id']])[0] ?? []; + $likes_counter = (int) $image['likes']; // This is stored as "bigint" but PDO MySQL get it as string. Fuck my code, fuck PHP. + if ($autoliked !== []) { + $likes_counter -= 1; + } + if ($likes_counter > 0) { + $likes_counter = 0 - $likes_counter; + } + if ($likes_counter !== 0) { + DB::increment('users', ['likes' => $likes_counter], ['id' => $image['user']['id']]); + } + Notification::delete([ + 'table' => 'images', + 'image_id' => $image['id'], + 'user_id' => $image['user']['id'], + ]); + } + DB::delete('likes', ['content_type' => 'image', 'content_id' => $image['id']]); + DB::insert('deletions', [ + 'date_gmt' => datetimegmt(), + 'content_id' => $image['id'], + 'content_date_gmt' => $image['date_gmt'], + 'content_user_id' => $image['user']['id'] ?? null, + 'content_ip' => $image['uploader_ip'], + 'content_views' => $image['views'], + 'content_md5' => $image['md5'], + 'content_likes' => $image['likes'], + 'content_original_filename' => $image['original_filename'], + ]); + + $result = DB::delete('images', ['id' => $id]); + DB::delete('images_hash', ['image_id' => $id]); + + return $result; + } + + public static function deleteMultiple(array $ids): int + { + $affected = 0; + foreach ($ids as $id) { + if (self::delete((int) $id) !== 0) { + $affected += 1; + } + } + + return $affected; + } + + public static function deleteExpired(int $limit = 50): void + { + if (!$limit || !is_numeric($limit)) { + $limit = 50; + } + $db = DB::getInstance(); + $db->query('SELECT image_id FROM ' . DB::getTable('images') . ' WHERE image_expiration_date_gmt IS NOT NULL AND image_expiration_date_gmt < :datetimegmt ORDER BY image_expiration_date_gmt DESC LIMIT ' . $limit . ';'); // Just 50 files per request to prevent CPU meltdown or something like that + $db->bind(':datetimegmt', datetimegmt()); + $expired_db = $db->fetchAll(); + if ($expired_db) { + $expired = []; + foreach ($expired_db as $k => $v) { + $expired[] = $v['image_id']; + } + self::deleteMultiple($expired); + } + } + + public static function verifyPassword(int $id, string $password): bool + { + $get = DB::get('images_hash', ['image_id' => $id])[0] ?? []; + if ($get === []) { + return false; + } + $get = DB::formatRow($get, 'image_hash'); + + return password_verify($password, $get['hash']); + } + + public static function fill(array &$image): void + { + $image['id_encoded'] = encodeID((int) $image['id']); + $targets = self::getSrcTargetSingle($image, false); + $medium_size = getSetting('upload_medium_size'); + $medium_fixed_dimension = getSetting('upload_medium_fixed_dimension'); + if ($targets['type'] == 'path') { + if ($image['size'] == 0) { + $get_image_fileinfo = GGet_image_fileinfo($targets['chain']['image']); + $update_missing_values = [ + 'width' => $get_image_fileinfo['width'], + 'height' => $get_image_fileinfo['height'], + 'size' => $get_image_fileinfo['size'], + ]; + foreach (['thumb', 'medium'] as $k) { + if (!array_key_exists($k, $targets['chain'])) { + continue; + } + if ($image[$k . '_size'] == 0) { + $update_missing_values[$k . '_size'] = GGet_image_fileinfo($targets['chain'][$k])['size']; + } + } + self::update($image['id'], $update_missing_values); + $image = array_merge($image, $update_missing_values); + } + $is_animated = isset($targets['chain']['image']) && is_animated_image($targets['chain']['image']); + if (count($targets['chain']) > 0 && !isset($targets['chain']['thumb'])) { + try { + $targets['chain']['thumb'] = self::resize( + $targets['chain']['image'], + pathinfo($targets['chain']['image'], PATHINFO_DIRNAME), + $image['name'] . '.th', + [ + 'width' => getSetting('upload_thumb_width'), + 'height' => getSetting('upload_thumb_height'), + 'forced' => $image['extension'] == 'gif' && $is_animated + ] + )['file']; + } catch (Exception $e) { + } + } + if ($image[$medium_fixed_dimension] > $medium_size + && count($targets['chain']) > 0 + && !isset($targets['chain']['medium']) + ) { + try { + $targets['chain']['medium'] = self::resize( + $targets['chain']['image'], + pathinfo($targets['chain']['image'], PATHINFO_DIRNAME), + $image['name'] . '.md', + [ + $medium_fixed_dimension => $medium_size, + 'forced' => $image['extension'] == 'gif' && $is_animated + ] + )['file']; + } catch (Throwable $e) { + } + } + if (count($targets['chain']) > 0) { + $original_md5 = $image['md5']; + $image = array_merge($image, get_image_fileinfo($targets['chain']['image'])); + $image['md5'] = $original_md5; + } + if ($is_animated && !$image['is_animated']) { + self::update($image['id'], ['is_animated' => 1]); + $image['is_animated'] = 1; + } + } else { + $image_fileinfo = [ + 'ratio' => $image['width'] / $image['height'], + 'size' => (int) $image['size'], + 'size_formatted' => format_bytes($image['size']) + ]; + $image = array_merge($image, get_image_fileinfo($targets['chain']['image']), $image_fileinfo); + } + $image['file_resource'] = $targets; + $image['url_viewer'] = self::getUrlViewer( + $image['id_encoded'], + getSetting('seo_image_urls') + ? ($image['title'] ?? '') + : '' + ); + $image['path_viewer'] = url_to_relative($image['url_viewer']); + $image['url_short'] = self::getUrlViewer($image['id_encoded']); + foreach ($targets['chain'] as $k => $v) { + if ($targets['type'] == 'path') { + $image[$k] = file_exists($v) ? get_image_fileinfo($v) : null; + } else { + $image[$k] = get_image_fileinfo($v); + } + $image[$k]['size'] = $image[($k == 'image' ? '' : $k . '_') . 'size']; + } + $image['size_formatted'] = format_bytes($image['size']); + $display_url = $image['url'] ?? ''; + $display_width = $image['width']; + $display_height = $image['height']; + if (!empty($image['medium'])) { + $display_url = $image['medium']['url']; + $image_ratio = $image['width'] / $image['height']; + switch ($medium_fixed_dimension) { + case 'width': + $display_width = $medium_size; + $display_height = (int) round($medium_size / $image_ratio); + + break; + case 'height': + $display_height = $medium_size; + $display_width = (int) round($medium_size * $image_ratio); + + break; + } + // if (!$image["is_animated"]) { + // // $display_url = $image['url'] ?? ''; + // } + } elseif ($image['size'] > get_bytes('200 KB')) { + $display_url = $image['thumb']['url'] ?? ''; + $display_width = getSetting('upload_thumb_width'); + $display_height = getSetting('upload_thumb_height'); + } + + $image['display_url'] = $display_url; + $image['display_width'] = $display_width; + $image['display_height'] = $display_height; + $image['views_label'] = _n('view', 'views', $image['views']); + $image['likes_label'] = _n('like', 'likes', $image['likes']); + $image['how_long_ago'] = time_elapsed_string($image['date_gmt']); + $image['date_fixed_peer'] = Login::isLoggedUser() + ? datetimegmt_convert_tz($image['date_gmt'], Login::getUser()['timezone']) + : $image['date_gmt']; + $image['title_truncated'] = truncate($image['title'] ?? '', 28); + $image['title_truncated_html'] = safe_html($image['title_truncated']); + $image['is_use_loader'] = getSetting('image_load_max_filesize_mb') !== '' ? ($image['size'] > get_bytes(getSetting('image_load_max_filesize_mb') . 'MB')) : false; + } + + public static function formatArray(array $dbRow, bool $safe = false): array + { + $output = DB::formatRow($dbRow); + if (isset($output['user']['id'])) { + User::fill($output['user']); + } else { + unset($output['user']); + } + if (isset($output['album']['id']) || isset($output['user']['id'])) { + $output['user'] = $output['user'] ?? []; + Album::fill($output['album'], $output['user']); + } else { + unset($output['album']); + } + self::fill($output); + if ($safe) { + unset( + $output['storage'], $output['id'], $output['path'], $output['uploader_ip'], + $output['album']['id'], $output['album']['privacy_extra'], $output['album']['user_id'], + $output['album']['password'], $output['album']['cover_id'], $output['album']['parent_id'], + $output['user']['id'], $output['user']['email'], + $output['file_resource'], + $output['file']['resource']['chain'], + ); + } + + return $output; + } +} diff --git a/app/src/Legacy/Classes/ImageConvert.php b/app/src/Legacy/Classes/ImageConvert.php new file mode 100644 index 0000000..4cc7f8d --- /dev/null +++ b/app/src/Legacy/Classes/ImageConvert.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use Intervention\Image\ImageManagerStatic; + +class ImageConvert +{ + private string $out; + + public function __construct( + array|string $source, + string $to, + string $destination, + int $quality = 90 + ) { + if (!in_array($to, ['jpg', 'jpeg', 'gif', 'png'])) { + return; + } + $image = ImageManagerStatic::make($source); + $image->encode($to, $quality)->save($destination); + $this->out = $destination; + } + + public function out(): string + { + return $this->out; + } +} diff --git a/app/src/Legacy/Classes/ImageResize.php b/app/src/Legacy/Classes/ImageResize.php new file mode 100644 index 0000000..bf17209 --- /dev/null +++ b/app/src/Legacy/Classes/ImageResize.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\RangeException; +use function Chevereto\Legacy\G\add_ending_slash; +use function Chevereto\Legacy\G\get_basename_without_extension; +use function Chevereto\Legacy\G\get_filename; +use function Chevereto\Legacy\G\get_image_fileinfo; +use function Chevereto\Legacy\G\is_writable; +use function Chevereto\Legacy\missing_values_to_exception; +use Exception; +use Intervention\Image\Image; +use Intervention\Image\ImageManagerStatic; + +class ImageResize +{ + private string $file_extension; + + // filename => name.ext + // file => /full/path/to/name.ext + // name => name + + private string $resized_file; + + private array $resized; + + private Image $image; + + private string $source; + + private string $destination = ''; + + private string $filename; + + private array $options = []; + + private int $width = 0; + + private int $height = 0; + + private array $source_image_fileinfo; + + public function __construct(string $source) + { + clearstatcache(true, $source); + $this->source = $source; + if (!file_exists($this->source)) { + throw new Exception("Source file doesn't exists", 600); + } + $this->image = ImageManagerStatic::make($source); + } + + public function setDestination(string $destination): void + { + $this->destination = $destination; + } + + public function setFilename(string $name): void + { + $this->filename = $name; + } + + public function setOptions(array $options): void + { + $this->options = $options; + } + + public function setOption(string $key, mixed $value): void + { + $this->options[$key] = $value; + } + + public function setWidth(int $width): void + { + $this->width = $width; + } + + public function setHeight(int $height): void + { + $this->height = $height; + } + + public function width(): int + { + return $this->width; + } + + public function resized(): array + { + return $this->resized; + } + + public function exec(): void + { + $this->validateInput(); // Exception 1xx + $source_filename = get_basename_without_extension($this->source); + $this->file_extension = $this->source_image_fileinfo['extension']; + if (!isset($this->filename)) { + $this->filename = $source_filename; + } + $this->destination = add_ending_slash($this->destination); + $this->resized_file = $this->destination . $this->filename . '.' . $this->file_extension; + $this->resize(); + $this->resized = [ + 'file' => $this->resized_file, + 'filename' => get_filename($this->resized_file), + 'name' => get_basename_without_extension($this->resized_file), + 'fileinfo' => get_image_fileinfo($this->resized_file), + ]; + } + + protected function validateInput(): void + { + $check_missing = ['source']; + missing_values_to_exception($this, Exception::class, $check_missing, 600); + if (!$this->width && !$this->height) { + throw new Exception('Missing width and/or height', 602); + } + if ($this->destination === '') { + $this->destination = add_ending_slash(dirname($this->source)); + } + $this->source_image_fileinfo = get_image_fileinfo($this->source); + if (!$this->source_image_fileinfo) { + throw new Exception("Can't get source image info", 611); + } + if (!is_dir($this->destination)) { + $old_umask = umask(0); + $make_destination = mkdir($this->destination, 0755, true); + umask($old_umask); + if (!$make_destination) { + throw new Exception('Destination ' . $this->destination . ' is not a dir', 620); + } + } + if (!is_writable($this->destination)) { + throw new Exception("Can't write target destination dir " . $this->destination, 622); + } + } + + protected function resize(): void + { + $this->options['over_resize'] ??= false; + $this->options['fitted'] ??= false; + if ($this->width > 0 && $this->height === 0) { + $this->height = (int) round($this->width / $this->source_image_fileinfo['ratio']); + } + if ($this->height > 0 && $this->width === 0) { + $this->width = (int) round($this->height * $this->source_image_fileinfo['ratio']); + } + $imageSX = $this->source_image_fileinfo['width']; + $imageSY = $this->source_image_fileinfo['height']; + if (!$this->options['over_resize']) { + if ($this->width > $imageSX) { + throw new RangeException( + message('Target width is greater than the original image width'), + 100 + ); + } + if ($this->height > $imageSY) { + throw new RangeException( + message('Target height is greater than the original image width'), + 100 + ); + } + } + if ($this->options['fitted']) { + $this->image->fit($this->width, $this->height); + } else { + $this->image->resize($this->width, $this->height); + } + $this->image->save($this->resized_file); + if (!file_exists($this->resized_file)) { + throw new Exception("Can't create final output image", 630); + } + } +} diff --git a/app/src/Legacy/Classes/IpBan.php b/app/src/Legacy/Classes/IpBan.php new file mode 100644 index 0000000..b7058b4 --- /dev/null +++ b/app/src/Legacy/Classes/IpBan.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +class IpBan +{ + public static function getSingle(array $args = []): array + { + return []; + } + + public static function getAll(): array + { + return []; + } + + public static function delete(array $args = []): int + { + return 0; + } + + public static function update(array $where = [], array $values = []): int + { + return 0; + } + + public static function insert(array $args = []): int + { + return 0; + } + + public static function fill(array &$ip_ban): void + { + } + + public static function validateIP(string $ip, bool $wildcards = true): bool + { + return true; + } +} diff --git a/app/src/Legacy/Classes/L10n.php b/app/src/Legacy/Classes/L10n.php new file mode 100644 index 0000000..7f0d51a --- /dev/null +++ b/app/src/Legacy/Classes/L10n.php @@ -0,0 +1,2335 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Filesystem\fileForPath; +use function Chevere\Filesystem\filePhpReturnForPath; +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Chevere\VariableSupport\StorableVariable; +use Chevereto\Config\Config; +use function Chevereto\Legacy\G\get_client_languages; +use Chevereto\Legacy\G\Gettext; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Vars\cookieVar; +use DirectoryIterator; +use RegexIterator; +use Throwable; + +class L10n +{ + protected static $instance; + + protected static $processed; + + protected const CHV_DEFAULT_LANGUAGE_EXTENSION = 'po'; + + public const PATH_CACHE = PATH_APP_CACHE . 'languages/'; + + public const PATH_CACHE_OVERRIDES = self::PATH_CACHE . 'overrides/'; + + public const LOCALES_AVAILABLE_FILEPATH = self::PATH_CACHE . '_locales.php'; + + public const CHV_BASE_LANGUAGE = 'en'; + + protected static Gettext $gettext; + + protected static array $translation_table; + + protected static array $available_languages = []; + + protected static array $enabled_languages = []; + + protected static array $disabled_languages = []; + + protected static string $locale = self::CHV_BASE_LANGUAGE; + + protected static string $forced_locale = ''; + + public static function cacheFilesystemLocales(): array + { + $directory = new DirectoryIterator(PATH_APP_LANGUAGES); + $regex = new RegexIterator($directory, '/^.+\.' . self::CHV_DEFAULT_LANGUAGE_EXTENSION . '$/i', RegexIterator::GET_MATCH); + $files = []; + foreach ($regex as $file) { + $file = $file[0]; + $locale_code = basename($file, '.' . self::CHV_DEFAULT_LANGUAGE_EXTENSION); + $files[$locale_code] = self::getLocales()[$locale_code]; + } + $files = array_filter($files); + ksort($files); + fileForPath(self::LOCALES_AVAILABLE_FILEPATH) + ->createIfNotExists(); + filePhpReturnForPath(self::LOCALES_AVAILABLE_FILEPATH) + ->put( + new StorableVariable($files) + ); + + return $files; + } + + public static function getLocalesAvailable(): array + { + $file = filePhpReturnForPath(self::LOCALES_AVAILABLE_FILEPATH); + if (!$file->filePhp()->file()->exists()) { + return []; + } + + return $file->raw(); + } + + public static function bindEnabled() + { + $locales = self::getLocales(); + self::$available_languages = self::getLocalesAvailable(); + self::$enabled_languages = self::$available_languages; + foreach (getSetting('languages_disable') as $k) { + $k = str_replace('_', '-', $k); + self::$disabled_languages[$k] = $locales[$k]; + unset(self::$enabled_languages[$k]); + } + } + + public function __construct( + string $defaultLanguage, + bool $autoLanguage, + ) { + if (self::$available_languages === []) { + self::bindEnabled(); + } + if (self::$forced_locale === '') { + if (array_key_exists($defaultLanguage, self::$available_languages)) { + $locale = $defaultLanguage; + } else { + $locale = self::$locale; + } + if ($autoLanguage) { + foreach (get_client_languages() as $k => $v) { + $user_locale = str_replace('_', '-', $k); + if (array_key_exists($user_locale, self::$available_languages) && !array_key_exists($user_locale, self::$disabled_languages)) { + $locale = $user_locale; + + break; + } else { + foreach (self::$available_languages as $k => $v) { + if ($v['base'] == substr($user_locale, 0, 2)) { + $locale = $k; + + break; + } + } + } + if ($locale) { + break; + } + } + } + } else { + $locale = self::$forced_locale; + } + if (!defined('CHV_LANGUAGE_CODE')) { + define('CHV_LANGUAGE_CODE', $locale); + } + if (!defined('CHV_LANGUAGE_FILE')) { + define('CHV_LANGUAGE_FILE', PATH_APP_LANGUAGES . $locale . '.' . self::CHV_DEFAULT_LANGUAGE_EXTENSION); + } + self::processTranslation($locale); + self::$instance = $this; + } + + public static function hasInstance(): bool + { + return isset(self::$instance); + } + + public static function getInstance(): static + { + if (is_null(self::$instance)) { + throw new LogicException( + message('L10n instance is not set'), + 600 + ); + } + + return self::$instance; + } + + public static function setLocale(string $locale): void + { + if (is_null(self::$instance)) { + self::$forced_locale = $locale; + } else { + self::processTranslation($locale); + } + } + + public static function processTranslation(string $locale): void + { + if ($locale === self::$locale && isset(self::$translation_table)) { + return; + } + if (!array_key_exists($locale, self::$available_languages) + && array_key_exists(self::$locale, self::$available_languages)) { + $array = self::$available_languages; + reset($array); + $first_key = key($array); + $locale = $first_key; + } + $filename = $locale . '.' . self::CHV_DEFAULT_LANGUAGE_EXTENSION; + $language_file = PATH_APP_LANGUAGES . $filename; + $language_override_file = PATH_APP_LANGUAGES . 'overrides/' . $filename; + self::$locale = $locale; + $language_handling = [ + 'base' => [ + 'file' => $language_file, + 'cache_path' => self::PATH_CACHE, + 'table' => [], + ], + 'override' => [ + 'file' => $language_override_file, + 'cache_path' => self::PATH_CACHE . 'overrides/', + 'table' => [], + ] + ]; + foreach ($language_handling as $k => $v) { + $cache_path = $v['cache_path']; + $cache_file = basename($v['file']) . '.cache.php'; + if (!file_exists($v['file'])) { + continue; + } + if (!file_exists($cache_path)) { + try { + mkdir($cache_path); + } catch (Throwable $e) { + $cache_path = dirname($cache_path); + } + } + self::$gettext = new Gettext([ + 'file' => $v['file'], + 'cache_filepath' => $cache_path . $cache_file, + 'cache_header' => $k == 'base', + ]); + if ($k == 'base') { + $translation_plural = self::$gettext->translation_plural; + $translation_header = self::$gettext->translation_header; + } + $language_handling[$k]['table'] = self::$gettext->translation_table; + } + if (!isset($translation_plural, $translation_header)) { + throw new LogicException(); + } + self::$gettext->translation_plural = $translation_plural; + self::$gettext->translation_header = $translation_header; + self::$gettext->translation_table = array_merge( + $language_handling['base']['table'], + $language_handling['override']['table'] + ); + self::$translation_table = self::$gettext->translation_table; + } + + public static function gettext(string $msg): string + { + return self::getGettext()->gettext($msg) ?? $msg; + } + + public static function ngettext(string $msg, string $msg_plural, int $count): string + { + return self::getGettext()->ngettext($msg, $msg_plural, $count) ?? $msg; + } + + public static function setStatic(string $var, mixed $value): void + { + $instance = self::getInstance(); + $instance::${$var} = $value; + } + + public static function getStatic(string $var): mixed + { + $instance = self::getInstance(); + + return $instance::${$var}; + } + + public static function getAvailableLanguages(): array + { + return self::getStatic('available_languages'); + } + + public static function getEnabledLanguages(): array + { + if (is_null(self::$instance)) { + self::bindEnabled(); + } + + return self::$enabled_languages; + } + + public static function getDisabledLanguages(): array + { + return self::getStatic('disabled_languages'); + } + + public static function getGettext(): Gettext + { + return self::getStatic('gettext'); + } + + public static function getTranslation(): array + { + return self::getStatic('translation_table'); + } + + public static function getLocale(): string + { + return self::getStatic('locale'); + } + + public static function setCookieLang(string $lang): void + { + $args = [ + 'USER_SELECTED_LANG', + $lang, + time() + (60 * 60 * 24 * 30), + Config::host()->hostnamePath(), + Config::host()->hostname(), + HTTP_APP_PROTOCOL == 'https', // secure, + true, // httpOnly + ]; + if (setcookie(...$args)) { + cookieVar()->put('USER_SELECTED_LANG', $lang); + } + } + + public static function getLocales(): array + { + return [ + 'af' => [ + 'code' => 'af', + 'dir' => 'ltr', + 'name' => 'Afrikaans', + 'base' => 'af', + 'short_name' => 'AF', + ], + 'af-AF' => [ + 'code' => 'af-AF', + 'dir' => 'ltr', + 'name' => 'Afrikaans', + 'base' => 'af', + 'short_name' => 'AF (AF)', + ], + 'am' => [ + 'code' => 'am', + 'dir' => 'ltr', + 'name' => 'āmariññā', + 'base' => 'am', + 'short_name' => 'AM', + ], + 'am-AM' => [ + 'code' => 'am-AM', + 'dir' => 'ltr', + 'name' => 'āmariññā', + 'base' => 'am', + 'short_name' => 'AM (AM)', + ], + 'an' => [ + 'code' => 'an', + 'dir' => 'ltr', + 'name' => 'Aragonés', + 'base' => 'an', + 'short_name' => 'AN', + ], + 'an-AN' => [ + 'code' => 'an-AN', + 'dir' => 'ltr', + 'name' => 'Aragonés', + 'base' => 'an', + 'short_name' => 'AN (AN)', + ], + 'ar' => [ + 'code' => 'ar', + 'dir' => 'rtl', + 'name' => 'العربية', + 'base' => 'ar', + 'short_name' => 'AR', + ], + 'ar-AE' => [ + 'code' => 'ar-AE', + 'dir' => 'rtl', + 'name' => 'العربية (الإمارات)', + 'base' => 'ar', + 'short_name' => 'AR (AE)', + ], + 'ar-BH' => [ + 'code' => 'ar-BH', + 'dir' => 'rtl', + 'name' => 'العربية (البحرين)', + 'base' => 'ar', + 'short_name' => 'AR (BH)', + ], + 'ar-DZ' => [ + 'code' => 'ar-DZ', + 'dir' => 'rtl', + 'name' => 'العربية (الجزائر)', + 'base' => 'ar', + 'short_name' => 'AR (DZ)', + ], + 'ar-EG' => [ + 'code' => 'ar-EG', + 'dir' => 'rtl', + 'name' => 'العربية (مصر)', + 'base' => 'ar', + 'short_name' => 'AR (EG)', + ], + 'ar-IQ' => [ + 'code' => 'ar-IQ', + 'dir' => 'rtl', + 'name' => 'العربية (العراق)', + 'base' => 'ar', + 'short_name' => 'AR (IQ)', + ], + 'ar-JO' => [ + 'code' => 'ar-JO', + 'dir' => 'rtl', + 'name' => 'العربية (الأردن)', + 'base' => 'ar', + 'short_name' => 'AR (JO)', + ], + 'ar-KW' => [ + 'code' => 'ar-KW', + 'dir' => 'rtl', + 'name' => 'العربية (الكويت)', + 'base' => 'ar', + 'short_name' => 'AR (KW)', + ], + 'ar-LB' => [ + 'code' => 'ar-LB', + 'dir' => 'rtl', + 'name' => 'العربية (لبنان)', + 'base' => 'ar', + 'short_name' => 'AR (LB)', + ], + 'ar-LY' => [ + 'code' => 'ar-LY', + 'dir' => 'rtl', + 'name' => 'العربية (ليبيا)', + 'base' => 'ar', + 'short_name' => 'AR (LY)', + ], + 'ar-MA' => [ + 'code' => 'ar-MA', + 'dir' => 'rtl', + 'name' => 'العربية (المغرب)', + 'base' => 'ar', + 'short_name' => 'AR (MA)', + ], + 'ar-OM' => [ + 'code' => 'ar-OM', + 'dir' => 'rtl', + 'name' => 'العربية (سلطنة عمان)', + 'base' => 'ar', + 'short_name' => 'AR (OM)', + ], + 'ar-QA' => [ + 'code' => 'ar-QA', + 'dir' => 'rtl', + 'name' => 'العربية (قطر)', + 'base' => 'ar', + 'short_name' => 'AR (QA)', + ], + 'ar-SA' => [ + 'code' => 'ar-SA', + 'dir' => 'rtl', + 'name' => 'العربية (السعودية)', + 'base' => 'ar', + 'short_name' => 'AR (SA)', + ], + 'ar-SD' => [ + 'code' => 'ar-SD', + 'dir' => 'rtl', + 'name' => 'العربية (السودان)', + 'base' => 'ar', + 'short_name' => 'AR (SD)', + ], + 'ar-SY' => [ + 'code' => 'ar-SY', + 'dir' => 'rtl', + 'name' => 'العربية (سوريا)', + 'base' => 'ar', + 'short_name' => 'AR (SY)', + ], + 'ar-TN' => [ + 'code' => 'ar-TN', + 'dir' => 'rtl', + 'name' => 'العربية (تونس)', + 'base' => 'ar', + 'short_name' => 'AR (TN)', + ], + 'ar-YE' => [ + 'code' => 'ar-YE', + 'dir' => 'rtl', + 'name' => 'العربية (اليمن)', + 'base' => 'ar', + 'short_name' => 'AR (YE)', + ], + 'as' => [ + 'code' => 'as', + 'dir' => 'ltr', + 'name' => 'অসমীয়া', + 'base' => 'as', + 'short_name' => 'AS', + ], + 'as-AS' => [ + 'code' => 'as-AS', + 'dir' => 'ltr', + 'name' => 'অসমীয়া', + 'base' => 'as', + 'short_name' => 'AS (AS)', + ], + 'ast' => [ + 'code' => 'ast', + 'dir' => 'ltr', + 'name' => 'Asturianu', + 'base' => 'ast', + 'short_name' => 'AST', + ], + 'ast-AST' => [ + 'code' => 'ast-AST', + 'dir' => 'ltr', + 'name' => 'Asturianu', + 'base' => 'ast', + 'short_name' => 'AST (AST)', + ], + 'az' => [ + 'code' => 'az', + 'dir' => 'ltr', + 'name' => 'Azərbaycan', + 'base' => 'az', + 'short_name' => 'AZ', + ], + 'az-AZ' => [ + 'code' => 'az-AZ', + 'dir' => 'ltr', + 'name' => 'Azərbaycan', + 'base' => 'az', + 'short_name' => 'AZ (AZ)', + ], + 'ba' => [ + 'code' => 'ba', + 'dir' => 'ltr', + 'name' => 'Башҡорт', + 'base' => 'ba', + 'short_name' => 'BA', + ], + 'ba-BA' => [ + 'code' => 'ba-BA', + 'dir' => 'ltr', + 'name' => 'Башҡорт', + 'base' => 'ba', + 'short_name' => 'BA (BA)', + ], + 'be' => [ + 'code' => 'be', + 'dir' => 'ltr', + 'name' => 'Беларускі', + 'base' => 'be', + 'short_name' => 'BE', + ], + 'be-BY' => [ + 'code' => 'be-BY', + 'dir' => 'ltr', + 'name' => 'Беларускі', + 'base' => 'be', + 'short_name' => 'BE (BY)', + ], + 'bg' => [ + 'code' => 'bg', + 'dir' => 'ltr', + 'name' => 'Български', + 'base' => 'bg', + 'short_name' => 'BG', + ], + 'bg-BG' => [ + 'code' => 'bg-BG', + 'dir' => 'ltr', + 'name' => 'Български', + 'base' => 'bg', + 'short_name' => 'BG (BG)', + ], + 'bn' => [ + 'code' => 'bn', + 'dir' => 'ltr', + 'name' => 'Bangla', + 'base' => 'bn', + 'short_name' => 'BN', + ], + 'bn-BN' => [ + 'code' => 'bn-BN', + 'dir' => 'ltr', + 'name' => 'Bangla', + 'base' => 'bn', + 'short_name' => 'BN (BN)', + ], + 'br' => [ + 'code' => 'br', + 'dir' => 'ltr', + 'name' => 'Brezhoneg', + 'base' => 'br', + 'short_name' => 'BR', + ], + 'br-BR' => [ + 'code' => 'br-BR', + 'dir' => 'ltr', + 'name' => 'Brezhoneg', + 'base' => 'br', + 'short_name' => 'BR (BR)', + ], + 'bs' => [ + 'code' => 'bs', + 'dir' => 'ltr', + 'name' => 'Bosanski', + 'base' => 'bs', + 'short_name' => 'BS', + ], + 'bs-BS' => [ + 'code' => 'bs-BS', + 'dir' => 'ltr', + 'name' => 'Bosanski', + 'base' => 'bs', + 'short_name' => 'BS (BS)', + ], + 'ca' => [ + 'code' => 'ca', + 'dir' => 'ltr', + 'name' => 'Сatalà', + 'base' => 'ca', + 'short_name' => 'CA', + ], + 'ca-ES' => [ + 'code' => 'ca-ES', + 'dir' => 'ltr', + 'name' => 'Сatalà (Espanya)', + 'base' => 'ca', + 'short_name' => 'CA (ES)', + ], + 'ce' => [ + 'code' => 'ce', + 'dir' => 'ltr', + 'name' => 'Нохчийн', + 'base' => 'ce', + 'short_name' => 'CE', + ], + 'ce-CE' => [ + 'code' => 'ce-CE', + 'dir' => 'ltr', + 'name' => 'Нохчийн', + 'base' => 'ce', + 'short_name' => 'CE (CE)', + ], + 'ch' => [ + 'code' => 'ch', + 'dir' => 'ltr', + 'name' => 'Chamoru', + 'base' => 'ch', + 'short_name' => 'CH', + ], + 'ch-CH' => [ + 'code' => 'ch-CH', + 'dir' => 'ltr', + 'name' => 'Chamoru', + 'base' => 'ch', + 'short_name' => 'CH (CH)', + ], + 'co' => [ + 'code' => 'co', + 'dir' => 'ltr', + 'name' => 'Corsu', + 'base' => 'co', + 'short_name' => 'CO', + ], + 'co-CO' => [ + 'code' => 'co-CO', + 'dir' => 'ltr', + 'name' => 'Corsu', + 'base' => 'co', + 'short_name' => 'CO (CO)', + ], + 'cr' => [ + 'code' => 'cr', + 'dir' => 'ltr', + 'name' => 'Cree', + 'base' => 'cr', + 'short_name' => 'CR', + ], + 'cr-CR' => [ + 'code' => 'cr-CR', + 'dir' => 'ltr', + 'name' => 'Cree', + 'base' => 'cr', + 'short_name' => 'CR (CR)', + ], + 'cs' => [ + 'code' => 'cs', + 'dir' => 'ltr', + 'name' => 'Čeština', + 'base' => 'cs', + 'short_name' => 'CS', + ], + 'cs-CZ' => [ + 'code' => 'cs-CZ', + 'dir' => 'ltr', + 'name' => 'Čeština', + 'base' => 'cs', + 'short_name' => 'CS (CZ)', + ], + 'cv' => [ + 'code' => 'cv', + 'dir' => 'ltr', + 'name' => 'Чăвашла', + 'base' => 'cv', + 'short_name' => 'CV', + ], + 'cv-CV' => [ + 'code' => 'cv-CV', + 'dir' => 'ltr', + 'name' => 'Чăвашла', + 'base' => 'cv', + 'short_name' => 'CV (CV)', + ], + 'cy' => [ + 'code' => 'cy', + 'dir' => 'ltr', + 'name' => 'Cymraeg', + 'base' => 'cy', + 'short_name' => 'CY', + ], + 'cy-CY' => [ + 'code' => 'cy-CY', + 'dir' => 'ltr', + 'name' => 'Cymraeg', + 'base' => 'cy', + 'short_name' => 'CY (CY)', + ], + 'da' => [ + 'code' => 'da', + 'dir' => 'ltr', + 'name' => 'Dansk', + 'base' => 'da', + 'short_name' => 'DA', + ], + 'da-DK' => [ + 'code' => 'da-DK', + 'dir' => 'ltr', + 'name' => 'Dansk', + 'base' => 'da', + 'short_name' => 'DA (DK)', + ], + 'de' => [ + 'code' => 'de', + 'dir' => 'ltr', + 'name' => 'Deutsch', + 'base' => 'de', + 'short_name' => 'DE', + ], + 'de-AT' => [ + 'code' => 'de-AT', + 'dir' => 'ltr', + 'name' => 'Deutsch (Österreich)', + 'base' => 'de', + 'short_name' => 'DE (AT)', + ], + 'de-CH' => [ + 'code' => 'de-CH', + 'dir' => 'ltr', + 'name' => 'Deutsch (Schweiz)', + 'base' => 'de', + 'short_name' => 'DE (CH)', + ], + 'de-DE' => [ + 'code' => 'de-DE', + 'dir' => 'ltr', + 'name' => 'Deutsch (Deutschland)', + 'base' => 'de', + 'short_name' => 'DE (DE)', + ], + 'de-LU' => [ + 'code' => 'de-LU', + 'dir' => 'ltr', + 'name' => 'Deutsch (Luxemburg)', + 'base' => 'de', + 'short_name' => 'DE (LU)', + ], + 'el' => [ + 'code' => 'el', + 'dir' => 'ltr', + 'name' => 'Ελληνικά', + 'base' => 'el', + 'short_name' => 'EL', + ], + 'el-CY' => [ + 'code' => 'el-CY', + 'dir' => 'ltr', + 'name' => 'Ελληνικά (Κύπρος)', + 'base' => 'el', + 'short_name' => 'EL (CY)', + ], + 'el-GR' => [ + 'code' => 'el-GR', + 'dir' => 'ltr', + 'name' => 'Ελληνικά (Ελλάδα)', + 'base' => 'el', + 'short_name' => 'EL (GR)', + ], + 'en' => [ + 'code' => 'en', + 'dir' => 'ltr', + 'name' => 'English', + 'base' => 'en', + 'short_name' => 'EN', + ], + 'en-AU' => [ + 'code' => 'en-AU', + 'dir' => 'ltr', + 'name' => 'English (Australia)', + 'base' => 'en', + 'short_name' => 'EN (AU)', + ], + 'en-CA' => [ + 'code' => 'en-CA', + 'dir' => 'ltr', + 'name' => 'English (Canada)', + 'base' => 'en', + 'short_name' => 'EN (CA)', + ], + 'en-GB' => [ + 'code' => 'en-GB', + 'dir' => 'ltr', + 'name' => 'English (UK)', + 'base' => 'en', + 'short_name' => 'EN (GB)', + ], + 'en-IE' => [ + 'code' => 'en-IE', + 'dir' => 'ltr', + 'name' => 'English (Ireland)', + 'base' => 'en', + 'short_name' => 'EN (IE)', + ], + 'en-IN' => [ + 'code' => 'en-IN', + 'dir' => 'ltr', + 'name' => 'English (India)', + 'base' => 'en', + 'short_name' => 'EN (IN)', + ], + 'en-MT' => [ + 'code' => 'en-MT', + 'dir' => 'ltr', + 'name' => 'English (Malta)', + 'base' => 'en', + 'short_name' => 'EN (MT)', + ], + 'en-NZ' => [ + 'code' => 'en-NZ', + 'dir' => 'ltr', + 'name' => 'English (New Zealand)', + 'base' => 'en', + 'short_name' => 'EN (NZ)', + ], + 'en-PH' => [ + 'code' => 'en-PH', + 'dir' => 'ltr', + 'name' => 'English (Philippines)', + 'base' => 'en', + 'short_name' => 'EN (PH)', + ], + 'en-SG' => [ + 'code' => 'en-SG', + 'dir' => 'ltr', + 'name' => 'English (Singapore)', + 'base' => 'en', + 'short_name' => 'EN (SG)', + ], + 'en-US' => [ + 'code' => 'en-US', + 'dir' => 'ltr', + 'name' => 'English (US)', + 'base' => 'en', + 'short_name' => 'EN (US)', + ], + 'en-ZA' => [ + 'code' => 'en-ZA', + 'dir' => 'ltr', + 'name' => 'English (South Africa)', + 'base' => 'en', + 'short_name' => 'EN (ZA)', + ], + 'eo' => [ + 'code' => 'eo', + 'dir' => 'ltr', + 'name' => 'Esperanta', + 'base' => 'eo', + 'short_name' => 'EO', + ], + 'eo-EO' => [ + 'code' => 'eo-EO', + 'dir' => 'ltr', + 'name' => 'Esperanta', + 'base' => 'eo', + 'short_name' => 'EO (EO)', + ], + 'es' => [ + 'code' => 'es', + 'dir' => 'ltr', + 'name' => 'Español', + 'base' => 'es', + 'short_name' => 'ES', + ], + 'es-AR' => [ + 'code' => 'es-AR', + 'dir' => 'ltr', + 'name' => 'Español (Argentina)', + 'base' => 'es', + 'short_name' => 'ES (AR)', + ], + 'es-BO' => [ + 'code' => 'es-BO', + 'dir' => 'ltr', + 'name' => 'Español (Bolivia)', + 'base' => 'es', + 'short_name' => 'ES (BO)', + ], + 'es-CL' => [ + 'code' => 'es-CL', + 'dir' => 'ltr', + 'name' => 'Español (Chile)', + 'base' => 'es', + 'short_name' => 'ES (CL)', + ], + 'es-CO' => [ + 'code' => 'es-CO', + 'dir' => 'ltr', + 'name' => 'Español (Colombia)', + 'base' => 'es', + 'short_name' => 'ES (CO)', + ], + 'es-CR' => [ + 'code' => 'es-CR', + 'dir' => 'ltr', + 'name' => 'Español (Costa Rica)', + 'base' => 'es', + 'short_name' => 'ES (CR)', + ], + 'es-DO' => [ + 'code' => 'es-DO', + 'dir' => 'ltr', + 'name' => 'Español (República Dominicana)', + 'base' => 'es', + 'short_name' => 'ES (DO)', + ], + 'es-EC' => [ + 'code' => 'es-EC', + 'dir' => 'ltr', + 'name' => 'Español (Ecuador)', + 'base' => 'es', + 'short_name' => 'ES (EC)', + ], + 'es-ES' => [ + 'code' => 'es-ES', + 'dir' => 'ltr', + 'name' => 'Español (España)', + 'base' => 'es', + 'short_name' => 'ES (ES)', + ], + 'es-GT' => [ + 'code' => 'es-GT', + 'dir' => 'ltr', + 'name' => 'Español (Guatemala)', + 'base' => 'es', + 'short_name' => 'ES (GT)', + ], + 'es-HN' => [ + 'code' => 'es-HN', + 'dir' => 'ltr', + 'name' => 'Español (Honduras)', + 'base' => 'es', + 'short_name' => 'ES (HN)', + ], + 'es-MX' => [ + 'code' => 'es-MX', + 'dir' => 'ltr', + 'name' => 'Español (México)', + 'base' => 'es', + 'short_name' => 'ES (MX)', + ], + 'es-NI' => [ + 'code' => 'es-NI', + 'dir' => 'ltr', + 'name' => 'Español (Nicaragua)', + 'base' => 'es', + 'short_name' => 'ES (NI)', + ], + 'es-PA' => [ + 'code' => 'es-PA', + 'dir' => 'ltr', + 'name' => 'Español (Panamá)', + 'base' => 'es', + 'short_name' => 'ES (PA)', + ], + 'es-PE' => [ + 'code' => 'es-PE', + 'dir' => 'ltr', + 'name' => 'Español (Perú)', + 'base' => 'es', + 'short_name' => 'ES (PE)', + ], + 'es-PR' => [ + 'code' => 'es-PR', + 'dir' => 'ltr', + 'name' => 'Español (Puerto Rico)', + 'base' => 'es', + 'short_name' => 'ES (PR)', + ], + 'es-PY' => [ + 'code' => 'es-PY', + 'dir' => 'ltr', + 'name' => 'Español (Paraguay)', + 'base' => 'es', + 'short_name' => 'ES (PY)', + ], + 'es-SV' => [ + 'code' => 'es-SV', + 'dir' => 'ltr', + 'name' => 'Español (El Salvador)', + 'base' => 'es', + 'short_name' => 'ES (SV)', + ], + 'es-US' => [ + 'code' => 'es-US', + 'dir' => 'ltr', + 'name' => 'Español (Estados Unidos)', + 'base' => 'es', + 'short_name' => 'ES (US)', + ], + 'es-UY' => [ + 'code' => 'es-UY', + 'dir' => 'ltr', + 'name' => 'Español (Uruguay)', + 'base' => 'es', + 'short_name' => 'ES (UY)', + ], + 'es-VE' => [ + 'code' => 'es-VE', + 'dir' => 'ltr', + 'name' => 'Español (Venezuela)', + 'base' => 'es', + 'short_name' => 'ES (VE)', + ], + 'et' => [ + 'code' => 'et', + 'dir' => 'ltr', + 'name' => 'Eesti', + 'base' => 'et', + 'short_name' => 'ET', + ], + 'et-EE' => [ + 'code' => 'et-EE', + 'dir' => 'ltr', + 'name' => 'Eesti (Eesti)', + 'base' => 'et', + 'short_name' => 'ET (EE)', + ], + 'eu' => [ + 'code' => 'eu', + 'dir' => 'ltr', + 'name' => 'Euskera', + 'base' => 'eu', + 'short_name' => 'EU', + ], + 'eu-EU' => [ + 'code' => 'eu-EU', + 'dir' => 'ltr', + 'name' => 'Euskera', + 'base' => 'eu', + 'short_name' => 'EU (EU)', + ], + 'fa' => [ + 'code' => 'fa', + 'dir' => 'rtl', + 'name' => 'فارسی', + 'base' => 'fa', + 'short_name' => 'FA', + ], + 'fa-FA' => [ + 'code' => 'fa-FA', + 'dir' => 'rtl', + 'name' => 'فارسی', + 'base' => 'fa', + 'short_name' => 'FA (FA)', + ], + 'fi' => [ + 'code' => 'fi', + 'dir' => 'ltr', + 'name' => 'Suomi', + 'base' => 'fi', + 'short_name' => 'FI', + ], + 'fi-FI' => [ + 'code' => 'fi-FI', + 'dir' => 'ltr', + 'name' => 'Suomi', + 'base' => 'fi', + 'short_name' => 'FI (FI)', + ], + 'fj' => [ + 'code' => 'fj', + 'dir' => 'ltr', + 'name' => 'Na Vosa Vakaviti', + 'base' => 'fj', + 'short_name' => 'FJ', + ], + 'fj-FJ' => [ + 'code' => 'fj-FJ', + 'dir' => 'ltr', + 'name' => 'Na Vosa Vakaviti', + 'base' => 'fj', + 'short_name' => 'FJ (FJ)', + ], + 'fo' => [ + 'code' => 'fo', + 'dir' => 'ltr', + 'name' => 'Føroyskt', + 'base' => 'fo', + 'short_name' => 'FO', + ], + 'fo-FO' => [ + 'code' => 'fo-FO', + 'dir' => 'ltr', + 'name' => 'Føroyskt', + 'base' => 'fo', + 'short_name' => 'FO (FO)', + ], + 'fr' => [ + 'code' => 'fr', + 'dir' => 'ltr', + 'name' => 'Français', + 'base' => 'fr', + 'short_name' => 'FR', + ], + 'fr-BE' => [ + 'code' => 'fr-BE', + 'dir' => 'ltr', + 'name' => 'Français (Belgique)', + 'base' => 'fr', + 'short_name' => 'FR (BE)', + ], + 'fr-CA' => [ + 'code' => 'fr-CA', + 'dir' => 'ltr', + 'name' => 'Français (Canada)', + 'base' => 'fr', + 'short_name' => 'FR (CA)', + ], + 'fr-CH' => [ + 'code' => 'fr-CH', + 'dir' => 'ltr', + 'name' => 'Français (Suisse)', + 'base' => 'fr', + 'short_name' => 'FR (CH)', + ], + 'fr-FR' => [ + 'code' => 'fr-FR', + 'dir' => 'ltr', + 'name' => 'Français (France)', + 'base' => 'fr', + 'short_name' => 'FR (FR)', + ], + 'fr-LU' => [ + 'code' => 'fr-LU', + 'dir' => 'ltr', + 'name' => 'Français (Luxembourg)', + 'base' => 'fr', + 'short_name' => 'FR (LU)', + ], + 'fy' => [ + 'code' => 'fy', + 'dir' => 'ltr', + 'name' => 'Frysk', + 'base' => 'fy', + 'short_name' => 'FY', + ], + 'fy-FY' => [ + 'code' => 'fy-FY', + 'dir' => 'ltr', + 'name' => 'Frysk', + 'base' => 'fy', + 'short_name' => 'FY (FY)', + ], + 'ga' => [ + 'code' => 'ga', + 'dir' => 'ltr', + 'name' => 'Gaeilge', + 'base' => 'ga', + 'short_name' => 'GA', + ], + 'ga-IE' => [ + 'code' => 'ga-IE', + 'dir' => 'ltr', + 'name' => 'Gaeilge (Éire)', + 'base' => 'ga', + 'short_name' => 'GA (IE)', + ], + 'gd' => [ + 'code' => 'gd', + 'dir' => 'ltr', + 'name' => 'Gàidhlig', + 'base' => 'gd', + 'short_name' => 'GD', + ], + 'gd-GD' => [ + 'code' => 'gd-GD', + 'dir' => 'ltr', + 'name' => 'Gàidhlig', + 'base' => 'gd', + 'short_name' => 'GD (GD)', + ], + 'gl' => [ + 'code' => 'gl', + 'dir' => 'ltr', + 'name' => 'Galego', + 'base' => 'gl', + 'short_name' => 'GL', + ], + 'gl-GL' => [ + 'code' => 'gl-GL', + 'dir' => 'ltr', + 'name' => 'Galego', + 'base' => 'gl', + 'short_name' => 'GL (GL)', + ], + 'gu' => [ + 'code' => 'gu', + 'dir' => 'ltr', + 'name' => 'Gujarati', + 'base' => 'gu', + 'short_name' => 'GU', + ], + 'gu-GU' => [ + 'code' => 'gu-GU', + 'dir' => 'ltr', + 'name' => 'Gujarati', + 'base' => 'gu', + 'short_name' => 'GU (GU)', + ], + 'he' => [ + 'code' => 'he', + 'dir' => 'rtl', + 'name' => 'עברית', + 'base' => 'he', + 'short_name' => 'HE', + ], + 'he-IL' => [ + 'code' => 'he-IL', + 'dir' => 'rtl', + 'name' => 'עברית', + 'base' => 'he', + 'short_name' => 'HE (IL)', + ], + 'hi' => [ + 'code' => 'hi', + 'dir' => 'ltr', + 'name' => 'हिंदी', + 'base' => 'hi', + 'short_name' => 'HI', + ], + 'hi-IN' => [ + 'code' => 'hi-IN', + 'dir' => 'ltr', + 'name' => 'हिंदी (भारत)', + 'base' => 'hi', + 'short_name' => 'HI (IN)', + ], + 'hr' => [ + 'code' => 'hr', + 'dir' => 'ltr', + 'name' => 'Hrvatski', + 'base' => 'hr', + 'short_name' => 'HR', + ], + 'hr-HR' => [ + 'code' => 'hr-HR', + 'dir' => 'ltr', + 'name' => 'Hrvatski', + 'base' => 'hr', + 'short_name' => 'HR (HR)', + ], + 'hsb' => [ + 'code' => 'hsb', + 'dir' => 'ltr', + 'name' => 'Hornjoserbšćina', + 'base' => 'hsb', + 'short_name' => 'HSB', + ], + 'hsb-HSB' => [ + 'code' => 'hsb-HSB', + 'dir' => 'ltr', + 'name' => 'Hornjoserbšćina', + 'base' => 'hsb', + 'short_name' => 'HSB (HSB)', + ], + 'ht' => [ + 'code' => 'ht', + 'dir' => 'ltr', + 'name' => 'Kreyòl Ayisyen', + 'base' => 'ht', + 'short_name' => 'HT', + ], + 'ht-HT' => [ + 'code' => 'ht-HT', + 'dir' => 'ltr', + 'name' => 'Kreyòl Ayisyen', + 'base' => 'ht', + 'short_name' => 'HT (HT)', + ], + 'hu' => [ + 'code' => 'hu', + 'dir' => 'ltr', + 'name' => 'Magyar', + 'base' => 'hu', + 'short_name' => 'HU', + ], + 'hu-HU' => [ + 'code' => 'hu-HU', + 'dir' => 'ltr', + 'name' => 'Magyar', + 'base' => 'hu', + 'short_name' => 'HU (HU)', + ], + 'hy' => [ + 'code' => 'hy', + 'dir' => 'ltr', + 'name' => 'հայերեն', + 'base' => 'hy', + 'short_name' => 'HY', + ], + 'hy-HY' => [ + 'code' => 'hy-HY', + 'dir' => 'ltr', + 'name' => 'հայերեն', + 'base' => 'hy', + 'short_name' => 'HY (HY)', + ], + 'ia' => [ + 'code' => 'ia', + 'dir' => 'ltr', + 'name' => 'Interlingua', + 'base' => 'ia', + 'short_name' => 'IA', + ], + 'ia-IA' => [ + 'code' => 'ia-IA', + 'dir' => 'ltr', + 'name' => 'Interlingua', + 'base' => 'ia', + 'short_name' => 'IA (IA)', + ], + 'id' => [ + 'code' => 'id', + 'dir' => 'ltr', + 'name' => 'Bahasa Indonesia', + 'base' => 'id', + 'short_name' => 'ID', + ], + 'id-ID' => [ + 'code' => 'id-ID', + 'dir' => 'ltr', + 'name' => 'Bahasa Indonesia', + 'base' => 'id', + 'short_name' => 'ID (ID)', + ], + 'ie' => [ + 'code' => 'ie', + 'dir' => 'ltr', + 'name' => 'Interlingue', + 'base' => 'ie', + 'short_name' => 'IE', + ], + 'ie-IE' => [ + 'code' => 'ie-IE', + 'dir' => 'ltr', + 'name' => 'Interlingue', + 'base' => 'ie', + 'short_name' => 'IE (IE)', + ], + 'in' => [ + 'code' => 'in', + 'dir' => 'ltr', + 'name' => 'Bahasa Indonesia', + 'base' => 'in', + 'short_name' => 'IN', + ], + 'in-ID' => [ + 'code' => 'in-ID', + 'dir' => 'ltr', + 'name' => 'Bahasa Indonesia (Indonesia)', + 'base' => 'in', + 'short_name' => 'IN (ID)', + ], + 'is' => [ + 'code' => 'is', + 'dir' => 'ltr', + 'name' => 'Íslenska', + 'base' => 'is', + 'short_name' => 'IS', + ], + 'is-IS' => [ + 'code' => 'is-IS', + 'dir' => 'ltr', + 'name' => 'Íslenska (Ísland)', + 'base' => 'is', + 'short_name' => 'IS (IS)', + ], + 'it' => [ + 'code' => 'it', + 'dir' => 'ltr', + 'name' => 'Italiano', + 'base' => 'it', + 'short_name' => 'IT', + ], + 'it-CH' => [ + 'code' => 'it-CH', + 'dir' => 'ltr', + 'name' => 'Italiano (Svizzera)', + 'base' => 'it', + 'short_name' => 'IT (CH)', + ], + 'it-IT' => [ + 'code' => 'it-IT', + 'dir' => 'ltr', + 'name' => 'Italiano (Italia)', + 'base' => 'it', + 'short_name' => 'IT (IT)', + ], + 'iu' => [ + 'code' => 'iu', + 'dir' => 'ltr', + 'name' => 'Inuktitut', + 'base' => 'iu', + 'short_name' => 'IU', + ], + 'iu-IU' => [ + 'code' => 'iu-IU', + 'dir' => 'ltr', + 'name' => 'Inuktitut', + 'base' => 'iu', + 'short_name' => 'IU (IU)', + ], + 'iw' => [ + 'code' => 'iw', + 'dir' => 'ltr', + 'name' => 'עברית', + 'base' => 'iw', + 'short_name' => 'IW', + ], + 'iw-IL' => [ + 'code' => 'iw-IL', + 'dir' => 'ltr', + 'name' => 'עברית', + 'base' => 'iw', + 'short_name' => 'IW (IL)', + ], + 'ja' => [ + 'code' => 'ja', + 'dir' => 'ltr', + 'name' => '日本語', + 'base' => 'ja', + 'short_name' => 'JA', + ], + 'ja-JP' => [ + 'code' => 'ja-JP', + 'dir' => 'ltr', + 'name' => '日本語', + 'base' => 'ja', + 'short_name' => 'JA (JP)', + ], + 'ka' => [ + 'code' => 'ka', + 'dir' => 'ltr', + 'name' => 'ქართული', + 'base' => 'ka', + 'short_name' => 'KA', + ], + 'ka-KA' => [ + 'code' => 'ka-KA', + 'dir' => 'ltr', + 'name' => 'ქართული', + 'base' => 'ka', + 'short_name' => 'KA (KA)', + ], + 'kk' => [ + 'code' => 'kk', + 'dir' => 'ltr', + 'name' => 'Қазақша', + 'base' => 'kk', + 'short_name' => 'KK', + ], + 'kk-KK' => [ + 'code' => 'kk-KK', + 'dir' => 'ltr', + 'name' => 'Қазақша', + 'base' => 'kk', + 'short_name' => 'KK (KK)', + ], + 'km' => [ + 'code' => 'km', + 'dir' => 'ltr', + 'name' => 'Khmer', + 'base' => 'km', + 'short_name' => 'KM', + ], + 'km-KM' => [ + 'code' => 'km-KM', + 'dir' => 'ltr', + 'name' => 'Khmer', + 'base' => 'km', + 'short_name' => 'KM (KM)', + ], + 'ko' => [ + 'code' => 'ko', + 'dir' => 'ltr', + 'name' => '한국어', + 'base' => 'ko', + 'short_name' => 'KO', + ], + 'ko-KR' => [ + 'code' => 'ko-KR', + 'dir' => 'ltr', + 'name' => '한국어', + 'base' => 'ko', + 'short_name' => 'KO (KR)', + ], + 'ky' => [ + 'code' => 'ky', + 'dir' => 'ltr', + 'name' => 'Кыргызча', + 'base' => 'ky', + 'short_name' => 'KY', + ], + 'ky-KY' => [ + 'code' => 'ky-KY', + 'dir' => 'ltr', + 'name' => 'Кыргызча', + 'base' => 'ky', + 'short_name' => 'KY (KY)', + ], + 'la' => [ + 'code' => 'la', + 'dir' => 'ltr', + 'name' => 'Latina', + 'base' => 'la', + 'short_name' => 'LA', + ], + 'la-LA' => [ + 'code' => 'la-LA', + 'dir' => 'ltr', + 'name' => 'Latina', + 'base' => 'la', + 'short_name' => 'LA (LA)', + ], + 'lb' => [ + 'code' => 'lb', + 'dir' => 'ltr', + 'name' => 'Lëtzebuergesch', + 'base' => 'lb', + 'short_name' => 'LB', + ], + 'lb-LB' => [ + 'code' => 'lb-LB', + 'dir' => 'ltr', + 'name' => 'Lëtzebuergesch', + 'base' => 'lb', + 'short_name' => 'LB (LB)', + ], + 'lt' => [ + 'code' => 'lt', + 'dir' => 'ltr', + 'name' => 'Lietuvių', + 'base' => 'lt', + 'short_name' => 'LT', + ], + 'lt-LT' => [ + 'code' => 'lt-LT', + 'dir' => 'ltr', + 'name' => 'Lietuvių (Lietuva)', + 'base' => 'lt', + 'short_name' => 'LT (LT)', + ], + 'lv' => [ + 'code' => 'lv', + 'dir' => 'ltr', + 'name' => 'Latviešu', + 'base' => 'lv', + 'short_name' => 'LV', + ], + 'lv-LV' => [ + 'code' => 'lv-LV', + 'dir' => 'ltr', + 'name' => 'Latviešu (Latvija)', + 'base' => 'lv', + 'short_name' => 'LV (LV)', + ], + 'mi' => [ + 'code' => 'mi', + 'dir' => 'ltr', + 'name' => 'Te Reo Māori', + 'base' => 'mi', + 'short_name' => 'MI', + ], + 'mi-MI' => [ + 'code' => 'mi-MI', + 'dir' => 'ltr', + 'name' => 'Te Reo Māori', + 'base' => 'mi', + 'short_name' => 'MI (MI)', + ], + 'mk' => [ + 'code' => 'mk', + 'dir' => 'ltr', + 'name' => 'Македонски', + 'base' => 'mk', + 'short_name' => 'MK', + ], + 'mk-MK' => [ + 'code' => 'mk-MK', + 'dir' => 'ltr', + 'name' => 'Македонски (Македонија)', + 'base' => 'mk', + 'short_name' => 'MK (MK)', + ], + 'ml' => [ + 'code' => 'ml', + 'dir' => 'ltr', + 'name' => 'Malayalam', + 'base' => 'ml', + 'short_name' => 'ML', + ], + 'ml-ML' => [ + 'code' => 'ml-ML', + 'dir' => 'ltr', + 'name' => 'Malayalam', + 'base' => 'ml', + 'short_name' => 'ML (ML)', + ], + 'mo' => [ + 'code' => 'mo', + 'dir' => 'ltr', + 'name' => 'Graiul Moldovenesc', + 'base' => 'mo', + 'short_name' => 'MO', + ], + 'mo-MO' => [ + 'code' => 'mo-MO', + 'dir' => 'ltr', + 'name' => 'Graiul Moldovenesc', + 'base' => 'mo', + 'short_name' => 'MO (MO)', + ], + 'mr' => [ + 'code' => 'mr', + 'dir' => 'ltr', + 'name' => 'मराठी', + 'base' => 'mr', + 'short_name' => 'MR', + ], + 'mr-MR' => [ + 'code' => 'mr-MR', + 'dir' => 'ltr', + 'name' => 'मराठी', + 'base' => 'mr', + 'short_name' => 'MR (MR)', + ], + 'ms' => [ + 'code' => 'ms', + 'dir' => 'ltr', + 'name' => 'Bahasa Melayu', + 'base' => 'ms', + 'short_name' => 'MS', + ], + 'ms-MY' => [ + 'code' => 'ms-MY', + 'dir' => 'ltr', + 'name' => 'Bahasa Melayu', + 'base' => 'ms', + 'short_name' => 'MS (MY)', + ], + 'mt' => [ + 'code' => 'mt', + 'dir' => 'ltr', + 'name' => 'Malti', + 'base' => 'mt', + 'short_name' => 'MT', + ], + 'mt-MT' => [ + 'code' => 'mt-MT', + 'dir' => 'ltr', + 'name' => 'Malti', + 'base' => 'mt', + 'short_name' => 'MT (MT)', + ], + 'nb' => [ + 'code' => 'nb', + 'dir' => 'ltr', + 'name' => '‪Norsk Bokmål‬', + 'base' => 'nb', + 'short_name' => 'NB', + ], + 'nb-NB' => [ + 'code' => 'nb-NB', + 'dir' => 'ltr', + 'name' => '‪Norsk Bokmål‬', + 'base' => 'nb', + 'short_name' => 'NB (NB)', + ], + 'ne' => [ + 'code' => 'ne', + 'dir' => 'ltr', + 'name' => 'नेपाली', + 'base' => 'ne', + 'short_name' => 'NE', + ], + 'ne-NE' => [ + 'code' => 'ne-NE', + 'dir' => 'ltr', + 'name' => 'नेपाली', + 'base' => 'ne', + 'short_name' => 'NE (NE)', + ], + 'ng' => [ + 'code' => 'ng', + 'dir' => 'ltr', + 'name' => 'Oshiwambo', + 'base' => 'ng', + 'short_name' => 'NG', + ], + 'ng-NG' => [ + 'code' => 'ng-NG', + 'dir' => 'ltr', + 'name' => 'Oshiwambo', + 'base' => 'ng', + 'short_name' => 'NG (NG)', + ], + 'nl' => [ + 'code' => 'nl', + 'dir' => 'ltr', + 'name' => 'Nederlands', + 'base' => 'nl', + 'short_name' => 'NL', + ], + 'nl-BE' => [ + 'code' => 'nl-BE', + 'dir' => 'ltr', + 'name' => 'Nederlands (België)', + 'base' => 'nl', + 'short_name' => 'NL (BE)', + ], + 'nl-NL' => [ + 'code' => 'nl-NL', + 'dir' => 'ltr', + 'name' => 'Nederlands (Nederland)', + 'base' => 'nl', + 'short_name' => 'NL (NL)', + ], + 'nn' => [ + 'code' => 'nn', + 'dir' => 'ltr', + 'name' => 'Norsk', + 'base' => 'nn', + 'short_name' => 'NN', + ], + 'nn-NN' => [ + 'code' => 'nn-NN', + 'dir' => 'ltr', + 'name' => 'Norsk (Nynorsk)', + 'base' => 'nn', + 'short_name' => 'NN (NN)', + ], + 'no' => [ + 'code' => 'no', + 'dir' => 'ltr', + 'name' => 'Norsk', + 'base' => 'no', + 'short_name' => 'NO', + ], + 'no-NO' => [ + 'code' => 'no-NO', + 'dir' => 'ltr', + 'name' => 'Norsk (Norge)', + 'base' => 'no', + 'short_name' => 'NO (NO)', + ], + 'nv' => [ + 'code' => 'nv', + 'dir' => 'ltr', + 'name' => 'Diné Bizaad', + 'base' => 'nv', + 'short_name' => 'NV', + ], + 'nv-NV' => [ + 'code' => 'nv-NV', + 'dir' => 'ltr', + 'name' => 'Diné Bizaad', + 'base' => 'nv', + 'short_name' => 'NV (NV)', + ], + 'oc' => [ + 'code' => 'oc', + 'dir' => 'ltr', + 'name' => 'Lenga d’òc', + 'base' => 'oc', + 'short_name' => 'OC', + ], + 'oc-OC' => [ + 'code' => 'oc-OC', + 'dir' => 'ltr', + 'name' => 'Lenga d’òc', + 'base' => 'oc', + 'short_name' => 'OC (OC)', + ], + 'om' => [ + 'code' => 'om', + 'dir' => 'ltr', + 'name' => 'Afaan Oromoo', + 'base' => 'om', + 'short_name' => 'OM', + ], + 'om-OM' => [ + 'code' => 'om-OM', + 'dir' => 'ltr', + 'name' => 'Afaan Oromoo', + 'base' => 'om', + 'short_name' => 'OM (OM)', + ], + 'pa' => [ + 'code' => 'pa', + 'dir' => 'rtl', + 'name' => 'भारत गणराज्य', + 'base' => 'pa', + 'short_name' => 'PA', + ], + 'pa-IN' => [ + 'code' => 'pa-IN', + 'dir' => 'rtl', + 'name' => 'भारत गणराज्य (नेपाली)', + 'base' => 'pa', + 'short_name' => 'PA (IN)', + ], + 'pa-PK' => [ + 'code' => 'pa-PK', + 'dir' => 'rtl', + 'name' => 'یپنجاب (پنجاب)', + 'base' => 'pa', + 'short_name' => 'PA (PK)', + ], + 'pl' => [ + 'code' => 'pl', + 'dir' => 'ltr', + 'name' => 'Polski', + 'base' => 'pl', + 'short_name' => 'PL', + ], + 'pl-PL' => [ + 'code' => 'pl-PL', + 'dir' => 'ltr', + 'name' => 'Polski (Polska)', + 'base' => 'pl', + 'short_name' => 'PL (PL)', + ], + 'pt' => [ + 'code' => 'pt', + 'dir' => 'ltr', + 'name' => 'Português', + 'base' => 'pt', + 'short_name' => 'PT', + ], + 'pt-BR' => [ + 'code' => 'pt-BR', + 'dir' => 'ltr', + 'name' => 'Português (Brasil)', + 'base' => 'pt', + 'short_name' => 'PT (BR)', + ], + 'pt-PT' => [ + 'code' => 'pt-PT', + 'dir' => 'ltr', + 'name' => 'Português (Portugal)', + 'base' => 'pt', + 'short_name' => 'PT (PT)', + ], + 'qu' => [ + 'code' => 'qu', + 'dir' => 'ltr', + 'name' => 'Runa Simi', + 'base' => 'qu', + 'short_name' => 'QU', + ], + 'qu-QU' => [ + 'code' => 'qu-QU', + 'dir' => 'ltr', + 'name' => 'Runa Simi', + 'base' => 'qu', + 'short_name' => 'QU (QU)', + ], + 'rm' => [ + 'code' => 'rm', + 'dir' => 'ltr', + 'name' => 'Rumantsch', + 'base' => 'rm', + 'short_name' => 'RM', + ], + 'rm-RM' => [ + 'code' => 'rm-RM', + 'dir' => 'ltr', + 'name' => 'Rumantsch', + 'base' => 'rm', + 'short_name' => 'RM (RM)', + ], + 'ro' => [ + 'code' => 'ro', + 'dir' => 'ltr', + 'name' => 'Română', + 'base' => 'ro', + 'short_name' => 'RO', + ], + 'ro-RO' => [ + 'code' => 'ro-RO', + 'dir' => 'ltr', + 'name' => 'Română (România)', + 'base' => 'ro', + 'short_name' => 'RO (RO)', + ], + 'ru' => [ + 'code' => 'ru', + 'dir' => 'ltr', + 'name' => 'Русский', + 'base' => 'ru', + 'short_name' => 'RU', + ], + 'ru-RU' => [ + 'code' => 'ru-RU', + 'dir' => 'ltr', + 'name' => 'Русский (Россия)', + 'base' => 'ru', + 'short_name' => 'RU (RU)', + ], + 'sa' => [ + 'code' => 'sa', + 'dir' => 'ltr', + 'name' => 'संस्कृत', + 'base' => 'sa', + 'short_name' => 'SA', + ], + 'sa-SA' => [ + 'code' => 'sa-SA', + 'dir' => 'ltr', + 'name' => 'संस्कृत', + 'base' => 'sa', + 'short_name' => 'SA (SA)', + ], + 'sc' => [ + 'code' => 'sc', + 'dir' => 'ltr', + 'name' => 'Sardu', + 'base' => 'sc', + 'short_name' => 'SC', + ], + 'sc-SC' => [ + 'code' => 'sc-SC', + 'dir' => 'ltr', + 'name' => 'Sardu', + 'base' => 'sc', + 'short_name' => 'SC (SC)', + ], + 'sd' => [ + 'code' => 'sd', + 'dir' => 'rtl', + 'name' => 'فارسی', + 'base' => 'sd', + 'short_name' => 'SD', + ], + 'sd-SD' => [ + 'code' => 'sd-SD', + 'dir' => 'rtl', + 'name' => 'فارسی', + 'base' => 'sd', + 'short_name' => 'SD (SD)', + ], + 'sg' => [ + 'code' => 'sg', + 'dir' => 'ltr', + 'name' => 'Sango', + 'base' => 'sg', + 'short_name' => 'SG', + ], + 'sg-SG' => [ + 'code' => 'sg-SG', + 'dir' => 'ltr', + 'name' => 'Sango', + 'base' => 'sg', + 'short_name' => 'SG (SG)', + ], + 'sk' => [ + 'code' => 'sk', + 'dir' => 'ltr', + 'name' => 'Slovenčina', + 'base' => 'sk', + 'short_name' => 'SK', + ], + 'sk-SK' => [ + 'code' => 'sk-SK', + 'dir' => 'ltr', + 'name' => 'Slovenčina (Slovenská republika)', + 'base' => 'sk', + 'short_name' => 'SK (SK)', + ], + 'sl' => [ + 'code' => 'sl', + 'dir' => 'ltr', + 'name' => 'Slovenščina', + 'base' => 'sl', + 'short_name' => 'SL', + ], + 'sl-SI' => [ + 'code' => 'sl-SI', + 'dir' => 'ltr', + 'name' => 'Slovenščina (Slovenija)', + 'base' => 'sl', + 'short_name' => 'SL (SI)', + ], + 'so' => [ + 'code' => 'so', + 'dir' => 'ltr', + 'name' => 'Af Somali', + 'base' => 'so', + 'short_name' => 'SO', + ], + 'so-SO' => [ + 'code' => 'so-SO', + 'dir' => 'ltr', + 'name' => 'Af Somali', + 'base' => 'so', + 'short_name' => 'SO (SO)', + ], + 'sq' => [ + 'code' => 'sq', + 'dir' => 'ltr', + 'name' => 'Shqipe', + 'base' => 'sq', + 'short_name' => 'SQ', + ], + 'sq-AL' => [ + 'code' => 'sq-AL', + 'dir' => 'ltr', + 'name' => 'Shqipe', + 'base' => 'sq', + 'short_name' => 'SQ (AL)', + ], + 'sr' => [ + 'code' => 'sr', + 'dir' => 'ltr', + 'name' => 'Српски', + 'base' => 'sr', + 'short_name' => 'SR', + ], + 'sr-BA' => [ + 'code' => 'sr-BA', + 'dir' => 'ltr', + 'name' => 'Српски (Босна и Херцеговина)', + 'base' => 'sr', + 'short_name' => 'SR (BA)', + ], + 'sr-CS' => [ + 'code' => 'sr-CS', + 'dir' => 'ltr', + 'name' => 'Српски (Србија и Црна Гора)', + 'base' => 'sr', + 'short_name' => 'SR (CS)', + ], + 'sr-RS' => [ + 'code' => 'sr-RS', + 'dir' => 'ltr', + 'name' => 'Српски', + 'base' => 'sr', + 'short_name' => 'SR (RS)', + ], + 'sv' => [ + 'code' => 'sv', + 'dir' => 'ltr', + 'name' => 'Svenska', + 'base' => 'sv', + 'short_name' => 'SV', + ], + 'sv-SE' => [ + 'code' => 'sv-SE', + 'dir' => 'ltr', + 'name' => 'Svenska (Sverige)', + 'base' => 'sv', + 'short_name' => 'SV (SE)', + ], + 'sw' => [ + 'code' => 'sw', + 'dir' => 'ltr', + 'name' => 'Kiswahili', + 'base' => 'sw', + 'short_name' => 'SW', + ], + 'sw-SW' => [ + 'code' => 'sw-SW', + 'dir' => 'ltr', + 'name' => 'Kiswahili', + 'base' => 'sw', + 'short_name' => 'SW (SW)', + ], + 'ta' => [ + 'code' => 'ta', + 'dir' => 'ltr', + 'name' => 'தமிழ', + 'base' => 'ta', + 'short_name' => 'TA', + ], + 'ta-TA' => [ + 'code' => 'ta-TA', + 'dir' => 'ltr', + 'name' => 'தமிழ', + 'base' => 'ta', + 'short_name' => 'TA (TA)', + ], + 'th' => [ + 'code' => 'th', + 'dir' => 'ltr', + 'name' => 'ไทย', + 'base' => 'th', + 'short_name' => 'TH', + ], + 'th-TH' => [ + 'code' => 'th-TH', + 'dir' => 'ltr', + 'name' => 'ไทย (ประเทศไทย)', + 'base' => 'th', + 'short_name' => 'TH (TH)', + ], + 'tig' => [ + 'code' => 'tig', + 'dir' => 'ltr', + 'name' => 'Tigré', + 'base' => 'tig', + 'short_name' => 'TIG', + ], + 'tig-TIG' => [ + 'code' => 'tig-TIG', + 'dir' => 'ltr', + 'name' => 'Tigré', + 'base' => 'tig', + 'short_name' => 'TIG (TIG)', + ], + 'tk' => [ + 'code' => 'tk', + 'dir' => 'ltr', + 'name' => 'Түркмен', + 'base' => 'tk', + 'short_name' => 'TK', + ], + 'tk-TK' => [ + 'code' => 'tk-TK', + 'dir' => 'ltr', + 'name' => 'Түркмен', + 'base' => 'tk', + 'short_name' => 'TK (TK)', + ], + 'tlh' => [ + 'code' => 'tlh', + 'dir' => 'ltr', + 'name' => 'tlhIngan Hol', + 'base' => 'tlh', + 'short_name' => 'TLH', + ], + 'tlh-TLH' => [ + 'code' => 'tlh-TLH', + 'dir' => 'ltr', + 'name' => 'tlhIngan Hol', + 'base' => 'tlh', + 'short_name' => 'TLH (TLH)', + ], + 'tr' => [ + 'code' => 'tr', + 'dir' => 'ltr', + 'name' => 'Türkçe', + 'base' => 'tr', + 'short_name' => 'TR', + ], + 'tr-TR' => [ + 'code' => 'tr-TR', + 'dir' => 'ltr', + 'name' => 'Türkçe', + 'base' => 'tr', + 'short_name' => 'TR (TR)', + ], + 'uk' => [ + 'code' => 'uk', + 'dir' => 'ltr', + 'name' => 'Українська', + 'base' => 'uk', + 'short_name' => 'UK', + ], + 'uk-UA' => [ + 'code' => 'uk-UA', + 'dir' => 'ltr', + 'name' => 'Українська', + 'base' => 'uk', + 'short_name' => 'UK (UA)', + ], + 've' => [ + 'code' => 've', + 'dir' => 'ltr', + 'name' => 'Tshivenda', + 'base' => 've', + 'short_name' => 'VE', + ], + 've-VE' => [ + 'code' => 've-VE', + 'dir' => 'ltr', + 'name' => 'Tshivenda', + 'base' => 've', + 'short_name' => 'VE (VE)', + ], + 'vi' => [ + 'code' => 'vi', + 'dir' => 'ltr', + 'name' => 'Tiếng Việt', + 'base' => 'vi', + 'short_name' => 'VI', + ], + 'vi-VN' => [ + 'code' => 'vi-VN', + 'dir' => 'ltr', + 'name' => 'Tiếng Việt', + 'base' => 'vi', + 'short_name' => 'VI (VN)', + ], + 'vo' => [ + 'code' => 'vo', + 'dir' => 'ltr', + 'name' => 'Volapük', + 'base' => 'vo', + 'short_name' => 'VO', + ], + 'vo-VO' => [ + 'code' => 'vo-VO', + 'dir' => 'ltr', + 'name' => 'Volapük', + 'base' => 'vo', + 'short_name' => 'VO (VO)', + ], + 'wa' => [ + 'code' => 'wa', + 'dir' => 'ltr', + 'name' => 'Walon', + 'base' => 'wa', + 'short_name' => 'WA', + ], + 'wa-WA' => [ + 'code' => 'wa-WA', + 'dir' => 'ltr', + 'name' => 'Walon', + 'base' => 'wa', + 'short_name' => 'WA (WA)', + ], + 'xh' => [ + 'code' => 'xh', + 'dir' => 'ltr', + 'name' => 'isiXhosa', + 'base' => 'xh', + 'short_name' => 'XH', + ], + 'xh-XH' => [ + 'code' => 'xh-XH', + 'dir' => 'ltr', + 'name' => 'isiXhosa', + 'base' => 'xh', + 'short_name' => 'XH (XH)', + ], + 'yi' => [ + 'code' => 'yi', + 'dir' => 'rtl', + 'name' => 'ייִדיש', + 'base' => 'yi', + 'short_name' => 'YI', + ], + 'yi-YI' => [ + 'code' => 'yi-YI', + 'dir' => 'rtl', + 'name' => 'ייִדיש', + 'base' => 'yi', + 'short_name' => 'YI (YI)', + ], + 'zh' => [ + 'code' => 'zh', + 'dir' => 'ltr', + 'name' => '中文', + 'base' => 'zh', + 'short_name' => 'ZH', + ], + // hans + 'zh-CN' => [ + 'code' => 'zh-CN', + 'dir' => 'ltr', + 'name' => '简体中文', + 'base' => 'zh', + 'short_name' => 'ZH (CN)', + ], + 'zh-HK' => [ + 'code' => 'zh-HK', + 'dir' => 'ltr', + 'name' => '中文 (香港)', + 'base' => 'zh', + 'short_name' => 'ZH (HK)', + ], + 'zh-SG' => [ + 'code' => 'zh-SG', + 'dir' => 'ltr', + 'name' => '中文 (新加坡)', + 'base' => 'zh', + 'short_name' => 'ZH (SG)', + ], + // hant + 'zh-TW' => [ + 'code' => 'zh-TW', + 'dir' => 'ltr', + 'name' => '繁體中文', + 'base' => 'zh', + 'short_name' => 'ZH (TW)', + ], + 'zu' => [ + 'code' => 'zu', + 'dir' => 'ltr', + 'name' => 'isiZulu', + 'base' => 'zu', + 'short_name' => 'ZU', + ], + 'zu-ZU' => [ + 'code' => 'zu-ZU', + 'dir' => 'ltr', + 'name' => 'isiZulu', + 'base' => 'zu', + 'short_name' => 'ZU (ZU)', + ], + ]; + } +} diff --git a/app/src/Legacy/Classes/Listing.php b/app/src/Legacy/Classes/Listing.php new file mode 100644 index 0000000..bcdf61a --- /dev/null +++ b/app/src/Legacy/Classes/Listing.php @@ -0,0 +1,1057 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use BadMethodCallException; +use function Chevereto\Legacy\decodeID; +use function Chevereto\Legacy\encodeID; +use function Chevereto\Legacy\G\ends_with; +use function Chevereto\Legacy\G\forward_slash; +use function Chevereto\Legacy\G\get_base_url; +use function Chevereto\Legacy\G\get_route_name; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\str_replace_first; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\missing_values_to_exception; +use function Chevereto\Vars\env; +use function Chevereto\Vars\request; +use DateTime; +use Exception; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use RecursiveRegexIterator; +use RegexIterator; + +class Listing +{ + public string $query; + + private int $offset; + + public array $seek; + + private array $params_hidden; + + private int $limit; + + private string $sort_type; + + private string $sort_order; + + private int $owner; + + private array $requester = []; + + private $privacy; + + private int $output_count = 0; + + private bool $has_page_next; + + public string $seekEnd = ''; + + public string $seekStart = ''; + + public int $count = 0; + + public bool $nsfw; + + private array $output_assoc = []; + + private bool $sfw = true; + + private bool $has_page_prev; + + private int $isApproved = 1; + + public static array $valid_types = ['images', 'albums', 'users']; + + public static array $valid_sort_types = ['date_gmt', 'size', 'views', 'id', 'image_count', 'name', 'title', 'username']; + + public array $output = []; + + private array $binds = []; + + private string $type; + + private int $category; + + private string $where = ''; + + private array|bool $tools = false; + + private bool $reverse = false; + + private ?string $outputTpl; + + public function outputCount(): int + { + return $this->output_count; + } + + public function outputAssoc(): array + { + return $this->output_assoc; + } + + public function setOutputTpl(string $tpl): void + { + $this->outputTpl = $tpl; + } + + public function outputTpl(): ?string + { + return $this->outputTpl ?? null; + } + + public function debugQuery() + { + if (!isset($this->query)) { + throw new BadMethodCallException(); + } + $params = []; + foreach ($this->binds as $bind) { + $params[] = $bind['param'] . '=' . $bind['value']; + } + + return '# Dumped listing query' + . "\n" . $this->query + . "\n\n# Dumped query params" + . "\n" . implode("\n", $params); + } + + public function limit(): int + { + return $this->limit; + } + + // Sets the `image_is_approved` flag + public function setApproved($bool) + { + $this->isApproved = $bool; + } + + // Sets the type of resource being listed + public function setType($type) + { + $this->type = $type; + } + + // Sets the offset (sql> LIMIT offset,limit) + public function setOffset($offset) + { + $this->offset = (int) $offset; + } + + public function sfw(): bool + { + return $this->sfw; + } + + public function has_page_prev(): bool + { + return $this->has_page_prev; + } + + public function has_page_next(): bool + { + return $this->has_page_next; + } + + // Sets ID to seek next-to + public function setSeek(string $seek) + { + if (strpos($seek, '.') !== false) { + $explode = explode('.', $seek); + $copy = $explode; + end($explode); + $last = key($explode); + unset($copy[$last]); + $array = [ + 0 => implode('.', $copy), + 1 => decodeID($explode[$last]) + ]; + $this->seek = $array; + + return; + } + $decodeID = decodeID($seek); + if (ctype_digit(strval($decodeID))) { + $this->seek = ['0000-01-01 00:00:00', $decodeID]; + } + } + + public function setReverse($bool) + { + $this->reverse = $bool; + } + + public function setParamsHidden($params) + { + $this->params_hidden = $params; + } + + // Sets the limit (sql> LIMIT offset,limit) + public function setLimit($limit) + { + $this->limit = (int) $limit; + } + + // Sets the sort type (sql> SORT BY sort_type) + public function setSortType($sort_type) + { + $this->sort_type = $sort_type == 'date' ? 'date_gmt' : $sort_type; + } + + // Sets the sort order (sql> DESC | ASC) + public function setSortOrder($sort_order) + { + $this->sort_order = $sort_order; + } + + // Sets the WHERE clause + public function setWhere(string $where) + { + $this->where = $where; + } + + public function setOwner(int $user_id) + { + $this->owner = $user_id; + } + + public function setRequester(array $user) + { + $this->requester = $user; + } + + public function setCategory($category) + { + $this->category = (int) $category; + } + + public function setPrivacy($privacy) + { + $this->privacy = $privacy; + } + + public function setTools(array|bool $flag) + { + $this->tools = $flag; + } + + public function bind($param, $value, $type = null) + { + $this->binds[] = [ + 'param' => $param, + 'value' => $value, + 'type' => $type + ]; + } + + private function getWhere(string $where): string + { + return ($this->where == '' ? 'WHERE ' : ($this->where . ' AND ')) . $where; + } + + /** + * Do the thing + * @Exeption 4xx + */ + public function exec() + { + $this->validateInput(); + $tables = DB::getTables(); + if ($this->requester === []) { + $this->setRequester(Login::getUser()); + } + if ($this->type == 'images') { + $this->where = $this->getWhere('image_is_approved = ' . (int) $this->isApproved); + } + if (!(bool) env()['CHEVERETO_ENABLE_USERS']) { + $userId = getSetting('website_mode_personal_uid') ?? 0; + $this->where = match ($this->type) { + 'images' => $this->getWhere('image_user_id=' . $userId), + 'albums' => $this->getWhere('album_user_id=' . $userId), + default => $this->where + }; + } + $joins = [ + 'images' => [ + 'storages' => 'LEFT JOIN ' . $tables['storages'] . ' ON ' . $tables['images'] . '.image_storage_id = ' . $tables['storages'] . '.storage_id', + 'users' => 'LEFT JOIN ' . $tables['users'] . ' ON ' . $tables['images'] . '.image_user_id = ' . $tables['users'] . '.user_id', + 'albums' => 'LEFT JOIN ' . $tables['albums'] . ' ON ' . $tables['images'] . '.image_album_id = ' . $tables['albums'] . '.album_id', + 'categories' => 'LEFT JOIN ' . $tables['categories'] . ' ON ' . $tables['images'] . '.image_category_id = ' . $tables['categories'] . '.category_id', + ], + 'users' => [], + 'albums' => [ + 'users' => 'LEFT JOIN ' . $tables['users'] . ' ON ' . $tables['albums'] . '.album_user_id = ' . $tables['users'] . '.user_id' + ] + ]; + if ($this->type == 'users' && $this->sort_type == 'views') { + $this->sort_type = 'content_views'; + } + if (isset($this->params_hidden)) { + $emptyTypeClauses['users'][] = 'user_image_count > 0 OR user_avatar_filename IS NOT NULL OR user_background_filename IS NOT NULL'; + if ($this->sort_type == 'views') { + $emptyTypeClauses['albums'][] = 'album_views > 0'; + $emptyTypeClauses['images'][] = 'image_views > 0'; + $emptyTypeClauses['users'][] = 'user_content_views > 0'; + } + if ($this->sort_type == 'likes') { + $emptyTypeClauses['albums'][] = 'album_likes > 0'; + $emptyTypeClauses['images'][] = 'image_likes > 0'; + $emptyTypeClauses['users'][] = 'user_likes > 0'; + } + if ($this->type == 'albums') { + if (isset($this->params_hidden['album_min_image_count']) && $this->params_hidden['album_min_image_count'] > 0) { + $whereClauses[] = sprintf('album_image_count >= %d', $this->params_hidden['album_min_image_count']); + } else { + $emptyTypeClauses['albums'][] = 'album_image_count > 0'; + } + } + if (array_key_exists($this->type, $emptyTypeClauses) && isset($this->params_hidden['hide_empty']) && $this->params_hidden['hide_empty'] == 1) { + $whereClauses[] = '(' . implode(') AND (', $emptyTypeClauses[$this->type]) . ')'; + } + if (isset($this->params_hidden['hide_banned']) && $this->params_hidden['hide_banned'] == 1) { + $whereClauses[] = '(' . $tables['users'] . '.user_status IS NULL OR ' . $tables['users'] . '.user_status <> "banned"' . ')'; + } + if ($this->type == 'images' && isset($this->params_hidden['is_animated']) && $this->params_hidden['is_animated'] == 1) { + $whereClauses[] = 'image_is_animated = 1'; + } + if (!empty($whereClauses)) { + $whereClauses = implode(' AND ', $whereClauses); + $this->where = $this->getWhere($whereClauses); + } + } + $type_singular = DB::getFieldPrefix($this->type); + if ($this->where !== '') { + $where_clauses = explode(' ', str_ireplace('WHERE ', '', $this->where)); + $where_arr = []; + foreach ($where_clauses as $clause) { + if (!preg_match('/\./', $clause)) { + $field_prefix = explode('_', $clause, 2)[0]; // field prefix (singular) + $table = DB::getTableFromFieldPrefix($field_prefix); // image -> chv_images + $table_prefix = env()['CHEVERETO_DB_TABLE_PREFIX']; + $table_key = empty($table_prefix) ? $table : str_replace_first($table_prefix, '', $table); + $where_arr[] = array_key_exists($table_key, $tables) ? $table . '.' . $clause : $clause; + } else { + $where_arr[] = $clause; // Let it be + } + } + $this->where = 'WHERE ' . implode(' ', $where_arr); + } + if (version_compare(Settings::get('chevereto_version_installed'), '3.7.0', '>=')) { + // Dynamic since v3.9.0 + $likes_join = 'LEFT JOIN ' . $tables['likes'] . ' ON ' . $tables['likes'] . '.like_content_type = "' . $type_singular . '" AND ' . $tables['likes'] . '.like_content_id = ' . $tables[$this->type] . '.' . $type_singular . '_id'; + if (preg_match('/like_user_id/', $this->where)) { + $joins[$this->type]['likes'] = $likes_join; + } elseif ($this->requester !== [] && $this->type !== 'users') { + $joins[$this->type]['likes'] = $likes_join . ' AND ' . $tables['likes'] . '.like_user_id = ' . $this->requester['id']; + } + $follow_tpl_join = 'LEFT JOIN ' . $tables['follows'] . ' ON ' . $tables['follows'] . '.%FIELD = ' . $tables[$this->type] . '.' . ($this->type == 'users' ? 'user' : DB::getFieldPrefix($this->type) . '_user') . '_id'; + if (preg_match('/follow_user_id/', $this->where)) { + $joins[$this->type]['follows'] = strtr($follow_tpl_join, ['%FIELD' => 'follow_followed_user_id']); + } + if (preg_match('/follow_followed_user_id/', $this->where)) { + $joins[$this->type]['follows'] = strtr($follow_tpl_join, ['%FIELD' => 'follow_user_id']); + } + } + // Add ID reservation clause + if ($this->type == 'images') { + $res_id_where = 'image_size > 0'; + if ($this->where == '') { + $this->where = 'WHERE ' . $res_id_where; + } else { + $this->where .= ' AND ' . $res_id_where; + } + } + // Add category clause + if ($this->type == 'images' && isset($this->category)) { + $category_qry = $tables['images'] . '.image_category_id = ' . $this->category; + if ($this->where == '') { + $this->where = 'WHERE ' . $category_qry; + } else { + $this->where .= ' AND ' . $category_qry; + } + } + // Privacy layer + if ( + !($this->requester['is_admin'] ?? false) + && in_array($this->type, ['images', 'albums', 'users']) + && ( + (!isset($this->owner) || $this->requester === []) || $this->owner !== $this->requester['id'] + ) + ) { + if ($this->where == '') { + $this->where = 'WHERE '; + } else { + $this->where .= ' AND '; + } + $nsfw_off = $this->requester !== [] + ? !$this->requester['show_nsfw_listings'] + : !getSetting('show_nsfw_in_listings'); + switch ($this->type) { + case 'images': + if ($nsfw_off) { + $nsfw_off_clause = $tables['images'] . '.image_nsfw = 0'; + if ($this->requester !== []) { + $this->where .= '(' . $nsfw_off_clause . ' OR (' . $tables['images'] . '.image_nsfw = 1 AND ' . $tables['images'] . '.image_user_id = ' . $this->requester['id'] . ')) AND '; + } else { + $this->where .= $nsfw_off_clause . ' AND '; + } + } + + break; + case 'users': + $this->where .= $tables['users'] . '.user_is_private = 0'; + + break; + } + if ($this->type !== 'users') { + if (getSetting('website_privacy_mode') == 'public' || $this->privacy == 'private_but_link' || getSetting('website_content_privacy_mode') == 'default') { + $this->where .= '(' . $tables['albums'] . '.album_privacy NOT IN'; + $privacy_modes = ['private', 'private_but_link', 'custom']; + if ($this->type === 'images') { + $privacy_modes[] = 'password'; + } + if (isset($this->privacy) && in_array($this->privacy, $privacy_modes)) { + unset($privacy_modes[array_search($this->privacy, $privacy_modes)]); + } + $this->where .= " (" . "'" . implode("','", $privacy_modes) . "'" . ") "; + $this->where .= "OR " . $tables['albums'] . '.album_privacy IS NULL'; + if ($this->requester !== []) { + $this->where .= ' OR ' . $tables['albums'] . '.album_user_id =' . $this->requester['id']; + } + $this->where .= ')'; + } else { + $injected_requester = $this->requester['id'] ?? '0'; + $this->where .= '(' . $tables['albums'] . '.album_user_id = ' . $injected_requester; + $this->where .= $this->type == 'albums' ? ')' : (' OR ' . $tables['images'] . '.image_user_id = ' . $injected_requester . ')'); + } + } + } + $sort_field = $type_singular . '_' . $this->sort_type; + $key_field = $type_singular . '_id'; + if (isset($this->seek)) { + if (ends_with('date_gmt', $this->sort_type)) { + $d = DateTime::createFromFormat('Y-m-d H:i:s', $this->seek[0]); + if (!$d || $d->format('Y-m-d H:i:s') !== $this->seek[0]) { + $this->seek = ['0000-01-01 00:00:00', $this->seek[1]]; + } + } + if ($this->where == '') { + $this->where = 'WHERE '; + } else { + $this->where .= ' AND '; + } + if ($this->reverse) { + $this->sort_order = $this->sort_order == 'asc' ? 'desc' : 'asc'; + } + $signo = $this->sort_order == 'desc' ? '<=' : '>='; + if ($this->sort_type == 'id') { + $this->where .= $sort_field . ' ' . $signo . ' :seek'; + $this->bind(':seek', $this->seek); + } else { + $signo = $this->sort_order == 'desc' ? '<' : '>'; + $this->where .= '((' . $sort_field . ' ' . $signo . ' :seekSort) OR (' . $sort_field . ' = :seekSort AND ' . $key_field . ' ' . $signo . '= :seekKey))'; + $this->bind(':seekSort', $this->seek[0]); + $this->bind(':seekKey', $this->seek[1]); + } + } + if ($this->where !== '') { + $this->where = "\n" . $this->where; + } + $sort_order = strtoupper($this->sort_order); + $table_order = DB::getTableFromFieldPrefix($type_singular); + $order_by = "\n" . 'ORDER BY '; + if (in_array($this->sort_type, ['name', 'title', 'username'])) { + $order_by .= 'CAST(' . $table_order . '.' . $sort_field . ' as CHAR) ' . $sort_order . ', '; + $order_by .= 'LENGTH(' . $table_order . '.' . $sort_field . ') ' . $sort_order . ', '; + } + $order_by .= '' . $table_order . '.' . $sort_field . ' ' . $sort_order; + if ($this->sort_type != 'id') { + $order_by .= ', ' . $table_order . '.' . $key_field . ' ' . $sort_order; + } + $limit = ''; + if ($this->limit > 0) { + $limit = "\n" . 'LIMIT ' . ($this->limit + 1); // +1 allows to fetch "one extra" to detect prev/next pages + } + $base_table = $tables[$this->type]; + // Normal query + if (empty($joins[$this->type])) { + $query = 'SELECT * FROM ' . $base_table; + $query .= $this->where . $order_by . $limit; + // Alternative query + } else { + if ($this->where !== '') { + preg_match_all('/' . env()['CHEVERETO_DB_TABLE_PREFIX'] . '([\w_]+)\./', $this->where, $where_tables); + $where_tables = array_values(array_diff(array_unique($where_tables[1]), [$this->type])); + } else { + $where_tables = false; + } + if ($where_tables !== []) { + $join_tables = $where_tables; + } else { + reset($joins); + $join_tables = [key($joins)]; + } + $join = ''; + if (is_iterable($join_tables)) { + foreach ($join_tables as $join_table) { + if (!empty($joins[$this->type][$join_table])) { + $join .= "\n" . $joins[$this->type][$join_table]; + unset($joins[$this->type][$join_table]); + } + } + } + // Get rid of the original Exif data (for listings) + $null_db = $this->type == 'images' ? ', NULL as image_original_exifdata ' : null; + $query = 'SELECT * ' . $null_db . 'FROM (SELECT * FROM ' . $base_table . $join . $this->where . $order_by . $limit . ') ' . $base_table; + if (!empty($joins[$this->type])) { + $query .= "\n" . implode("\n", $joins[$this->type]); + } + $query .= $order_by; + } + $db = DB::getInstance(); + $this->query = $query; + $db->query($this->query); + foreach ($this->binds as $bind) { + $db->bind($bind['param'], $bind['value'], $bind['type'] ?? null); + } + $this->output = $db->fetchAll(); + $this->output_count = $db->rowCount(); + $this->has_page_next = $db->rowCount() > $this->limit; + $this->has_page_prev = $this->offset > 0; + if ($this->reverse) { + $this->output = array_reverse($this->output); + } + $start = current($this->output); + $end = end($this->output); + $seekEnd = $end[$sort_field] ?? ''; + $seekStart = $start[$sort_field] ?? ''; + if ($this->sort_type == 'id') { + $seekEnd = encodeID((int) $seekEnd); + $seekStart = encodeID((int) $seekStart); + } else { + if (is_array($end)) { + $seekEnd .= '.' . encodeID((int) $end[$key_field]); + } + if (is_array($start)) { + $seekStart .= '.' . encodeID((int) $start[$key_field]); + } + } + if (!$this->has_page_next) { + $seekEnd = ''; + } + if (!$this->has_page_prev) { + $seekStart = ''; + } + $this->seekEnd = $seekEnd; + $this->seekStart = $seekStart; + if ($db->rowCount() > $this->limit) { + array_pop($this->output); + } + $this->output = safe_html($this->output); + $this->count = count($this->output); + $this->nsfw = false; + $this->output_assoc = []; + $formatfn = 'Chevereto\Legacy\Classes\\' . ucfirst(substr($this->type, 0, -1)); + foreach ($this->output as $k => $v) { + $val = $formatfn::formatArray($v); + $this->output_assoc[] = $val; + if (!$this->nsfw && isset($val['nsfw']) && $val['nsfw']) { + $this->nsfw = true; + } + } + if ($this->type === 'albums') { + $this->nsfw = true; + } + $this->sfw = !$this->nsfw; + Handler::setCond('show_viewer_zero', isset(request()['viewer']) && $this->count > 0); + if ($this->type == 'albums' && $this->output !== []) { + $coverTpl = '(SELECT * + FROM %tImages% + LEFT JOIN %tStorages% ON %tImages%.image_storage_id = %tStorages%.storage_id + WHERE image_id = (SELECT album_cover_id FROM %tAlbums% WHERE album_id = %ALBUM_ID%) + AND %tImages%.image_is_approved = 1 + LIMIT 1)'; + $album_cover_qry_tpl = strtr($coverTpl, [ + '%tImages%' => $tables['images'], + '%tStorages%' => $tables['storages'], + '%tAlbums%' => $tables['albums'], + ]); + $albums_cover_qry_arr = []; + $albums_mapping = []; + foreach ($this->output as $k => &$album) { + $album['album_id'] ??= ''; + // @phpstan-ignore-next-line + $album['album_image_count'] ??= 0; + // @phpstan-ignore-next-line + if ($album['album_image_count'] < 0) { + $album['album_image_count'] = 0; + } + $album['album_image_count_label'] = _n('image', 'images', $album['album_image_count']); + $albums_cover_qry_arr[] = str_replace( + '%ALBUM_ID%', + $album['album_id'], + $album_cover_qry_tpl + ); + $albums_mapping[$album['album_id']] = $k; + } + $albums_slice_qry = implode("\n" . 'UNION ALL ' . "\n", $albums_cover_qry_arr); + $db->query($albums_slice_qry); + $albums_slice = $db->fetchAll(); + if (!empty($albums_slice)) { + foreach ($albums_slice as $slice) { + $album_key = $albums_mapping[$slice['image_album_id']] ?? null; + if ($album_key === null) { + continue; + } + if (!isset($this->output[$album_key]['album_images_slice'])) { + $this->output[$album_key]['album_images_slice'] = []; + } + $this->output[$album_key]['album_images_slice'][] = $slice; + } + } + } + } + + public static function getTabs($args = [], $autoParams = [], $expanded = false) + { + $default = [ + 'list' => true, + 'REQUEST' => request(), + 'listing' => 'explore', + 'basename' => get_route_name(), + 'tools' => true, + 'tools_available' => [], + ]; + $args = array_merge($default, $args); + $semantics = [ + 'recent' => [ + 'icon' => 'fas fa-history', + 'label' => _s('Recent'), + 'content' => 'all', + 'sort' => 'date_desc', + ], + 'trending' => [ + 'icon' => 'fas fa-poll', + 'label' => _s('Trending'), + 'content' => 'all', + 'sort' => 'views_desc', + ], + ]; + // Criteria -> images | albums | users + // Criteria -> [CONTENT TABS] + $criterias = [ + 'top-users' => [ + 'icon' => 'fas fa-crown', + 'label' => _s('Top'), + 'sort' => 'image_count_desc', + 'content' => 'users', + ], + 'most-recent' => [ + 'icon' => $semantics['recent']['icon'], + 'label' => _s('Most recent'), + 'sort' => 'date_desc', + 'content' => 'all', + ], + 'most-oldest' => [ + 'icon' => 'fas fa-fast-backward', + 'label' => _s('Oldest'), + 'sort' => 'date_asc', + 'content' => 'all', + ], + 'most-viewed' => [ + 'icon' => $semantics['trending']['icon'], + 'label' => _s('Most viewed'), + 'sort' => 'views_desc', + 'content' => 'all', + ], + ]; + if (Settings::get('enable_likes')) { + $semantics['popular'] = [ + 'icon' => 'fas fa-heart', + 'label' => _s('Popular'), + 'content' => 'all', + 'sort' => 'likes_desc', + ]; + $criterias['most-liked'] = [ + 'icon' => 'fas fa-heart', + 'label' => _s('Most liked'), + 'sort' => 'likes_desc', + 'content' => 'all', + ]; + } + $criterias['album-az-asc'] = [ + 'icon' => 'fas fa-sort-alpha-down', + 'label' => 'AZ', + 'sort' => 'name_asc', + 'content' => 'albums', + ]; + $criterias['image-az-asc'] = [ + 'icon' => 'fas fa-sort-alpha-down', + 'label' => 'AZ', + 'sort' => 'title_asc', + 'content' => 'images', + ]; + $criterias['user-az-asc'] = [ + 'icon' => 'fas fa-sort-alpha-down', + 'label' => 'AZ', + 'sort' => 'username_asc', + 'content' => 'users', + ]; + if (isset($args['order'])) { + $criterias = array_merge(array_flip($args['order']), $criterias); + } + $listings = [ + 'explore' => [ + 'label' => _s('Explore'), + 'content' => 'images', + ], + 'animated' => [ + 'label' => _s('Animated'), + 'content' => 'images', + 'where' => 'image_is_animated = 1', + 'semantic' => true, + ], + 'search' => [ + 'label' => _s('Search'), + 'content' => 'all', + ], + 'users' => [ + 'icon' => 'fas fa-users', + 'label' => _s('People'), + 'content' => 'users', + ], + 'images' => [ + 'icon' => 'fas fa-image', + 'label' => _s('Images'), + 'content' => 'images', + ], + 'albums' => [ + 'icon' => 'fas fa-images', + 'label' => _s('Albums'), + 'content' => 'albums', + ], + ]; + $listings = array_merge($listings, $semantics); + $parameters = []; + if (isset($args['listing'], $listings[$args['listing']])) { + $parameters = $listings[$args['listing']]; + } + if (isset($args['exclude_criterias']) && is_array($args['exclude_criterias'])) { + foreach ($args['exclude_criterias'] as $exclude) { + if (array_key_exists($exclude, $criterias)) { + unset($criterias[$exclude]); + } + } + } + // Content -> most recent | oldest | most viewed | most liked + // Content -> [CRITERIA TABS] + $contents = [ + 'images' => [ + 'icon' => $listings['images']['icon'], + 'label' => _s('Images'), + ], + 'albums' => [ + 'icon' => $listings['albums']['icon'], + 'label' => _s('Albums'), + ], + ]; + if ((bool) env()['CHEVERETO_ENABLE_USERS']) { + $contents['users'] = [ + 'icon' => $listings['users']['icon'], + 'label' => _s('Users'), + ]; + } + $i = 0; + $currentKey = null; + if (!isset($parameters['content'])) { + $parameters['content'] = ''; + } + $iterate = ($parameters['content'] == 'all' ? $contents : (isset($parameters['semantic']) ? $semantics : $criterias)); + $tabs = []; + foreach ($iterate as $k => $v) { + if ($parameters['content'] == 'all') { + $content = $k; + $id = 'list-' . $args['listing'] . '-' . $content; // list-popular-images + $sort = $parameters['sort'] ?? 'date_desc'; + } else { + $content = $parameters['content']; + if ($v['content'] !== 'all' && $v['content'] !== $content) { + continue; + } + $id = 'list-' . $k; // list-most-oldest + $sort = $v['sort']; + } + if (!$content) { + $content = 'images'; // explore + } + $basename = $args['basename']; + $default_params = [ + 'list' => $content, + 'sort' => $sort, + 'page' => '1', + ]; + $params = $args['params'] ?? $default_params; + if (isset($args['params_remove_keys'])) { + foreach ((array) $args['params_remove_keys'] as $key) { + unset($params[$key]); + } + } + if (isset($args['params']) && is_array($args['params']) && array_key_exists('q', $args['params']) && $args['listing'] == 'search') { + $args['params_hidden']['list'] = $content; + $basename .= '/' . $content; + } + if (isset($args['params_hidden'])) { + foreach (array_keys((array) $args['params_hidden']) as $kk) { + if (array_key_exists($kk, $params)) { + unset($params[$kk]); + } + } + } + $http_build_query = http_build_query($params); + $url = get_base_url($basename . '/?' . $http_build_query); + $current = isset($args['REQUEST'], $args['REQUEST']['sort']) ? $args['REQUEST']['sort'] == ($v['sort'] ?? false) : false; + if ($i == 0 && !$current) { + $current = !isset($args['REQUEST']['sort']); + } + if ($current && is_null($currentKey)) { + $currentKey = $i; + } + $tab = [ + 'icon' => $v['icon'] ?? null, + 'list' => (bool) $args['list'], + 'tools' => $content == 'users' ? false : (bool) $args['tools'], + 'tools_available' => $args['tools_available'], + 'label' => $v['label'], + 'id' => $id, + 'params' => $http_build_query, + 'current' => false, + 'type' => $content, + 'url' => $url + ]; + if ($args['tools_available'] && !Handler::cond('allowed_to_delete_content') && array_key_exists('delete', $args['tools_available'])) { + unset($args['tools_available']['delete']); + } + if ($args['tools_available'] == null) { + unset($tab['tools_available']); + } + if (isset($args['params_hidden'])) { + $tab['params_hidden'] = http_build_query($args['params_hidden']); + } + $tabs[] = $tab; + unset($id, $params, $basename, $http_build_query, $content, $current); + $i++; + } + if (is_null($currentKey)) { + $currentKey = 0; + if ($parameters['content'] == 'all') { + foreach ($tabs as $k => &$v) { + if (isset($args['REQUEST']['list']) && $v['type'] == $args['REQUEST']['list']) { + $currentKey = $k; + + break; + } + } + } + } + $tabs[$currentKey]['current'] = 1; + self::fillCurrentTabPeekSeek($tabs, $currentKey, $autoParams); + if ($expanded) { + return ['tabs' => $tabs, 'currentKey' => $currentKey]; + } + + return $tabs; + } + + public static function fillCurrentTabPeekSeek(array &$tabs, $currentKey, array $autoParams): void + { + foreach (['peek', 'seek'] as $pick) { + $picked = $autoParams[$pick] ?? null; + if (isset($picked)) { + $pickedString = "&$pick=" . urlencode($picked); + $tabs[$currentKey]['params'] .= $pickedString; + $tabs[$currentKey]['url'] .= $pickedString; + + break; + } + } + } + + /** + * validate_input aka "first stage validation" + * This checks for valid input source data before exec + * @Exception 1XX + */ + protected function validateInput() + { + self::setValidSortTypes(); + if (empty($this->offset)) { + $this->offset = 0; + } + $check_missing = ['type', 'offset', 'limit', 'sort_type', 'sort_order']; + missing_values_to_exception($this, Exception::class, $check_missing, 600); + if (!in_array($this->type, self::$valid_types)) { + throw new Exception('Invalid $type "' . $this->type . '"', 610); + } + if ($this->offset < 0 || $this->limit < 0) { + throw new Exception('Limit integrity violation', 621); + } + if (!in_array($this->sort_type, self::$valid_sort_types)) { + throw new Exception('Invalid $sort_type "' . $this->sort_type . '"', 630); + } + if (!preg_match('/^(asc|desc)$/', $this->sort_order)) { + throw new Exception('Invalid $sort_order "' . $this->sort_order . '"', 640); + } + } + + protected static function setValidSortTypes() + { + if (getSetting('enable_likes') && !in_array('likes', self::$valid_sort_types)) { + self::$valid_sort_types[] = 'likes'; + } + } + + public function htmlOutput($tpl_list = null) + { + if (!is_array($this->output)) { + return; + } + if (is_null($tpl_list)) { + $tpl_list = $this->type ?: 'images'; + } + $directory = new RecursiveDirectoryIterator(PATH_PUBLIC_LEGACY_THEME . 'tpl_list_item/'); + $iterator = new RecursiveIteratorIterator($directory); + $regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); + $list_item_template = []; + foreach ($regex as $file) { + $file = forward_slash($file[0]); + $key = preg_replace('/\\.[^.\\s]{3,4}$/', '', str_replace(PATH_PUBLIC_LEGACY_THEME, '', $file)); + $override_file = str_replace_first(PATH_PUBLIC_LEGACY_THEME, PATH_PUBLIC_LEGACY_THEME . 'overrides/', $file); + if (is_readable($override_file)) { + $file = $override_file; + } + ob_start(); + require $file; + $file_get_contents = ob_get_contents(); + ob_end_clean(); + $list_item_template[$key] = $file_get_contents; + } + $html_output = ''; + $tpl_list = preg_replace('/s$/', '', $tpl_list); + if (function_exists('get_peafowl_item_list')) { + $render = 'get_peafowl_item_list'; + } else { + $render = 'Chevereto\Legacy\get_peafowl_item_list'; + } + $tools = $this->tools ?: []; + $requester = Login::getUser(); + foreach ($this->output as $row) { + switch ($tpl_list) { + case 'image': + case 'user/image': + case 'album/image': + default: // key thing here... + $Class = Image::class; + + break; + case 'album': + case 'user/album': + $Class = Album::class; + + break; + case 'user': + case 'user/user': + $Class = User::class; + + break; + } + $item = $Class::formatArray($row); + $html_output .= $render($item, $list_item_template, $tools, $tpl_list, $requester); + } + + return $html_output; + } + + public static function getAlbumHtml($album_id, $template = 'user/albums') + { + $listing = new Listing(); + $listing->setType('albums'); + $listing->setOffset(0); + $listing->setLimit(1); + $listing->setSortType('date'); + $listing->setSortOrder('desc'); + $listing->setWhere('WHERE album_id=:album_id'); + $listing->bind(':album_id', $album_id); + $listing->exec(); + + return $listing->htmlOutput($template); + } + + public static function getParams($request = [], bool $json_call = false) + { + self::setValidSortTypes(); + $items_per_page = getSetting('listing_items_per_page'); + $listing_pagination_mode = getSetting('listing_pagination_mode'); + $params = []; + $params['offset'] = 0; + $params['items_per_page'] = $items_per_page; + if (!$json_call && $listing_pagination_mode == 'endless') { + $params['page'] = max((int) ($request['page'] ?? 0), 1); + $params['limit'] = $params['items_per_page'] * $params['page']; + if ($params['limit'] > getSetting('listing_safe_count')) { + $listing_pagination_mode = 'classic'; + Settings::setValue('listing_pagination_mode', $listing_pagination_mode); + } + } + if (isset($request['pagination']) || $listing_pagination_mode == 'classic') { // Static single page display + $params['page'] = empty($request['page']) ? 0 : (int) ($request['page'] ?? 0) - 1; + $params['limit'] = $params['items_per_page']; + $params['offset'] = $params['page'] * $params['limit']; + } + if ($json_call) { + $params = array_merge($params, [ + 'page' => empty($request['page']) ? 0 : $request['page'] - 1, + 'limit' => $items_per_page + ]); + $params['offset'] = $params['page'] * $params['limit'] + ($request['offset'] ?? 0); + } + $default_sort = [ + 0 => 'date', + 1 => 'desc' + ]; + preg_match('/(.*)_(asc|desc)/', $request['sort'] ?? '', $sort_matches); + $params['sort'] = array_slice($sort_matches, 1); + if (count($params['sort']) !== 2) { + $params['sort'] = $default_sort; + } + if (!in_array($params['sort'][0], self::$valid_sort_types)) { + $params['sort'][0] = $default_sort[0]; + } + if (!in_array($params['sort'][1], ['asc', 'desc'])) { + $params['sort'][1] = $default_sort[1]; + } + if (!empty($request['seek'])) { + $params['seek'] = $request['seek']; + } elseif (!empty($request['peek'])) { + $params['seek'] = $request['peek']; + $params['reverse'] = true; + } + $params['page_show'] = empty($request['page']) ? null : (int) $request['page']; + + return $params; + } +} diff --git a/app/src/Legacy/Classes/LocalStorage.php b/app/src/Legacy/Classes/LocalStorage.php new file mode 100644 index 0000000..cedb883 --- /dev/null +++ b/app/src/Legacy/Classes/LocalStorage.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\G\is_writable; +use function Chevereto\Legacy\G\starts_with; +use function Chevereto\Legacy\G\unlinkIfExists; +use Exception; + +class LocalStorage +{ + private string $path; + + private string $realPath; + + private array $deleted = []; + + public function __construct(array $args = []) + { + $this->path = rtrim($args['bucket'], '/') . '/'; + $this->realPath = realpath($this->path) . '/'; + if ($this->realPath === '/') { + $this->realPath = $this->path; + } + $this->assertPath($this->realPath); + } + + public function realPath(): string + { + return $this->realPath; + } + + protected function assertPath(string $path): void + { + if (is_writable($path) === false) { + throw new Exception( + sprintf("Path %s is not writable", $path), + 600 + ); + } + } + + public function put(array $args = []): void + { + // [filename] => photo-1460378150801-e2c95cb65a50.jpg + // [source_file] => /tmp/photo-1460378150801-e2c95cb65a50.jpg + // [path] => /path/sdk/2018/08/18/ + extract($args); + $path ??= ''; + $filename ??= ''; + $source_file ??= ''; + $this->assertPath($path); + $target_filename = $path . $filename; + $target_filename = str_replace('/.\/', '/', $target_filename); + if ($source_file == $target_filename) { + return; + } + $uploaded = copy($source_file, $target_filename); + $errors = error_get_last(); + if ($uploaded == false) { + throw new Exception( + strtr("Can't move source file %source% to %destination%: %message%", [ + '%source%' => $source_file, + '%destination%' => $target_filename, + '%message%' => 'Copy error ' . $errors['type'] . ' > ' . $errors['message'], + ]), + 600 + ); + } + chmod($target_filename, 0644); + clearstatcache(); + } + + public function delete(string $filename): void + { + $filename = $this->getWorkingPath($filename); + if (file_exists($filename) == false) { + return; + } + if (unlinkIfExists($filename) == false) { + throw new Exception("Can't delete file '$filename'", 600); + } + clearstatcache(); + } + + public function deleteMultiple(array $filenames = []): void + { + $this->deleted = []; + foreach ($filenames as $v) { + $this->delete($v); + $this->deleted[] = $v; + } + } + + public function deleted(): array + { + return $this->deleted; + } + + public function mkdirRecursive(string $dirname): void + { + $dirname = $this->getWorkingPath($dirname); + if (is_dir($dirname)) { + return; + } + $path_perms = fileperms($this->realPath); + $old_umask = umask(0); + $make_pathname = mkdir($dirname, $path_perms, true); + chmod($dirname, $path_perms); + umask($old_umask); + if (!$make_pathname) { + throw new Exception('$dirname ' . $dirname . ' is not a dir', 630); + } + } + + protected function getWorkingPath(string $dirname): string + { + if (starts_with('/', $dirname) == false) { // relative thing + return $this->realPath . $dirname; + } + + return realpath($dirname); + } +} diff --git a/app/src/Legacy/Classes/Lock.php b/app/src/Legacy/Classes/Lock.php new file mode 100644 index 0000000..b145d77 --- /dev/null +++ b/app/src/Legacy/Classes/Lock.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\G\datetime_add; +use function Chevereto\Legacy\G\datetime_diff; +use function Chevereto\Legacy\G\datetimegmt; + +class Lock +{ + private string $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function create(): bool + { + $lock = DB::get('locks', ['name' => $this->name], 'AND', [], 1); + if ($lock !== false) { + $diff = datetime_diff($lock['lock_expires_gmt']); + if ($diff < 0) { + return false; + } + $this->destroy(); + } + $datetime = datetimegmt(); + $insert = DB::insert('locks', [ + 'name' => $this->name, + 'date_gmt' => $datetime, + 'expires_gmt' => datetime_add($datetime, 'PT15S'), + ]); + + return $insert !== false; + } + + public function destroy(): bool + { + DB::delete('locks', ['name' => $this->name]); + + return true; + } +} diff --git a/app/src/Legacy/Classes/Login.php b/app/src/Legacy/Classes/Login.php new file mode 100644 index 0000000..acc96b3 --- /dev/null +++ b/app/src/Legacy/Classes/Login.php @@ -0,0 +1,1191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\InvalidArgumentException; +use Chevere\Throwable\Exceptions\LogicException; +use Chevereto\Config\Config; +use function Chevereto\Encryption\decryptValues; +use function Chevereto\Encryption\encryptValues; +use function Chevereto\Encryption\hasEncryption; +use function Chevereto\Legacy\check_hashed_token; +use function Chevereto\Legacy\decodeID; +use function Chevereto\Legacy\G\datetime; +use function Chevereto\Legacy\G\datetimegmt; +use function Chevereto\Legacy\G\get_client_ip; +use function Chevereto\Legacy\G\is_valid_timezone; +use function Chevereto\Legacy\G\parse_user_agent; +use function Chevereto\Legacy\G\starts_with; +use function Chevereto\Legacy\G\str_replace_first; +use function Chevereto\Legacy\generate_hashed_token; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Vars\cookie; +use function Chevereto\Vars\cookieVar; +use function Chevereto\Vars\server; +use function Chevereto\Vars\session; +use function Chevereto\Vars\sessionVar; +use DateTime; +use DateTimeZone; +use Exception; +use Throwable; + +class Login +{ + protected static array $logged_user = []; + + protected static array $session = []; + + protected static array $providersPriorMacanudo = [ + 'facebook' => [ + 'label' => 'Facebook', + ], + 'google' => [ + 'label' => 'Google', + ], + 'twitter' => [ + 'label' => 'Twitter', + ], + 'vk' => [ + 'label' => 'VK', + ], + ]; + + public const ENCRYPTED_PROVIDER_NAMES = ['key_id', 'key_secret']; + + public const ENCRYPTED_CONNECTION_NAMES = ['token']; + + public const COOKIE = 'KEEP_LOGIN'; + + protected static array $cookies = [ + self::COOKIE => 'cookie', + self::COOKIE . '_AMAZON' => 'cookie_amazon', + self::COOKIE . '_APPLE' => 'cookie_apple', + self::COOKIE . '_BITBUCKET' => 'cookie_bitbucket', + self::COOKIE . '_DISCORD' => 'cookie_discord', + self::COOKIE . '_DRIBBBLE' => 'cookie_dribbble', + self::COOKIE . '_DROPBOX' => 'cookie_dropbox', + self::COOKIE . '_FACEBOOK' => 'cookie_facebook', + self::COOKIE . '_GITHUB' => 'cookie_github', + self::COOKIE . '_GITLAB' => 'cookie_gitlab', + self::COOKIE . '_GOOGLE' => 'cookie_google', + self::COOKIE . '_INSTAGRAM' => 'cookie_instagram', + self::COOKIE . '_LINKEDIN' => 'cookie_linkedin', + self::COOKIE . '_MAILRU' => 'cookie_mailru', + self::COOKIE . '_MEDIUM' => 'cookie_medium', + self::COOKIE . '_ORCID' => 'cookie_orcid', + self::COOKIE . '_ODNOKLASSNIKI' => 'cookie_odnoklassniki', + self::COOKIE . '_QQ' => 'cookie_qq', + self::COOKIE . '_REDDIT' => 'cookie_reddit', + self::COOKIE . '_SPOTIFY' => 'cookie_spotify', + // self::COOKIE . '_STACKEXCHANGE' => 'cookie_stackexchange', + self::COOKIE . '_STEAM' => 'cookie_steam', + self::COOKIE . '_STRAVA' => 'cookie_strava', + self::COOKIE . '_TELEGRAM' => 'cookie_telegram', + self::COOKIE . '_TUMBLR' => 'cookie_tumblr', + self::COOKIE . '_TWITCHTV' => 'cookie_twitchtv', + self::COOKIE . '_TWITTER' => 'cookie_twitter', + self::COOKIE . '_VKONTAKTE' => 'cookie_vkontakte', + self::COOKIE . '_WECHAT' => 'cookie_wechat', + self::COOKIE . '_WORDPRESS' => 'cookie_wordpress', + self::COOKIE . '_YAHOO' => 'cookie_yahoo', + self::COOKIE . '_YANDEX' => 'cookie_yandex', + //DeviantArt + //Patreon + //Paypal + //Pinterest + //Slack + ]; + + protected static bool $isPi; + + protected static bool $isMacanudo; + + public static function isPi(): bool + { + $version = Settings::get('chevereto_version_installed') ?? ''; + + return self::$isPi + ??= version_compare($version, '3.14.0.beta.1', '>='); + } + + public static function isMacanudo(): bool + { + $version = Settings::get('chevereto_version_installed') ?? ''; + + return self::$isMacanudo + ??= ( + $version === '' + ? true + : version_compare($version, '4.0.0-beta.11', '>=') + ); + } + + public static function getSocialCookieName(string $name): string + { + return array_flip(self::$cookies)['cookie_' . $name]; + } + + public static function tryLogin(): void + { + if (self::isPi()) { + self::tryCookies(); + } else { + try { + $login = false; + if (isset(cookie()['KEEP_LOGIN'])) { + $login = self::loginCookiePriorPi('internal'); + } elseif (isset(cookie()['KEEP_LOGIN_SOCIAL'])) { + $login = self::loginCookiePriorPi('social'); + } + if ($login === false && isset(session()['login'])) { + $login = self::login(session()['login']['id']); + } + } catch (Throwable $e) { + self::logoutPrePi(); + + throw new Exception($e->getMessage(), 600, $e); + } + } + } + + public static function addGuestContentToUser(array $user, int $id): void + { + if ($user === []) { + return; + } + foreach (['albums', 'images'] as $table) { + $sessionKey = 'guest_' . $table; + if (!is_array(session()[$sessionKey] ?? null)) { + continue; + } + + try { + $db = DB::getInstance(); + $getTable = DB::getTable($table); + $fieldPrefix = DB::getFieldPrefix($table); + $db->query('UPDATE ' . $getTable . ' SET ' . $fieldPrefix . '_user_id=' . $id . ' WHERE ' . $fieldPrefix . '_id IN (' . implode(',', session()[$sessionKey]) . ')'); + $db->exec(); + if ($db->rowCount() !== 0) { + DB::increment('users', [$fieldPrefix . '_count' => '+' . $db->rowCount()], ['id' => $id]); + } + } catch (Exception) { + } // Silence + sessionVar()->remove($sessionKey); + } + } + + public static function login(string|int $id, string $cookieType = 'cookie'): array + { + $id = (int) $id; + $flip = array_flip(self::$cookies); + if (!array_key_exists($cookieType, $flip)) { + throw new Exception(sprintf('Invalid login $by %s', $cookieType), 600); + } + $user = User::getSingle($id, 'id'); + self::addGuestContentToUser($user, $id); + RequestLog::delete([ + 'user_id' => $id, + 'result' => 'fail', + 'type' => 'login', + 'ip' => get_client_ip(), + ]); + if ($user['status'] == 'valid') { + self::unsetSignup(); + self::$session = [ + 'user_id' => $id, + 'type' => $cookieType, + ]; + } else { + self::setSignup([ + 'status' => $user['status'], + 'email' => $user['email'], + ]); + } + if (isset(self::getUser()['timezone']) + && self::getUser()['timezone'] !== Settings::get('default_timezone') + && is_valid_timezone($user['timezone'] ?? '') + ) { + date_default_timezone_set($user['timezone']); + } + foreach (['image_count_label', 'album_count_label'] as $v) { + $user[$v] = isset(self::$logged_user[$v]) ? _s(self::$logged_user[$v]) : ''; + } + self::$logged_user = $user; + + return self::$logged_user; + } + + public static function logout(): void + { + if (!self::isPi()) { + self::logoutPrePi(); + } + self::$logged_user = []; + self::$session = []; + self::unsetSignup(); + foreach (array_keys(self::$cookies) as $name) { + $validate = self::validateCookie($name); + if ($validate['valid']) { + DB::delete('login_cookies', ['id' => $validate['id']]); + + continue; + } + + try { + static::unsetCookie($name); + } catch (Throwable) { + } + } + } + + public static function insertCookie(string $type, string|int $userId): int + { + $values = [ + 'user_id' => $userId, + ]; + if (!self::isMacanudo()) { + return self::insertPriorMacanudo($type, $values); + } + self::assertNoSessionType($type); + $values['ip'] = get_client_ip(); + $values['date_gmt'] = datetimegmt(); + $values['user_agent'] = self::getUserAgent(); + + return self::putCookie($type, $values); + } + + public static function redirectToAfterCookie(int $userId, string $redirect): string + { + if (TwoFactor::hasFor($userId)) { + sessionVar()->put('challenge_two_factor', $userId); + $redirect = 'account/two-factor'; + } + + return $redirect; + } + + public static function insertConnection(string $provider, array $values): int + { + self::assertEnabledProvider($provider); + if (!self::isMacanudo()) { + return self::insertPriorMacanudo($provider, $values); + } + self::assertArrayWithKeys($values, ['user_id', 'resource_id', 'resource_name', 'token']); + if (!isset($values['date_gmt'])) { + $values['date_gmt'] = datetimegmt(); + } + if (is_array($values['token'])) { + $values['token'] = serialize($values['token']); + } + if (hasEncryption()) { + $values = encryptValues(self::ENCRYPTED_CONNECTION_NAMES, $values); + } + $query = << $provider, + ':user_id' => $values['user_id'], + ':date_gmt' => $values['date_gmt'], + ':resource_id' => $values['resource_id'], + ':resource_name' => $values['resource_name'], + ':token' => $values['token'], + ]); + } + + protected static function assertNoSessionType(string $type): void + { + if ($type === 'session') { + throw new LogicException( + message('Type %t is not supported') + ->withCode('%t', $type), + 600 + ); + } + } + + protected static function getUserAgent(): string + { + return json_encode(array_merge(parse_user_agent(server()['HTTP_USER_AGENT']))); + } + + protected static function putCookie(string $type, array $values): int + { + $table = 'login_cookies'; + $hashColumn = 'hash'; + if (self::isMacanudo()) { + $values['connection_id'] = 0; + } else { + $hashColumn = 'secret'; + $table = 'logins'; + $values['type'] = $type; + } + $tokenize = generate_hashed_token((int) $values['user_id']); + $values[$hashColumn] = $tokenize['hash']; + $cookieName = self::COOKIE; + $provider = self::getProviderFromCookieType($type); + if ($provider !== '') { + self::assertEnabledProvider($provider); + $cookieName .= '_' . str_replace_first('COOKIE_', '', strtoupper($provider)); + } + if (self::isMacanudo() && $type !== 'cookie') { + $query = << $provider, + ':user_id' => $values['user_id'], + ':date_gmt' => $values['date_gmt'], + ':ip' => $values['ip'], + ':user_agent' => $values['user_agent'], + ':hash' => $tokenize['hash'], + ]); + } else { + $insert = DB::insert($table, $values); + } + if ($insert !== 0) { + $dateTime = DateTime::createFromFormat( + 'Y-m-d H:i:s', + $values['date_gmt'], + new DateTimeZone('UTC') + ); + $cookie = $tokenize['public_token_format'] + . ':' + . $dateTime->getTimestamp(); + static::setCookie($cookieName, $cookie); + } + + return $insert; + } + + protected static function insertPriorMacanudo(string $type, array $values): int + { + if (!isset($values['ip'])) { + $values['ip'] = get_client_ip(); + } + if (!isset($values['hostname'])) { + $values['hostname'] = self::getUserAgent(); + } + if (!isset($values['date'])) { + $values['date'] = datetime(); + } + if (!isset($values['date_gmt'])) { + $values['date_gmt'] = datetimegmt(); + } + if (starts_with('cookie', $type)) { + return self::putCookie($type, $values); + } else { + return DB::insert('logins', $values); + } + } + + protected static function getProviderFromCookieType(string $cookieType): string + { + $provider = ''; + if ($cookieType !== 'cookie') { + $provider = str_replace_first('cookie_', '', $cookieType); + } + + return $provider; + } + + public static function getCookie(string $type, array $values): array + { + if (!starts_with('cookie', $type)) { + throw new InvalidArgumentException( + message('Type %t is not supported') + ->withCode('%t', $type), + ); + } + $provider = self::getProviderFromCookieType($type); + self::assertArrayWithKeys($values, ['user_id', 'date_gmt']); + if (!self::isMacanudo()) { + if ($provider !== '') { + self::assertEnabledProvider($provider); + } + $values['type'] = $type; + $get = self::getPriorMacanudo(values: $values, limit: 1); + + return [ + 'id' => (int) ($get['id'] ?? 0), + 'user_id' => (int) $values['user_id'], + 'hash' => ($get['secret'] ?? '') + . ($get['token_hash'] ?? ''), + ]; + } + if ($type === 'cookie') { + $values['connection_id'] = 0; + $get = DB::get(table: 'login_cookies', values: $values, limit: 1); + $get = DB::formatRows($get, 'login_cookie'); + } else { + $query = << (int) $values['user_id'], + ':date_gmt' => $values['date_gmt'], + ':name' => $provider, + ]); + } + if (!$get) { + return [ + 'id' => 0, + 'user_id' => 0, + 'hash' => '', + ]; + } + + return [ + 'id' => (int) ($get['id']), + 'user_id' => (int) $values['user_id'], + 'hash' => $get['hash'], + ]; + } + + protected static function assertEnabledProvider(string $provider): void + { + $get = self::isMacanudo() + ? DB::get( + table: 'login_providers', + values: [ + 'name' => $provider, + 'is_enabled' => 1, + ], + limit: 1 + ) + : getSetting($provider); + if (!(bool) $get) { + throw new InvalidArgumentException( + message('Provider %t is not enabled') + ->withCode('%t', $provider), + ); + } + } + + protected static function assertArrayWithKeys(array $array, array $keys): void + { + foreach ($keys as $key) { + if (!isset($array[$key])) { + throw new InvalidArgumentException( + message('Key %t is missing') + ->withCode('%t', $key), + ); + } + } + } + + public static function getUserIdForResource(string $type, int|string $resourceId): int + { + if (!self::isMacanudo()) { + $get = self::getPriorMacanudo( + values: [ + 'resource_id' => $resourceId, + 'type' => $type, + ], + sort: ['field' => 'date_gmt', 'order' => 'desc'], + limit: 1 + ); + } + $query = << $resourceId, + ':name' => $type, + ]); + + return $get['user_id'] ?? 0; + } + + public static function getUserConnections(int $userId): array + { + $connections = []; + if (self::isMacanudo()) { + $query = << $userId, + ]); + foreach ($fetchAll as &$connection) { + $connections[$connection['name']] = $connection; + } + } else { + $logins = self::getPriorMacanudo([ + 'user_id' => $userId, + ]); + $providersEnabled = self::getProviders('enabled'); + foreach ($logins as $login) { + if (!array_key_exists($login['type'], $providersEnabled)) { + continue; + } + $connections[$login['type']] = $login; + $connections[$login['type']]['label'] = $providersEnabled[$login['type']]; + } + ksort($connections); + } + + return $connections; + } + + protected static function getPriorMacanudo(array $values, array $sort = [], ?int $limit = null): array + { + $get = DB::get('logins', $values, 'AND', $sort, $limit); + if (!$get) { + return []; + } + + return DB::formatRows($get, 'login'); + } + + protected static function assertProvider(string $provider): void + { + $get = DB::get( + table: 'login_providers', + values: [ + 'name' => $provider, + ], + limit: 1 + ); + if (!$get) { + throw new InvalidArgumentException( + message('Invalid login provider %s.') + ->withCode('%s', $provider) + ); + } + } + + public static function deleteCookies(string $type, array $values): int + { + if (!self::isMacanudo()) { + $values['type'] = $type; + + return DB::delete('logins', $values); + } + if ($type === 'session') { + return 0; + } + if ($type !== 'cookie') { + $provider = str_replace_first('cookie_', '', $type); + self::assertProvider($provider); + self::assertArrayWithKeys($values, ['user_id']); + $query = << $values['user_id'], + ':provider_name' => $provider, + ]); + } + $values['connection_id'] = 0; + + return DB::delete('login_cookies', $values); + } + + public static function getConnection(int $id): array + { + $query = << $id, + ]); + if (hasEncryption()) { + $fetchSingle = decryptValues(self::ENCRYPTED_CONNECTION_NAMES, $fetchSingle); + } + $fetchSingle['token'] = unserialize($fetchSingle['token'] ?? 'a:0:{}') ?: []; + + return $fetchSingle; + } + + public static function updateConnection(int $id, array $values): int + { + if (is_array($values['token'])) { + $values['token'] = serialize($values['token']); + } + if (hasEncryption()) { + $values = encryptValues(self::ENCRYPTED_CONNECTION_NAMES, $values); + } + + return DB::update( + table: 'login_connections', + values: $values, + wheres: [ + 'id' => $id, + ] + ); + } + + public static function deleteConnection(string $provider, int|string $userId): int + { + self::assertProvider($provider); + if (!self::isMacanudo()) { + return DB::delete('logins', ['type' => $provider]); + } + $query = << strval($userId), + ':provider_name' => $provider, + ]); + } + + public static function hasSignup(): bool + { + return isset(session()['signup']) && session()['signup'] !== []; + } + + public static function getSignup(): array + { + return session()['signup']; + } + + public static function setSignup(array $var): void + { + sessionVar()->put('signup', $var); + } + + public static function unsetSignup(): void + { + if (isset(session()['signup'])) { + sessionVar()->remove('signup'); + } + } + + public static function hasSession(): bool + { + return self::$session !== []; + } + + public static function getSession(): array + { + return self::$session; + } + + public static function getUser(): array + { + return self::$logged_user; + } + + public static function setUser(string $key, mixed $value) + { + if (self::$logged_user !== []) { + self::$logged_user[$key] = $value; + } + } + + public static function isLoggedUser(): bool + { + return self::$logged_user !== []; + } + + /** + * @return array + */ + public static function validateCookie(string $cookieName) + { + if (!isset(cookie()[$cookieName])) { + return [ + 'valid' => false, + ]; + } + $fetchCookie = static::fetchCookie($cookieName); + if ($fetchCookie === []) { + return [ + 'valid' => false, + 'cookie' => [], + 'id' => null, + 'user_id' => 0 + ]; + } + /** + * $fetchCookie = [ + * 'raw' => 'asdf', + * 'user_id' => $user_id, + * 'type' => $type, + * 'date_gmt' => $date_gmt,] + */ + $login_arr = $fetchCookie; + unset($login_arr['raw'], $login_arr['type']); + /** + * $login_arr = [ + * 'user_id' => $user_id, + * 'date_gmt' => $date_gmt,] + */ + $getCookie = self::getCookie( + type: $fetchCookie['type'], + values: $login_arr, + ); + $is_valid = check_hashed_token( + $getCookie['hash'] ?? '', + $fetchCookie['raw'] + ); + + return [ + 'valid' => $is_valid, + 'cookie' => $fetchCookie, + 'id' => $getCookie['id'] ?? null, + 'user_id' => $fetchCookie['user_id'] + ]; + } + + public static function hasPassword(int $userId): bool + { + if (self::isMacanudo()) { + $get = DB::get( + table: 'login_passwords', + values: ['user_id' => $userId], + limit: 1 + ); + } else { + $get = DB::get( + table: 'logins', + values: ['user_id' => $userId, 'type' => 'password'], + limit: 1 + ); + } + + return (bool) $get; + } + + public static function checkPassword(int $userId, string $tryPassword): bool + { + if (self::isMacanudo()) { + $get = DB::get( + table: 'login_passwords', + values: ['user_id' => $userId], + limit: 1 + ); + } else { + $get = DB::get( + table: 'logins', + values: ['user_id' => $userId, 'type' => 'password'], + limit: 1 + ); + } + if (!$get) { + return false; + } + + return password_verify($tryPassword, $get['login_password_hash'] ?? $get['login_secret']); + } + + public static function addPassword( + int $userId, + string $password, + bool $update_session = true + ): bool { + return self::passwordData('insert', $userId, $password, $update_session); + } + + public static function changePassword( + int $userId, + string $password, + bool $update_session = true + ): bool { + return self::passwordData('update', $userId, $password, $update_session); + } + + public static function updateProvider(string $provider, array $values): int + { + if (hasEncryption()) { + $values = encryptValues(self::ENCRYPTED_PROVIDER_NAMES, $values); + } + + return DB::update( + table: 'login_providers', + values: $values, + wheres: ['name' => $provider] + ); + } + + public static function getProviders(string $get = 'all'): array + { + $return = []; + if (!self::isMacanudo()) { + return $get === 'all' + ? self::$providersPriorMacanudo + : self::getProvidersPriorMacanudo($get); + } + $binds = []; + $query = + << $provider) { + if ($get === 'enabled' && !getSetting($name) + || $get === 'disabled' && getSetting($name) + ) { + continue; + } + $return[$name] = $provider; + } + + return $return; + } + + public static function isAdmin(): bool + { + if (self::$logged_user === []) { + return false; + } + + return (bool) self::$logged_user['is_admin']; + } + + public static function isManager(): bool + { + if (self::$logged_user === []) { + return false; + } + + return (bool) self::$logged_user['is_manager']; + } + + /** + * @return null|array|false Null if no cookies, array if cookie+login, false if cookie+error + */ + protected static function tryCookies(): array|bool|null + { + $login = null; + foreach (array_keys(self::$cookies) as $cookieName) { + if (!array_key_exists($cookieName, cookie())) { + continue; + } + $loginCookie = self::loginCookie($cookieName); + if ($loginCookie !== []) { + $login = $loginCookie; + + break; + } + } + + return $login; + } + + /** + * @return array logged user if any + */ + protected static function loginCookie(string $cookieName = self::COOKIE): array + { + if (!array_key_exists($cookieName, self::$cookies)) { + return []; + } + $validate = self::validateCookie($cookieName); + if ($validate['valid']) { + self::login($validate['user_id'], $validate['cookie']['type']); + self::$session['id'] = $validate['id']; + self::$session['login_cookies'][] = $validate['id']; + + return self::$logged_user; + } else { + RequestLog::insert([ + 'result' => 'fail', + 'type' => 'login', + 'user_id' => $validate['user_id'] + ]); + static::unsetCookie($cookieName); + + return []; + } + } + + protected static function loginCookiePriorPi(string $type = 'internal'): array|bool|null + { + if (!in_array($type, ['internal', 'social'])) { + throw new Exception('Invalid login type'); + } + $cookie = cookie()[$type == 'internal' ? 'KEEP_LOGIN' : 'KEEP_LOGIN_SOCIAL']; + $explode = array_filter(explode(':', $cookie)); + // CHV: 0->id | 1:token | 2:timestamp + // SOC: 0->id | 1:type | 2:hash | 3:timestamp + $count = $type == 'social' ? 4 : 3; + if (count($explode) !== $count) { + return false; + } + foreach ($explode as $exp) { + if ($exp == null) { + return false; + } + } + $user_id = decodeID($explode[0]); + $login_db_arr = [ + 'user_id' => $user_id, + 'date_gmt' => gmdate('Y-m-d H:i:s', (int) end($explode)), + ]; + $getCookie = self::getCookie( + type: $type == 'internal' + ? 'cookie' + : $explode[1], + values: $login_db_arr, + ); + $is_valid_token = $type == 'internal' + ? check_hashed_token($getCookie['hash'], $cookie) + : password_verify($getCookie['hash'], $explode[2]); + if ($is_valid_token) { + return self::login( + $getCookie['user_id'], + $type == 'internal' + ? 'cookie' + : $explode[1] + ); + } else { + RequestLog::insert( + [ + 'result' => 'fail', + 'type' => 'login', + 'user_id' => $user_id] + ); + self::logoutPrePi(); + + return null; + } + } + + protected static function logoutPrePi(): void + { + self::$logged_user = []; + $doing = session()['login']['type']; + if ($doing == 'session') { + self::deleteCookies('session', [ + 'user_id' => session()['login']['id'], + 'date_gmt' => session()['login']['datetime'], + ]); + } + session_unset(); + $cookies = ['KEEP_LOGIN', 'KEEP_LOGIN_SOCIAL']; + foreach ($cookies as $cookie_name) { + static::unsetCookie($cookie_name); + if ($cookie_name == 'KEEP_LOGIN_SOCIAL') { + continue; + } + $cookie = cookie()[$cookie_name]; + $explode = array_filter(explode(':', $cookie)); + if (count($explode) == 4) { + foreach ($explode as $exp) { + if ($exp == null) { + return; + } + } + $user_id = decodeID($explode[0]); + self::deleteCookies('cookie', [ + 'user_id' => $user_id, + 'date_gmt' => gmdate('Y-m-d H:i:s', (int) $explode[3]), + ]); + } + } + } + + public static function unsetCookie(string $key): bool + { + return static::cookie($key, '', -1); + } + + protected static function setCookie(string $key, string $value): bool + { + $args = [ + $key, $value, time() + (60 * 60 * 24 * 30), + ]; + + return static::cookie(...$args); + } + + protected static function cookie(string $key, string $value, int $time): bool + { + if ($time == -1) { + cookieVar()->remove($key); + } else { + cookieVar()->put($key, $value); + } + $args = func_get_args(); + $args[] = Config::host()->hostnamePath(); + if ($time == -1) { + // PrePi + setcookie(...$args); + } + $args[] = Config::host()->hostname(); + $args[] = HTTP_APP_PROTOCOL == 'https'; + $args[] = true; + + return setcookie(...$args); + } + + protected static function fetchCookie(string $cookieName): array + { + $rawCookie = cookie()[$cookieName]; + $explode = explode(':', $rawCookie); + if (count($explode) !== 3) { + return []; + } + foreach ($explode as $exp) { + if ($exp == null) { + return []; + } + } + + return [ + 'raw' => $rawCookie, + 'user_id' => decodeID($explode[0]), + 'type' => self::$cookies[$cookieName], + 'date_gmt' => gmdate('Y-m-d H:i:s', (int) $explode[2]) + ]; + } + + protected static function passwordData( + string $action, + int $userId, + string $password, + bool $updateSession + ): bool { + $action = strtoupper($action); + if (!in_array($action, ['UPDATE', 'INSERT'])) { + throw new Exception('Expecting UPDATE or INSERT statements'); + } + $hash = password_hash($password, PASSWORD_BCRYPT); + $wheres = ['user_id' => $userId]; + if (self::isMacanudo()) { + $table = 'login_passwords'; + $values = [ + 'date_gmt' => datetimegmt(), + 'hash' => $hash, + ]; + } else { + $table = 'logins'; + $wheres['type'] = 'password'; + $values = [ + 'ip' => get_client_ip(), + 'date' => datetime(), + 'date_gmt' => datetimegmt(), + 'secret' => $hash, + ]; + } + if ($action == 'UPDATE') { + $db = DB::update($table, $values, $wheres); + } else { + $values['user_id'] = $userId; + if (!self::isMacanudo()) { + $values['type'] = 'password'; + } + $db = DB::insert($table, $values); + } + if (self::isLoggedUser() + && self::getUser()['id'] == $userId + && self::hasSession() + && $updateSession) { + self::$session = [ + 'id' => $userId, + 'type' => 'password', + ]; + } + + return (bool) $db; + } +} diff --git a/app/src/Legacy/Classes/Mailer.php b/app/src/Legacy/Classes/Mailer.php new file mode 100644 index 0000000..b204c0a --- /dev/null +++ b/app/src/Legacy/Classes/Mailer.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use PHPMailer\PHPMailer\PHPMailer; + +class Mailer extends PHPMailer +{ + public $XMailer = ' '; +} diff --git a/app/src/Legacy/Classes/Page.php b/app/src/Legacy/Classes/Page.php new file mode 100644 index 0000000..d9e719d --- /dev/null +++ b/app/src/Legacy/Classes/Page.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +class Page +{ + public static function getSingle(string $var, $by = 'url_key'): array + { + return []; + } + + public static function getAll(array $args = [], array $sort = []): array + { + return []; + } + + public static function get(array $values, array $sort = [], int $limit = null): array + { + return []; + } + + public static function getPath(?string $var = null): string + { + return PATH_PUBLIC_CONTENT_PAGES . (is_string($var) ? $var : ''); + } + + public static function getFields(): array + { + return self::$table_fields; + } + + public static function update(int $id, array $values): int + { + return 0; + } + + public static function writePage(array $args = []): bool + { + return false; + } + + public static function fill(array &$page): void + { + } + + public static function formatRowValues(mixed &$values, mixed $row = []): void + { + } + + public static function insert(array $values = []): int + { + return 0; + } + + public static function delete(array|int $page): int + { + return 0; + } +} diff --git a/app/src/Legacy/Classes/Palettes.php b/app/src/Legacy/Classes/Palettes.php new file mode 100644 index 0000000..3be81cf --- /dev/null +++ b/app/src/Legacy/Classes/Palettes.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +class Palettes +{ + private array $handles = [ + 0 => 'blanco', + 1 => 'dark', + 2 => 'flickr', + 3 => 'imgur', + 4 => 'deviantart', + 5 => 'lush', + 6 => 'graffiti', + 7 => 'abstract', + 8 => 'cheers', + 9 => 'cmyk', + ]; + + private array $names = [ + 0 => 'Blanco', + 1 => 'Dark', + 2 => 'Flickr', + 3 => 'Imgur', + 4 => 'DeviantArt', + 5 => 'Lush', + 6 => 'Graffiti', + 7 => 'Abstract', + 8 => 'Cheers', + 9 => 'CMYK', + ]; + + private array $get = []; + + private array $handlesToId = []; + + public function __construct() + { + $this->handlesToId = array_flip($this->handles); + foreach ($this->handles as $id => $handle) { + $this->get[$id] = [$handle, $this->names[$id]]; + } + } + + public function handlesToId(): array + { + return $this->handlesToId; + } + + public function get(): array + { + return $this->get; + } + + public function getHandle(int $id): string + { + return $this->get()[$id][0] ?? ''; + } + + public function getName(int $id): string + { + return $this->get()[$id][1] ?? ''; + } + + public function getIdForHandle(string $handle): int + { + return $this->handlesToId[$handle] ?? 0; + } +} diff --git a/app/src/Legacy/Classes/RequestLog.php b/app/src/Legacy/Classes/RequestLog.php new file mode 100644 index 0000000..7094a74 --- /dev/null +++ b/app/src/Legacy/Classes/RequestLog.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\G\datetime; +use function Chevereto\Legacy\G\datetimegmt; +use function Chevereto\Legacy\G\get_client_ip; + +class RequestLog +{ + public static function get($values, $sort = [], $limit = null): array + { + return DB::get('requests', $values, 'AND', $sort, $limit); + } + + public static function insert(array $values): int + { + if (defined('PHPUNIT_CHEVERETO_TESTSUITE')) { + return 0; + } + if (!isset($values['ip'])) { + $values['ip'] = get_client_ip(); + } + $values['date'] = datetime(); + $values['date_gmt'] = datetimegmt(); + + return DB::insert('requests', $values); + } + + public static function getCounts(array|string $type, string $result, ?string $ip = null): array + { + if (is_array($type)) { + $type_qry = 'request_type IN('; + $binds = []; + foreach ($type as $i => $singleType) { + $type_qry .= ':rt' . $i . ','; + $binds[':rt' . $i] = $singleType; + } + $type_qry = rtrim($type_qry, ',') . ')'; + } else { + $type_qry = 'request_type=:request_type'; + $binds = [ + ':request_type' => $type + ]; + } + + $db = DB::getInstance(); + $db->query('SELECT + COUNT(IF(request_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MINUTE), 1, NULL)) AS minute, + COUNT(IF(request_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 HOUR), 1, NULL)) AS hour, + COUNT(IF(request_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 DAY), 1, NULL)) AS day, + COUNT(IF(request_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 WEEK), 1, NULL)) AS week, + COUNT(IF(request_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MONTH), 1, NULL)) AS month + FROM ' . DB::getTable('requests') . ' WHERE ' . $type_qry . ' AND request_result=:request_result AND request_ip=:request_ip AND request_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MONTH)'); + foreach ($binds as $k => $v) { + $db->bind($k, $v); + } + $db->bind(':request_result', $result); + $db->bind(':request_ip', $ip ?: get_client_ip()); + + return $db->fetchSingle(); + } + + public static function delete($values, $clause = 'AND'): int + { + return DB::delete('requests', $values, $clause); + } +} diff --git a/app/src/Legacy/Classes/Search.php b/app/src/Legacy/Classes/Search.php new file mode 100644 index 0000000..7ae940f --- /dev/null +++ b/app/src/Legacy/Classes/Search.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\G\str_replace_first; +use Exception; + +class Search +{ + public array $display; + + public static array $excluded = ['storage', 'ip']; + + public string $DBEngine; + + public string $wheres; + + public ?string $q; + + public string $type; + + public array $request; + + public array $requester; + + public array $binds; + + public array $op; + + public function __construct() + { + $this->DBEngine = DB::queryFetchSingle("SHOW TABLE STATUS WHERE Name = '" . DB::getTable('images') . "';")['Engine']; + } + + public function build(): void + { + if (!in_array($this->type, ['images', 'albums', 'users'])) { + throw new Exception('Invalid search type', 600); + } + $as_handle = ['as_q' => null, 'as_epq' => null, 'as_oq' => null, 'as_eq' => null, 'as_cat' => 'category']; + $as_handle_admin = ['as_stor' => 'storage', 'as_ip' => 'ip']; + if ($this->requester['is_content_manager'] ?? false) { + $as_handle = array_merge($as_handle, $as_handle_admin); + } + foreach ($as_handle as $k => $v) { + if (isset($this->request[$k]) && $this->request[$k] !== '') { + $this->q .= ' ' . (isset($v) ? $v . ':' : '') . $this->request[$k]; + } + } + $this->q = trim(preg_replace(['#\"+#', '#\'+#'], ['"', '\''], $this->q ?? '')); + $search_op = $this->handleSearchOperators($this->q, $this->requester['is_content_manager'] ?? false); + $this->q = null; + foreach ($search_op as $operator) { + $this->q .= implode(' ', $operator) . ' '; + } + if (isset($this->q)) { + $this->q = preg_replace('/\s+/', ' ', trim($this->q)); + } + $q_match = $this->q; + $search_binds = []; + $search_op_wheres = []; + foreach ($search_op['named'] as $v) { + $q_match = trim(preg_replace('/\s+/', ' ', str_replace($v, '', $q_match))); + if ($q_match === '') { + $q_match = null; + } + $op = explode(':', $v); + if (!in_array($op[0], ['category', 'ip', 'storage'])) { + continue; + } + switch ($this->type) { + case 'images': + switch ($op[0]) { + case 'category': + $search_op_wheres[] = 'category_url_key = :category'; + $search_binds[] = ['param' => ':category', 'value' => $op[1]]; + + break; + + case 'ip': + $search_op_wheres[] = 'image_uploader_ip LIKE REPLACE(:ip, "*", "%")'; + $search_binds[] = ['param' => ':ip', 'value' => str_replace_first('ip:', '', $this->q)]; + + break; + + case 'storage': + if (!filter_var($op[1], FILTER_VALIDATE_INT) && !in_array($op[1], ['local', 'external'])) { + break; + } + $storage_operator_clause = [ + $op[1] => '= :storage_id', + 'local' => 'IS NULL', + 'external' => 'IS NOT NULL', + ]; + + if (filter_var($op[1], FILTER_VALIDATE_INT)) { + $search_binds[] = ['param' => ':storage_id', 'value' => $op[1]]; + } + + $search_op_wheres[] = 'image_storage_id ' . ($storage_operator_clause[$op[1]]); + + break; + } + + break; + case 'albums': + case 'users': + if ($op[0] === 'ip') { + $search_binds[] = ['param' => ':ip', 'value' => str_replace_first('ip:', '', $this->q)]; + } + + break; + } + } + if (isset($q_match)) { + $q_value = $q_match; + if ($this->DBEngine == 'InnoDB') { + $q_value = trim($q_value, '><'); + } + $search_binds[] = ['param' => ':q', 'value' => $q_value]; + } + $this->binds = $search_binds; + $this->op = $search_op; + $wheres = null; + switch ($this->type) { + case 'images': + if (isset($q_match)) { + $wheres = 'WHERE MATCH(`image_name`,`image_title`,`image_description`,`image_original_filename`) AGAINST(:q IN BOOLEAN MODE)'; + } + if ($search_op_wheres !== []) { + $wheres .= (is_null($wheres) ? 'WHERE ' : ' AND ') . implode(' AND ', $search_op_wheres); + } + + break; + case 'albums': + if (empty($search_binds)) { + $wheres = 'WHERE album_id < 0'; + } else { + $wheres = (($op[0] ?? null) == 'ip' ? 'album_creation_ip LIKE REPLACE(:ip, "*", "%")' : 'WHERE MATCH(`album_name`,`album_description`) AGAINST(:q)'); + } + + break; + case 'users': + if (empty($search_binds)) { + $wheres = 'WHERE user_id < 0'; + } elseif (($op[0] ?? null) == 'ip') { + $wheres = 'user_registration_ip LIKE REPLACE(:ip, "*", "%")'; + } else { + $clauses = [ + 'name_username' => 'WHERE MATCH(`user_name`,`user_username`) AGAINST(:q)', + 'email' => '`user_email` LIKE CONCAT("%", :q, "%")', + ]; + if ($this->requester['is_content_manager'] ?? false) { + $pos = strpos($this->q, '@'); + if ($pos !== false) { + if (preg_match_all('/\s+/', $this->q)) { + $wheres = $clauses['name_username'] + . ' OR ' . $clauses['email']; + } else { + $wheres = $clauses['email']; + } + } else { + $wheres = $clauses['name_username']; + } + } else { + $wheres = $clauses['name_username']; + } + } + + break; + } + $this->wheres = $wheres; + $this->display = [ + 'type' => $this->type, + 'q' => $this->q, + 'd' => strlen($this->q) >= 25 ? (substr($this->q, 0, 22) . '...') : $this->q, + ]; + } + + protected function handleSearchOperators(string $q, bool $full = true): array + { + $operators = ['any' => [], 'exact_phrases' => [], 'excluded' => [], 'named' => []]; + $raw_regex = [ + 'named' => '[\S]+\:[\S]+', // take all the like:this operators + 'quoted' => '-*[\"\']+.+[\"\']+', // take all the "quoted stuff" "like" "this, one" + 'spaced' => '\S+', // Take all the space separated stuff + ]; + foreach ($raw_regex as $k => $v) { + if ($k == 'spaced') { + $q = str_replace(',', '', $q); + } + if (preg_match_all('/' . $v . '/', $q, $match)) { + foreach ($match[0] as $qMatch) { + switch ($k) { + case 'named': + if (!$full) { + $named_operator = explode(':', $qMatch); + if (in_array($named_operator[0], self::$excluded)) { + continue 2; + } + } + $operators[$k][] = $qMatch; + + break; + default: + if (0 === strpos($qMatch, '-')) { + $operators['excluded'][] = $qMatch; + } elseif (0 === strpos($qMatch, '"')) { + $operators['exact_phrases'][] = $qMatch; + } else { + $operators['any'][] = $qMatch; + } + + break; + } + $q = trim(preg_replace('/\s+/', ' ', str_replace($qMatch, '', $q))); + } + } + } + + return $operators; + } +} diff --git a/app/src/Legacy/Classes/Settings.php b/app/src/Legacy/Classes/Settings.php new file mode 100644 index 0000000..9c16eba --- /dev/null +++ b/app/src/Legacy/Classes/Settings.php @@ -0,0 +1,594 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use function Chevereto\Encryption\decryptValues; +use function Chevereto\Encryption\encryptValues; +use function Chevereto\Encryption\hasEncryption; +use function Chevereto\Legacy\G\get_regex_match; +use function Chevereto\Legacy\G\is_integer; +use function Chevereto\Legacy\G\is_url; +use function Chevereto\Legacy\G\nullify_string; +use function Chevereto\Vars\env; +use Exception; + +class Settings +{ + protected static ?self $instance; + + private static array $settings = []; + + private static array $defaults = []; + + private static array $typeset = []; + + private static array $decrypted = []; + + public const ENCRYPTED_NAMES = [ + 'api_v1_key', + 'email_smtp_server', + 'email_smtp_server_password', + 'email_smtp_server_port', + 'email_smtp_server_username', + 'captcha_secret', + 'disqus_secret_key', + 'akismet_api_key', + 'moderatecontent_key', + 'arachnid_key', + 'xr_host', + 'xr_port', + 'xr_key', + ]; + + public function __construct() + { + $settings = []; + $defaults = []; + $typeset = []; + + try { + $db_settings = DB::get( + table: 'settings', + values: 'all', + sort: ['field' => 'name', 'order' => 'asc'] + ); + foreach ($db_settings as $k => $v) { + $v = DB::formatRow($v); + $value = $v['value']; + $default = $v['default']; + if ($v['typeset'] == 'bool') { + $value = $value == 1; + $default = $default == 1; + } + if ($v['typeset'] == 'string') { + $value = (string) $value; + $default = (string) $default; + } + $typeset[$v['name']] = $v['typeset']; + $settings[$v['name']] = $value; + $defaults[$v['name']] = $default; + } + } catch (Exception $e) { + $settings = []; + $defaults = []; + } + $stock = [ + 'default_language' => 'en', + 'auto_language' => true, + 'theme_download_button' => true, + 'enable_signups' => true, + 'website_mode' => 'community', + 'listing_pagination_mode' => 'classic', + 'website_content_privacy_mode' => 'default', + 'website_privacy_mode' => 'public', + 'website_explore_page' => true, + 'website_search' => true, + 'website_random' => true, + 'theme_show_social_share' => true, + 'theme_show_embed_uploader' => true, + 'user_routing' => true, + 'require_user_email_confirmation' => true, + 'require_user_email_social_signup' => true, + 'homepage_style' => 'landing', + 'user_image_avatar_max_filesize_mb' => '1', + 'user_image_background_max_filesize_mb' => '2', + 'theme_image_right_click' => false, + 'theme_show_exif_data' => true, + 'homepage_cta_color' => 'green', + 'homepage_cta_outline' => false, + 'watermark_enable_guest' => true, + 'watermark_enable_user' => true, + 'watermark_enable_admin' => true, + 'language_chooser_enable' => true, + 'languages_disable' => null, + 'homepage_cta_fn' => 'cta-upload', + 'watermark_target_min_width' => 100, + 'watermark_target_min_height' => 100, + 'watermark_percentage' => 4, + 'watermark_enable_file_gif' => false, + 'upload_medium_fixed_dimension' => 'width', + 'upload_medium_size' => 500, + 'enable_followers' => true, + 'enable_likes' => true, + 'enable_consent_screen' => false, + 'user_minimum_age' => null, + 'route_image' => 'image', + 'route_album' => 'album', + 'enable_duplicate_uploads' => false, + 'upload_threads' => '2', + 'enable_automatic_updates_check' => true, + 'comments_api' => 'js', + 'image_load_max_filesize_mb' => '3', + 'upload_max_image_width' => '0', + 'upload_max_image_height' => '0', + 'enable_expirable_uploads' => null, + 'enable_user_content_delete' => false, + 'enable_plugin_route' => true, + 'sdk_pup_url' => null, + 'website_explore_page_guest' => true, + 'explore_albums_min_image_count' => 5, + 'upload_max_filesize_mb_guest' => 0.5, + 'notify_user_signups' => false, + 'listing_viewer' => true, + 'seo_image_urls' => true, + 'seo_album_urls' => true, + // 'website_https' => 'auto', + 'upload_gui' => 'js', + 'captcha_api' => 'hcaptcha', + 'force_captcha_contact_page' => true, + 'dump_update_query' => false, + 'enable_powered_by' => true, + 'akismet' => false, + 'stopforumspam' => false, + 'upload_enabled_image_formats' => 'jpg,png,bmp,gif,webp', + 'hostname' => null, + 'theme_show_embed_content_for' => 'all', + 'moderatecontent' => false, + 'moderatecontent_key' => '', + 'moderatecontent_block_rating' => 'a', + 'moderatecontent_flag_nsfw' => 'a', + 'moderate_uploads' => '', + 'image_lock_nsfw_editing' => false, + 'enable_uploads_url' => false, + 'chevereto_news' => 'a:0:{}', + 'cron_last_ran' => '0000-00-00 00:00:00', + 'logo_type' => 'vector', + 'theme_palette' => '0', + 'enable_xr' => false, + 'xr_host' => 'localhost', + 'xr_port' => '27420', + 'xr_key' => '', + 'route_user' => 'user', + 'root_route' => 'user', + 'arachnid' => false, + 'arachnid_key' => '', + 'image_first_tab' => 'info', + 'website_random_guest' => true, + 'website_search_guest' => true, + 'debug_errors' => false, + ]; + if (env()['CHEVERETO_SERVICING'] === 'docker') { + $stock['xr_host'] = 'host.docker.internal'; + } + $device_to_columns = [ + 'phone' => 1, + 'phablet' => 3, + 'tablet' => 4, + 'laptop' => 5, + 'desktop' => 6, + ]; + foreach ($device_to_columns as $k => $v) { + $stock['listing_columns_' . $k] = $v; + } + foreach ($stock as $k => $v) { + if (!array_key_exists($k, $settings)) { + $settings[$k] = $v; + $defaults[$k] = $v; + } + } + if (isset($settings['email_mode']) && $settings['email_mode'] == 'phpmail') { + $settings['email_mode'] = 'mail'; + } + if (!in_array($settings['upload_medium_fixed_dimension'], ['width', 'height'])) { + $settings['upload_medium_fixed_dimension'] = 'width'; + } + $settings['listing_device_to_columns'] = []; + foreach (array_keys($device_to_columns) as $k) { + $settings['listing_device_to_columns'][$k] = $settings['listing_columns_' . $k]; + } + $settings['listing_device_to_columns']['largescreen'] = $settings['listing_columns_desktop']; + $settings = array_merge($settings, [ + 'username_min_length' => 3, + 'username_max_length' => 16, + 'username_pattern' => '^[\w]{3,16}$', + 'user_password_min_length' => 6, + 'user_password_max_length' => 128, + 'user_password_pattern' => '^.{6,128}$', + 'maintenance_image' => 'default/maintenance_cover.jpg', + 'ip_whois_url' => 'https://ipinfo.io/%IP', + 'available_button_colors' => ['blue', 'green', 'orange', 'red', 'grey', 'black', 'white', 'default'], + 'routing_regex' => '([\w_-]+)', + 'routing_regex_path' => '([\w\/_-]+)', + 'single_user_mode_on_disables' => ['enable_signups', 'guest_uploads', 'user_routing'], + 'listing_safe_count' => 100, + 'image_title_max_length' => 100, + 'album_name_max_length' => 100, + 'upload_available_image_formats' => 'jpg,jpeg,png,bmp,gif,webp', + ]); + if (!array_key_exists('active_storage', $settings)) { + $settings['active_storage'] = null; + } + foreach ([ + 'CHEVERETO_ENABLE_CONSENT_SCREEN' => ['0', + [ + 'enable_consent_screen' => false + ] + ], + 'CHEVERETO_ENABLE_COOKIE_COMPLIANCE' => ['0', + [ + 'enable_cookie_law' => false + ] + ], + 'CHEVERETO_ENABLE_UPLOAD_PLUGIN' => ['0', + [ + 'enable_plugin_route' => false + ] + ], + 'CHEVERETO_ENABLE_FOLLOWERS' => ['0', + [ + 'enable_followers' => false + ] + ], + 'CHEVERETO_ENABLE_LIKES' => ['0', + [ + 'enable_likes' => false + ] + ], + 'CHEVERETO_ENABLE_MODERATION' => ['0', + [ + 'moderate_uploads' => '' + ] + ], + 'CHEVERETO_ENABLE_POWERED_BY_FOOTER_SITE_WIDE' => ['1', + [ + 'enable_powered_by' => true + ] + ], + 'CHEVERETO_ENABLE_UPLOAD_FLOOD_PROTECTION' => ['0', + [ + 'flood_uploads_protection' => false + ] + ], + 'CHEVERETO_ENABLE_FAVICON' => ['0', + [ + 'favicon_image' => 'default/favicon.png', + ] + ], + 'CHEVERETO_ENABLE_LOGO' => ['0', + [ + 'logo_type' => 'vector', + 'logo_image' => 'default/logo.png', + 'logo_vector' => 'default/logo.svg', + 'theme_logo_height' => '', + ] + ], + 'CHEVERETO_ENABLE_USERS' => ['0', + [ + 'website_mode' => 'personal', + 'website_mode_personal_uid' => 1, + 'website_mode_personal_routing' => '/', + 'image_lock_nsfw_editing' => false, + 'stop_words' => '', + 'show_banners_in_nsfw' => false, + ] + ], + 'CHEVERETO_ENABLE_ROUTING' => ['0', + [ + 'route_user' => 'user', + 'root_route' => 'user', + 'route_image' => 'image', + 'route_album' => 'album', + ] + ], + 'CHEVERETO_ENABLE_CDN' => ['0', + [ + 'cdn' => false, + 'cdn_url' => '' + ] + ], + 'CHEVERETO_ENABLE_SERVICE_AKISMET' => ['0', + [ + 'akismet' => false, + 'akismet_api_key' => '' + ] + ], + 'CHEVERETO_ENABLE_SERVICE_PROJECTARACHNID' => ['0', + [ + 'arachnid' => false, + 'arachnid_key' => '' + ] + ], + 'CHEVERETO_ENABLE_SERVICE_STOPFORUMSPAM' => ['0', + [ + 'stopforumspam' => false, + ] + ], + 'CHEVERETO_ENABLE_SERVICE_MODERATECONTENT' => ['0', + [ + 'moderatecontent' => false, + 'moderatecontent_key' => '' + ] + ], + 'CHEVERETO_ENABLE_CAPTCHA' => ['0', + [ + 'captcha' => false, + 'captcha_secret' => '', + 'captcha_sitekey' => '', + 'captcha_threshold' => '', + 'force_captcha_contact_page' => false, + ] + ], + 'CHEVERETO_ENABLE_LANGUAGE_CHOOSER' => ['0', + [ + 'auto_language' => false, + 'language_chooser_enable' => false, + ] + ], + 'CHEVERETO_ENABLE_UPLOAD_WATERMARK' => ['0', + [ + 'watermark_enable_admin' => false, + 'watermark_enable_file_gif' => false, + 'watermark_enable_guest' => false, + 'watermark_enable_user' => false, + 'watermark_enable' => false, + 'watermark_image' => 'default/watermark.png', + 'watermark_margin' => '10', + 'watermark_opacity' => '50', + 'watermark_percentage' => '4', + 'watermark_position' => 'center center', + 'watermark_target_min_height' => '100', + 'watermark_target_min_width' => '100', + ] + ], + ] as $envKey => $settingValues) { + if (env()[$envKey] == $settingValues[0]) { + foreach ($settingValues[1] as $k => $v) { + $settings[$k] = $v; + } + } + } + foreach ($settings as $k => &$v) { + nullify_string($v); + } + foreach ($defaults as $k => &$v) { + nullify_string($v); + } + if (isset($settings['theme_logo_height'])) { + $settings['theme_logo_height'] = (int) $settings['theme_logo_height']; + } + if ($settings['website_mode'] == 'personal') { + if (array_key_exists('website_mode_personal_routing', $settings)) { + if (is_null($settings['website_mode_personal_routing']) || $settings['website_mode_personal_routing'] == '/') { + $settings['website_mode_personal_routing'] = '/'; + } else { + $settings['website_mode_personal_routing'] = get_regex_match($settings['routing_regex'], $settings['website_mode_personal_routing'], '#', 1); + } + } + if (!is_integer($settings['website_mode_personal_uid'])) { + $settings['website_mode_personal_uid'] = 1; + } + + foreach ($settings['single_user_mode_on_disables'] as $k) { + $settings[$k] = false; + } + $settings['enable_likes'] = false; + $settings['enable_followers'] = false; + } + if (is_null($settings['homepage_cta_fn'])) { + $settings['homepage_cta_fn'] = 'cta-upload'; + } + if ($settings['homepage_cta_fn'] == 'cta-link' && !is_url($settings['homepage_cta_fn_extra'])) { + $settings['homepage_cta_fn_extra'] = get_regex_match($settings['routing_regex_path'], $settings['homepage_cta_fn_extra'], '#', 1); + } + if (!is_null($settings['languages_disable'])) { + $languages_disable = (array) explode(',', $settings['languages_disable']); + $languages_disable = array_filter(array_unique($languages_disable)); + } else { + $languages_disable = []; + } + $settings['languages_disable'] = $languages_disable; + if (hasEncryption()) { + $settings = decryptValues(self::ENCRYPTED_NAMES, $settings); + } + self::$settings = $settings; + self::$defaults = $defaults; + self::$typeset = $typeset; + self::$instance = $this; + } + + public static function getInstance(): self + { + if (!isset(self::$instance)) { + throw new LogicException( + message('No %type% initialized') + ->withCode('%type%', static::class), + 600 + ); + } + + return self::$instance; + } + + public static function getStatic(string $var): mixed + { + $instance = self::getInstance(); + + return $instance::$$var; + } + + public static function get(?string $key = null): mixed + { + $settings = self::getStatic('settings'); + if ($key === null) { + return $settings; + } + $value = $settings[$key] ?? null; + $typeset = self::getStatic('typeset'); + + return match ($typeset[$key] ?? null) { + 'bool' => (bool) $value, + default => $value, + }; + } + + public static function getTypeset(string $key): string + { + $typeset = self::getStatic('typeset'); + + return $typeset[$key] ?? '!'; + } + + public static function hasKey(string $key): bool + { + $settings = self::getStatic('settings'); + + return array_key_exists($key, $settings); + } + + public static function getType(int|string $val): string + { + return ($val === 0 || $val === 1) ? 'bool' : 'string'; + } + + public static function getDefaults(?string $key = null): mixed + { + $defaults = self::getStatic('defaults'); + if (!is_null($key)) { + return $defaults[$key]; + } else { + return $defaults; + } + } + + public static function getDefault(string $key): mixed + { + return self::getDefaults($key); + } + + public static function setValues(array $values): void + { + self::$settings = $values; + } + + public static function setValue(string $key, mixed $value): void + { + self::$settings[$key] = $value ?? null; + } + + public static function insert(array $keyValues): bool + { + $query = ''; + $binds = []; + $table = DB::getTable('settings'); + $query_tpl = + << $v) { + $value = $plainText[$k]; + $query .= strtr( + $query_tpl, + [ + '%name' => ':n_' . $i, + '%value' => ':v_' . $i, + '%typeset' => ':t_' . $i, + ] + ); + $binds[':n_' . $i] = $k; + $binds[':v_' . $i] = $v; + $binds[':t_' . $i] = ($value === 0 || $value === 1) ? 'bool' : 'string'; + ++$i; + } + unset($i); + $db = DB::getInstance(); + $db->query($query); + foreach ($binds as $bindK => $bindV) { + $db->bind($bindK, $bindV); + } + $db->exec(); + + return true; + } + + public static function update(array $keyValues): bool + { + $query = ''; + $binds = []; + $query_tpl = 'UPDATE `' + . DB::getTable('settings') + . '` SET `setting_value` = %v WHERE `setting_name` = %k;' . "\n"; + $plainText = $keyValues; + if (hasEncryption()) { + $keyValues = encryptValues(self::ENCRYPTED_NAMES, $keyValues); + } + $i = 0; + foreach ($keyValues as $k => $v) { + self::setValue($k, $plainText[$k]); + $query .= strtr( + $query_tpl, + ['%v' => ':v_' . $i, '%k' => ':n_' . $i] + ); + $binds[':v_' . $i] = $v; + $binds[':n_' . $i] = $k; + ++$i; + } + unset($i); + $db = DB::getInstance(); + $db->query($query); + foreach ($binds as $bindK => $bindV) { + $db->bind($bindK, $bindV); + } + + return $db->exec(); + } + + /** + * @deprecate + */ + public static function getChevereto(): array + { + $api = 'https://chevereto.com/api/'; + + return [ + 'id' => '', + 'edition' => APP_NAME, + 'version' => APP_VERSION, + 'source' => [ + 'label' => 'chevereto.com', + 'url' => 'https://chevereto.com/panel/downloads', + ], + 'api' => [ + 'download' => $api . 'download', + 'get' => [ + 'info' => $api . 'get/info/4', + ], + ], + ]; + } +} diff --git a/app/src/Legacy/Classes/Stat.php b/app/src/Legacy/Classes/Stat.php new file mode 100644 index 0000000..0359979 --- /dev/null +++ b/app/src/Legacy/Classes/Stat.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Chevere\Throwable\Exceptions\OverflowException; +use function Chevereto\Legacy\G\datetimegmt; +use function Chevereto\Vars\env; +use DateTime; +use Exception; + +class Stat +{ + public static function getTotals(): array + { + $res = DB::queryFetchSingle('SELECT * FROM ' . DB::getTable('stats') . ' WHERE stat_type = "total"'); + unset($res['stat_id'], $res['stat_type'], $res['date_gmt']); + + return DB::formatRow($res, 'stat'); + } + + public static function getDaily(): array + { + $res = DB::queryFetchAll('SELECT * FROM ' . DB::getTable('stats') . ' WHERE stat_type = "date" ORDER BY stat_date_gmt DESC LIMIT 365'); + $res = DB::formatRows($res, 'stat'); + + return array_reverse($res); + } + + public static function getByDateCumulative(): array + { + $res = static::getDaily(); + $return = []; + $cumulative = [ + 'users' => 0, + 'images' => 0, + 'albums' => 0, + 'image_views' => 0, + 'album_views' => 0, + 'image_likes' => 0, + 'disk_used' => 0, + ]; + foreach ($res as $k => $v) { + foreach ($cumulative as $col => &$sum) { + $sum += $v[$col]; + $v[$col . '_acc'] = $sum; + } + $return[$v['date_gmt']] = $v; + } + + return $return; + } + + public static function assertMax(string $type): void + { + if (!in_array($type, ['images', 'albums', 'users'])) { + throw new LogicException( + message('Invalid stat type: %s') + ->withCode('%s', $type), + 600 + ); + } + $maxLimit = (int) env()['CHEVERETO_MAX_' . strtoupper($type)]; + if ($maxLimit > 0) { + $count = Stat::getTotals()[$type] ?? 0; + if (($count + 1) > $maxLimit) { + throw new OverflowException( + message('Max %t reached (limit %s)') + ->withStrtr('%t', $type) + ->withStrtr('%s', strval($maxLimit)), + 999 + ); + } + } + } + + public static function rebuildTotals(): void + { + $query = 'TRUNCATE TABLE `%table_prefix%stats`; + INSERT INTO `%table_prefix%stats` (stat_id, stat_date_gmt, stat_type) VALUES ("1", NULL, "total") ON DUPLICATE KEY UPDATE stat_type=stat_type; + UPDATE `%table_prefix%stats` SET + stat_images = (SELECT IFNULL(COUNT(*),0) FROM `%table_prefix%images`), + stat_albums = (SELECT IFNULL(COUNT(*),0) FROM `%table_prefix%albums`), + stat_users = (SELECT IFNULL(COUNT(*),0) FROM `%table_prefix%users`), + stat_image_views = (SELECT IFNULL(SUM(image_views),0) FROM `%table_prefix%images`), + stat_disk_used = (SELECT IFNULL(SUM(image_size) + SUM(image_thumb_size) + SUM(image_medium_size),0) FROM `%table_prefix%images`) + WHERE stat_type = "total"; + INSERT INTO `%table_prefix%stats` (stat_type, stat_date_gmt, stat_images, stat_image_views, stat_disk_used) + SELECT sb.stat_type, sb.stat_date_gmt, sb.stat_images, sb.stat_image_views, sb.stat_disk_used + FROM (SELECT "date" AS stat_type, DATE(image_date_gmt) AS stat_date_gmt, COUNT(*) AS stat_images, SUM(image_views) AS stat_image_views, SUM(image_size + image_thumb_size + image_medium_size) AS stat_disk_used FROM `%table_prefix%images` GROUP BY DATE(image_date_gmt)) AS sb + ON DUPLICATE KEY UPDATE stat_images = sb.stat_images; + INSERT INTO `%table_prefix%stats` (stat_type, stat_date_gmt, stat_users) + SELECT sb.stat_type, sb.stat_date_gmt, sb.stat_users + FROM (SELECT "date" AS stat_type, DATE(user_date_gmt) AS stat_date_gmt, COUNT(*) AS stat_users FROM `%table_prefix%users` GROUP BY DATE(user_date_gmt)) AS sb + ON DUPLICATE KEY UPDATE stat_users = sb.stat_users; + INSERT INTO `%table_prefix%stats` (stat_type, stat_date_gmt, stat_albums) + SELECT sb.stat_type, sb.stat_date_gmt, sb.stat_albums + FROM (SELECT "date" AS stat_type, DATE(album_date_gmt) AS stat_date_gmt, COUNT(*) AS stat_albums FROM `%table_prefix%albums` GROUP BY DATE(album_date_gmt)) AS sb + ON DUPLICATE KEY UPDATE stat_albums = sb.stat_albums; + UPDATE `%table_prefix%users` SET user_content_views = COALESCE((SELECT SUM(image_views) FROM `%table_prefix%images` WHERE image_user_id = user_id GROUP BY user_id), "0");'; + $sql = strtr($query, [ + '%table_prefix%' => env()['CHEVERETO_DB_TABLE_PREFIX'], + ]); + $db = DB::getInstance(); + $db->query($sql); + $db->exec(); + } + + public static function track(array $args = []): void + { + if (!in_array($args['action'], ['insert', 'update', 'delete'])) { + throw new Exception(sprintf('Invalid stat action "%s" in ', $args['action']), 600); + } + $tables = DB::getTables(); + if (!array_key_exists($args['table'], $tables)) { + throw new Exception(sprintf('Unknown table "%s"', $args['table']), 601); + } + if ($args['action'] === 'insert' && !in_array($args['table'], ['albums', 'images', 'likes', 'users'])) { + throw new Exception(sprintf('Table "%s" does not bind an stat procedure', $args['table']), 601); + } + if ($args['table'] == 'images' && in_array($args['action'], ['insert', 'delete'])) { + if (!isset($args['disk_sum'])) { + $disk_sum_value = 0; + } elseif (preg_match('/^([\+\-]{1})?\s*([\d]+)$/', (string) $args['disk_sum'], $matches)) { + $disk_sum_value = $matches[2]; + } else { + throw new Exception(sprintf('Invalid disk_sum value "%s"', $args['disk_sum']), 604); + } + } + if (!isset($args['value'])) { + $value = 1; + } elseif (preg_match('/^([\+\-]{1})?\s*([\d]+)$/', (string) $args['value'], $matches)) { + $value = $matches[2]; + } else { + throw new Exception(sprintf('Invalid value "%s"', $args['value']), 602); + } + if (!isset($args['date_gmt'])) { + switch ($args['action']) { + case 'insert': + case 'update': + $args['date_gmt'] = datetimegmt(); + + break; + case 'delete': + throw new Exception('Missing date_gmt value', 605); + } + } else { + $date = new DateTime($args['date_gmt']); + $args['date_gmt'] = $date->format('Y-m-d'); + } + $sql_tpl = ''; + switch ($args['action']) { + case 'insert': + switch ($args['table']) { + case 'images': + if (!isset($args['disk_sum'])) { + throw new Exception('Missing disk_sum value', 603); + } + $sql_tpl = + 'UPDATE `%table_stats` SET stat_images = stat_images + %value, stat_disk_used = stat_disk_used + %disk_sum WHERE stat_type = "total";' + . "\n" + . 'INSERT INTO `%table_stats` (stat_type, stat_date_gmt, stat_images, stat_disk_used) VALUES ("date",DATE("%date_gmt"),"%value", "%disk_sum") ON DUPLICATE KEY UPDATE stat_images = stat_images + %value, stat_disk_used = stat_disk_used + %disk_sum;'; + + break; + default: // albums, likes, users + $sql_tpl = + 'UPDATE `%table_stats` SET stat_%related_table = stat_%related_table + %value WHERE stat_type = "total";' + . "\n" + . 'INSERT `%table_stats` (stat_type, stat_date_gmt, stat_%related_table) VALUES ("date",DATE("%date_gmt"),"%value") ON DUPLICATE KEY UPDATE stat_%related_table = stat_%related_table + %value;'; + + break; + } + + break; + + case 'update': + switch ($args['table']) { + case 'images': + case 'albums': + // Track image | album | user views + $sql_tpl = + 'UPDATE `%table_stats` SET stat_%aux_views = stat_%aux_views + %value WHERE stat_type = "total";' + . "\n" + . 'INSERT INTO `%table_stats` (stat_type, stat_date_gmt, stat_%aux_views) VALUES ("date",DATE("%date_gmt"),"%value") ON DUPLICATE KEY UPDATE stat_%aux_views = stat_%aux_views + %value;'; + if (isset($args['user_id'])) { + $sql_tpl .= "\n" . 'UPDATE `%table_users` SET user_content_views = user_content_views + %value WHERE user_id = %user_id;'; + } + $sql_tpl = strtr($sql_tpl, ['%aux' => DB::getFieldPrefix($args['table'])]); + + break; + } + + break; + + case 'delete': + switch ($args['table']) { + case 'images': + $sql_tpl = + 'UPDATE `%table_stats` SET stat_images = GREATEST(stat_images - %value, 0) WHERE stat_type = "total";' + . "\n" + . 'UPDATE `%table_stats` SET stat_images = GREATEST(stat_images - %value, 0) WHERE stat_type = "date" AND stat_date_gmt = DATE("%date_gmt");' + . "\n" + . 'UPDATE `%table_stats` SET stat_image_likes = GREATEST(stat_image_likes - %likes, 0) WHERE stat_type = "total";' + . "\n" + . 'UPDATE `%table_stats` SET stat_image_likes = GREATEST(stat_image_likes - %likes, 0) WHERE stat_type = "date" AND stat_date_gmt = DATE("%date_gmt");' + . "\n" + . 'UPDATE `%table_stats` SET stat_disk_used = GREATEST(stat_disk_used - %disk_sum, 0) WHERE stat_type = "total";' + . "\n" + . 'UPDATE `%table_stats` SET stat_disk_used = GREATEST(stat_disk_used - %disk_sum, 0) WHERE stat_type = "date" AND stat_date_gmt = DATE("%date_gmt");'; + + break; + default: // albums, likes, users + $sql_tpl = + 'UPDATE `%table_stats` SET stat_%related_table = GREATEST(stat_%related_table - %value, 0) WHERE stat_type = "total";' + . "\n" + . 'UPDATE `%table_stats` SET stat_%related_table = GREATEST(stat_%related_table - %value, 0) WHERE stat_type = "date" AND stat_date_gmt = DATE("%date_gmt");'; + if ($args['table'] == 'users') { + $sql_tpl .= + // Update likes stats related to this deleted user + 'UPDATE IGNORE `%table_stats` AS S + INNER JOIN ( + SELECT DATE(like_date_gmt) AS like_date_gmt, COUNT(*) AS cnt + FROM `%table_likes` + WHERE like_user_id = %user_id + GROUP BY DATE(like_date_gmt) + ) AS L ON S.stat_date_gmt = L.like_date_gmt + SET S.stat_image_likes = GREATEST(S.stat_image_likes - COALESCE(L.cnt, "0"), 0) WHERE stat_type = "date"; + UPDATE IGNORE `%table_stats` SET stat_image_likes = GREATEST(stat_image_likes - COALESCE((SELECT COUNT(*) FROM `%table_likes` WHERE like_user_id = %user_id), "0"), 0) WHERE stat_type = "total";' + . "\n" + // Update album stats related to this deleted user + . 'UPDATE IGNORE `%table_stats` AS S + INNER JOIN ( + SELECT DATE(album_date_gmt) AS album_date_gmt, COUNT(*) AS cnt + FROM `%table_albums` + WHERE album_user_id = %user_id + GROUP BY DATE(album_date_gmt) + ) AS A ON S.stat_date_gmt = A.album_date_gmt + SET S.stat_albums = GREATEST(S.stat_albums - COALESCE(A.cnt, "0"), 0) WHERE stat_type = "date"; + UPDATE IGNORE `%table_stats` SET stat_albums = GREATEST(stat_albums - COALESCE((SELECT COUNT(*) FROM `%table_albums` WHERE album_user_id = %user_id), "0"), 0) WHERE stat_type = "total";'; + } + + break; + } + + break; + } + if ($sql_tpl === '') { + throw new LogicException(); + } + $sql = strtr($sql_tpl, [ + '%table_stats' => $tables['stats'], + '%table_users' => $tables['users'], + '%table_likes' => $tables['likes'], + '%table_albums' => $tables['albums'], + '%related_table' => (isset($args['content_type']) ? ($args['content_type'] . '_') : null) . $args['table'], + '%value' => $value, + '%date_gmt' => $args['date_gmt'], + '%user_id' => $args['user_id'] ?? 0, + '%disk_sum' => $disk_sum_value ?? 0, + '%likes' => $args['likes'] ?? 0, + ]); + DB::queryExecute($sql); + } +} diff --git a/app/src/Legacy/Classes/Storage.php b/app/src/Legacy/Classes/Storage.php new file mode 100644 index 0000000..94b077d --- /dev/null +++ b/app/src/Legacy/Classes/Storage.php @@ -0,0 +1,199 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\G\get_basename_without_extension; +use function Chevereto\Legacy\G\get_file_extension; +use function Chevereto\Legacy\G\get_filename_by_method; +use Exception; +use LogicException; + +class Storage +{ + public const ENCRYPTED_NAMES = []; + + public static function getSingle(int $var): array + { + return []; + } + + public static function get(array $values = [], array $sort = [], int $limit = null): array + { + return []; + } + + protected static function requiredByApi(int $api_id): array + { + return ['api_id', 'bucket']; + } + + public static function uploadFiles( + array|string $targets, + array|int $storage, + array $options = [] + ): array { + $pathPrefix = $options['keyprefix'] ?? ''; + if (!is_array($storage)) { + throw new LogicException('Invalid storage'); + } else { + foreach (self::requiredByApi((int) $storage['api_id']) as $k) { + if (!isset($storage[$k])) { + throw new Exception('Missing ' . $k . ' value', 600); + } + } + } + if (!isset($storage['api_type'])) { + $storage['api_type'] = 'local'; + } + $API = self::requireAPI($storage); + $files = []; + if (!empty($targets['file'])) { + $files[] = $targets; + } elseif (!is_array($targets)) { + $files = ['file' => $targets, 'filename' => basename($targets)]; + } else { + $files = $targets; + } + $disk_space_used = 0; + foreach ($files as $k => $v) { + $source_file = $v['file']; + $target_path = $API->realPath() . $pathPrefix; + if ($pathPrefix !== '') { + $API->mkdirRecursive($pathPrefix); + } + $API->put([ + 'filename' => $v['filename'], + 'source_file' => $source_file, + 'path' => $target_path, + ]); + $filesize = @filesize($v['file']); + if ($filesize === false) { + throw new Exception("Can't get filesize for " . $v['file'], 601); + } else { + $disk_space_used += $filesize; + } + $files[$k]['stored_file'] = $storage['url'] . $pathPrefix . $v['filename']; + } + + return $files; + } + + /** + * Delete files from the external storage (using queues for non anon Storages). + * + * @param string|array $targets (key, single array key, multiple array key) + * @param int|array $storage (storage id, storage array) + */ + public static function deleteFiles(string|array $targets, int|array $storage): array|bool + { + if (!is_array($storage)) { + throw new LogicException('Invalid storage'); + } else { + foreach (self::requiredByApi((int) $storage['api_id']) as $k) { + if (!isset($storage[$k])) { + throw new Exception('Missing ' . $k . ' value', 600); + } + } + } + /** @var array $storage */ + $files = []; + if (!empty($targets['key'])) { + $files[] = $targets; + } elseif (!is_array($targets)) { + $files = [['key' => $targets]]; + } else { + $files = $targets; + } + $storage_keys = []; + foreach ($files as $k => $v) { + $files[$v['key']] = $v; + $storage_keys[] = $v['key']; + unset($files[$k]); + } + $deleted = []; + foreach ($storage_keys as $key) { + self::deleteObject($key, $storage); + $deleted[] = $key; + } + + return $deleted !== [] ? $deleted : false; + } + + /** + * Delete a single file from the external storage. + * + * @param string $key representation of the object (file) to delete relative to the bucket + */ + public static function deleteObject(string $key, array $storage): void + { + self::requireAPI($storage)->delete($key); + } + + public static function test(array|int $storage): void + { + } + + public static function insert(array $values): int + { + return 0; + } + + public static function update(int $id, array $values, bool $checkCredentials = true): int + { + return 0; + } + + public static function requireAPI(array $storage): object + { + return new LocalStorage($storage); + } + + public static function getAPIRegions(string $api): array + { + return []; + } + + public static function getStorageValidFilename( + string $filename, + int $storage_id, + string $filenaming, + string $destination + ): string { + if ($filenaming == 'id') { + return $filename; + } + $extension = get_file_extension($filename); + $wanted_names = []; + for ($i = 0; $i < 25; ++$i) { + if ($i > 0 && $i < 5) { + $filenaming = $filenaming == 'random' ? 'random' : 'mixed'; + } elseif ($i > 15) { + $filenaming = 'random'; + } + $filename_by_method = get_filename_by_method($filenaming, $filename); + $wanted_names[] = get_basename_without_extension($filename_by_method); + } + $return = $wanted_names[0]; + + return isset($return) ? ($return . '.' . $extension) : self::getStorageValidFilename($filename, $storage_id, $filenaming, $destination); + } + + public static function regenStorageStats(int $storageId): string + { + return ''; + } + + public static function migrateStorage(int $sourceStorageId, int $targetStorageId): string + { + return ''; + } +} diff --git a/app/src/Legacy/Classes/StorageApis.php b/app/src/Legacy/Classes/StorageApis.php new file mode 100644 index 0000000..0ce21c5 --- /dev/null +++ b/app/src/Legacy/Classes/StorageApis.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Vars\env; + +final class StorageApis +{ + private static array $apis = [ + 8 => [ + 'name' => 'Local', + 'type' => 'local', + 'url' => '', + ], + 1 => [ + 'name' => 'Amazon S3', + 'type' => 's3', + 'url' => 'https://s3.amazonaws.com//', + ], + 9 => [ + 'name' => 'S3 compatible', + 'type' => 's3compatible', + 'url' => '', + ], + 2 => [ + 'name' => 'Google Cloud', + 'type' => 'gcloud', + 'url' => 'https://storage.googleapis.com//', + ], + + 3 => [ + 'name' => 'Microsoft Azure', + 'type' => 'azure', + 'url' => 'https://.blob.core.windows.net//', + ], + 10 => [ + 'name' => 'Alibaba Cloud OSS', + 'type' => 'oss', + 'url' => 'https://./', + ], + 6 => [ + 'name' => 'SFTP', + 'type' => 'sftp', + 'url' => '', + ], + 5 => [ + 'name' => 'FTP', + 'type' => 'ftp', + 'url' => '', + ], + 7 => [ + 'name' => 'OpenStack', + 'type' => 'openstack', + 'url' => '', + ], + 11 => [ + 'name' => 'Backblaze B2 (legacy API)', + 'type' => 'b2', + 'url' => 'https://f002.backblazeb2.com/file//', + ], + ]; + + public static function getApiId(string $type): int + { + foreach (self::$apis as $id => $api) { + if ($api['type'] === $type) { + return $id; + } + } + + return 0; + } + + public static function getEnabled(): array + { + $apis = self::$apis; + if (!(bool) env()['CHEVERETO_ENABLE_LOCAL_STORAGE']) { + unset($apis[8]); + } + + return $apis; + } + + public static function getAnon( + string $type, + string $name, + string $url, + string $bucket, + ?string $key = null, + ?string $secret = null, + ?string $region = null, + ?string $server = null, + ?string $service = null, + ?string $accountId = null, + ?string $accountName = null + ): array { + return [ + 'api_id' => self::getApiId($type), + 'name' => $name, + 'url' => rtrim($url, '/') . '/', + 'bucket' => $type == 'local' ? (rtrim($bucket, '/') . '/') : $bucket, + 'region' => $region, + 'server' => $server, + 'service' => $service, + 'account_id' => $accountId, + 'account_name' => $accountName, + 'key' => $key, + 'secret' => $secret, + 'id' => null, + 'is_https' => str_starts_with($url, 'https'), + 'is_active' => true, + 'capacity' => null, + 'space_used' => null, + ]; + } + + public static function getApiType(int $api_id): string + { + return self::$apis[$api_id]['type']; + } +} diff --git a/app/src/Legacy/Classes/TwoFactor.php b/app/src/Legacy/Classes/TwoFactor.php new file mode 100644 index 0000000..4ae2dbc --- /dev/null +++ b/app/src/Legacy/Classes/TwoFactor.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use function Chevereto\Encryption\decryptValues; +use function Chevereto\Encryption\encryptValues; +use function Chevereto\Encryption\hasEncryption; +use function Chevereto\Legacy\G\datetimegmt; +use PragmaRX\Google2FAQRCode\Google2FA; +use PragmaRX\Google2FAQRCode\QRCode\Chillerlan; +use Throwable; + +class TwoFactor +{ + public const ENCRYPTED_NAMES = [ + 'secret' + ]; + + private Google2FA $google2FA; + + private string $secret; + + public function __construct() + { + $this->google2FA = new Google2FA(); + $this->google2FA->setQrcodeService( + new Chillerlan() + ); + $this->secret = $this->google2FA->generateSecretKey(16); + } + + public function google2FA(): Google2FA + { + return $this->google2FA; + } + + public function secret(): string + { + return $this->secret; + } + + public function withSecret(string $secret): self + { + $new = clone $this; + $new->secret = $secret; + + return $new; + } + + public function getQRCodeInline( + string $company, + string $holder, + int $size = 500, + ): string { + return $this->google2FA->getQRCodeInline( + company: $company, + holder: $holder, + secret: $this->secret, + size: $size, + ); + } + + public function verify(string $userOTP): bool + { + return $this->google2FA + ->verify($userOTP, $this->secret); + } + + public function insert(int $userId): int + { + $values = [ + 'user_id' => $userId, + 'date_gmt' => datetimegmt(), + 'secret' => $this->secret, + ]; + if (hasEncryption()) { + $values = encryptValues(self::ENCRYPTED_NAMES, $values); + } + self::assertSecret($values['secret']); + + return DB::insert('two_factors', $values); + } + + public static function update(int $id, array $values): int + { + $values['date_gmt'] = datetimegmt(); + if (hasEncryption()) { + $values = encryptValues(self::ENCRYPTED_NAMES, $values); + } + self::assertSecret($values['secret']); + + return DB::update('two_factors', $values, ['id' => $id]); + } + + protected static function assertSecret(string $secret): void + { + if ($secret === '') { + throw new LogicException( + message("Secret can't be empty string"), + 600 + ); + } + } + + public static function delete(int $userId): void + { + DB::delete('two_factors', ['user_id' => $userId]); + } + + public static function hasFor(int $userId): bool + { + return self::getFor($userId) !== []; + } + + public static function get(int $id, string $by = 'id'): array + { + try { + $get = DB::get('two_factors', [$by => $id], 'AND', ['field' => 'id', 'order' => 'desc'])[0] + ?? null; + } catch (Throwable) { + return []; + } + + $return = DB::formatRow($get, 'two_factor') ?? []; + if ($return === []) { + return $return; + } + if (hasEncryption()) { + $return = decryptValues(self::ENCRYPTED_NAMES, $return); + } + self::assertSecret($return['secret']); + + return $return; + } + + public static function getFor(int $userId): array + { + return self::get($userId, 'user_id'); + } + + public static function getSecretFor(int $userId): string + { + return self::getFor($userId)['secret']; + } +} diff --git a/app/src/Legacy/Classes/Upload.php b/app/src/Legacy/Classes/Upload.php new file mode 100644 index 0000000..1156e65 --- /dev/null +++ b/app/src/Legacy/Classes/Upload.php @@ -0,0 +1,604 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Chevereto\Config\Config; +use function Chevereto\Legacy\G\add_ending_slash; +use function Chevereto\Legacy\G\ends_with; +use function Chevereto\Legacy\G\fetch_url; +use function Chevereto\Legacy\G\format_bytes; +use function Chevereto\Legacy\G\forward_slash; +use function Chevereto\Legacy\G\get_basename_without_extension; +use function Chevereto\Legacy\G\get_bytes; +use function Chevereto\Legacy\G\get_client_ip; +use function Chevereto\Legacy\G\get_file_extension; +use function Chevereto\Legacy\G\get_filename; +use function Chevereto\Legacy\G\get_image_fileinfo; +use function Chevereto\Legacy\G\get_public_url; +use function Chevereto\Legacy\G\is_animated_webp; +use function Chevereto\Legacy\G\is_image_url; +use function Chevereto\Legacy\G\is_url; +use function Chevereto\Legacy\G\is_writable; +use function Chevereto\Legacy\G\name_unique_file; +use function Chevereto\Legacy\G\unlinkIfExists; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\missing_values_to_exception; +use function Chevereto\Legacy\system_notification_email; +use function Chevereto\Vars\session; +use function Chevereto\Vars\sessionVar; +use Exception; +use Intervention\Image\ImageManagerStatic; +use PHPExif\Exif; +use stdClass; +use Throwable; + +class Upload +{ + private string $source_name; + + private string $extension; + + private array $source_image_fileinfo; + + private string $fixed_filename; + + private ?Exif $source_image_exif = null; + + private string $uploaded_file; + + private ImageConvert $ImageConvert; + + private object $moderation; + + // filename => name.ext + // file => /full/path/to/name.ext + // name => name + + private array|string $source; + + private array $uploaded = []; + + public bool $detectFlood = true; + + private array $options = []; + + private string $destination; + + private string $type; + + private string $name; + + private ?int $storage_id; + + private string $downstream; + + private string $source_filename; + + public const URL_SCHEMES = [ + 'http', + 'https', + 'ftp' + ]; + + public function __construct() + { + $this->moderation = new stdClass(); + } + + public function uploaded(): array + { + return $this->uploaded; + } + + public function source(): string|array + { + return $this->source; + } + + public function moderation(): object + { + return $this->moderation; + } + + public function checkValidUrl(string $url): void + { + $aux = strtolower($url); + $scheme = parse_url($aux, PHP_URL_SCHEME); + if (!in_array($scheme, self::URL_SCHEMES)) { + throw new LogicException( + message("Unsupported URL scheme `%scheme%`") + ->withCode('%scheme%', $scheme), + 400 + ); + } + $host = parse_url($aux, PHP_URL_HOST); + if (parse_url(Config::host()->hostname(), PHP_URL_HOST) === $host) { + throw new LogicException( + message("Unsupported self host URL upload"), + 400 + ); + } + $ip = gethostbyname($host); + $typePub = \IPLib\Range\Type::getName(\IPLib\Range\Type::T_PUBLIC); + $address = \IPLib\Factory::parseAddressString($ip); + $type = $address->getRangeType(); + $typeName = \IPLib\Range\Type::getName($type); + if ($typeName !== $typePub) { + throw new LogicException( + message("Unsupported non-public IP address for upload"), + 400 + ); + } + } + + public function setSource(array|string $source): void + { + $this->source = $source; + $this->type = (is_image_url($this->source) || is_url($this->source)) + ? 'url' + : 'file'; + if ($this->type === 'url') { + if (Settings::get('enable_uploads_url') === false) { + throw new LogicException( + message('URL uploading is disabled'), + 403 + ); + } + $this->checkValidUrl($this->source); + } + } + + public function setDestination(string $destination): void + { + $this->destination = forward_slash($destination); + } + + public function setStorageId(?int $storage_id): void + { + $this->storage_id = $storage_id; + } + + public function setFilename(string $name): void + { + $this->name = $name; + } + + public function setOptions(array $options): void + { + $this->options = $options; + } + + public function setOption(string $key, mixed $value): void + { + $this->options[$key] = $value; + } + + public static function getDefaultOptions(): array + { + return [ + 'max_size' => get_bytes('2 MB'), + 'filenaming' => 'original', + 'exif' => true, + 'allowed_formats' => self::getAvailableImageFormats(), + ]; + } + + public function exec(): void + { + $this->options = array_merge(self::getDefaultOptions(), (array) $this->options); + $this->validateInput(); // Exception 1 + $this->fetchSource(); // Exception 2 + $this->validateSourceFile(); // Exception 3 + if (!is_array($this->options['allowed_formats'])) { + $this->options['allowed_formats'] = explode(',', $this->options['allowed_formats']); + } + $this->source_name = get_basename_without_extension($this->type == 'url' ? $this->source : $this->source['name']); + $this->extension = $this->source_image_fileinfo['extension']; + if (!isset($this->name)) { + $this->name = $this->source_name; + } + $this->name = ltrim($this->name, '.'); + if (get_file_extension($this->name) == $this->extension) { + $this->name = get_basename_without_extension($this->name); + } + $this->fixed_filename = preg_replace('/(.*)\.(th|md|original|lg)\.([\w]+)$/', '$1.$3', $this->name . '.' . $this->extension); + $is_360 = false; + if ($this->extension == 'jpeg') { + $xmpDataExtractor = new XmpMetadataExtractor(); + $xmpData = $xmpDataExtractor->extractFromFile($this->downstream); + $reader = \PHPExif\Reader\Reader::factory(\PHPExif\Reader\Reader::TYPE_NATIVE); + $is_360 = false; + if (isset($xmpData['rdf:RDF']['rdf:Description']['@attributes']['ProjectionType'])) { + $is_360 = $xmpData['rdf:RDF']['rdf:Description']['@attributes']['ProjectionType'] == 'equirectangular'; + } + if (array_key_exists('exif', $this->options)) { + try { + $this->source_image_exif = $reader->read($this->downstream); + } catch (Throwable $e) { + } + if ($this->source_image_exif instanceof Exif) { + $this->source_image_exif->setFileName($this->source_filename); + if ($this->source_image_exif->getOrientation() !== false) { + ImageManagerStatic::make($this->downstream)->orientate()->save(); + } + } + if (!$this->options['exif']) { + $this->source_image_exif = null; + if (ImageManagerStatic::getManager()->config['driver'] === 'imagick') { + $img = ImageManagerStatic::make($this->downstream); + $img->getCore()->stripImage(); + $img->save(); + } else { + $img = @imagecreatefromjpeg($this->downstream); + if ($img) { + imagejpeg($img, $this->downstream, 90); + imagedestroy($img); + } else { + throw new Exception("Unable to create a new JPEG without Exif data", 644); + } + } + } + } + } + /* + * Set uploaded_file + * Local storage uploads will be allocated at the target destination + * External storage will be allocated to the temp directory + */ + if (isset($this->storage_id)) { + $this->uploaded_file = forward_slash(dirname($this->downstream)) . '/' . Storage::getStorageValidFilename($this->fixed_filename, $this->storage_id, $this->options['filenaming'], $this->destination); + } else { + $this->uploaded_file = name_unique_file($this->destination, $this->fixed_filename, $this->options['filenaming']); + } + $this->panicExtension($this->uploaded_file); + $this->source = [ + 'filename' => $this->source_filename, // file.ext + 'name' => $this->source_name, // file + 'image_exif' => $this->source_image_exif, // exif-reader + 'fileinfo' => get_image_fileinfo($this->downstream), // fileinfo array + ]; + if (stream_resolve_include_path($this->downstream) == false) { + throw new Exception('Concurrency: Downstream gone, aborting operation', 666); + } + if (stream_resolve_include_path($this->uploaded_file) != false) { + throw new Exception('Concurrency: Target uploaded file already exists, aborting operation', 666); + } + + try { + $uploaded = rename($this->downstream, $this->uploaded_file); + } catch (Throwable $e) { + $uploaded = file_exists($this->uploaded_file); + } + unlinkIfExists($this->downstream); + if (!$uploaded) { + unlinkIfExists($this->uploaded_file); + + throw new Exception("Can't move temp file to its destination", 600); + } + if (!isset($this->storage_id)) { + try { + chmod($this->uploaded_file, 0644); + } catch (Throwable $e) { + } + } + $fileinfo = get_image_fileinfo($this->uploaded_file); + $fileinfo['is_360'] = $is_360; + $this->uploaded = [ + 'file' => $this->uploaded_file, + 'filename' => get_filename($this->uploaded_file), + 'name' => get_basename_without_extension($this->uploaded_file), + 'fileinfo' => $fileinfo, + ]; + } + + public static function getAvailableImageFormats(): array + { + $formats = Settings::get('upload_available_image_formats'); + + return explode(',', $formats); + } + + public static function getEnabledImageFormats(): array + { + return Image::getEnabledImageFormats(); + } + + /** + * validate_input aka "first stage validation" + * This checks for valid input source data. + * + * @Exception 1XX + */ + protected function validateInput(): void + { + $check_missing = ['type', 'source', 'destination']; + missing_values_to_exception($this, Exception::class, $check_missing, 600); + if (!preg_match('/^(url|file)$/', $this->type)) { + throw new Exception('Invalid upload type', 610); + } + if ($this->detectFlood) { + $flood = self::handleFlood(); + if ($flood !== []) { + throw new Exception( + _s( + 'Flooding detected. You can only upload %limit% %content% per %time%', + [ + '%content%' => _n('image', 'images', $flood['limit']), + '%limit%' => $flood['limit'], + '%time%' => $flood['by'] + ] + ), + 130 + ); + } + } + if ($this->type == 'file') { + if (count($this->source) < 5) { // Valid $_FILES ? + throw new Exception('Invalid file source', 620); + } + } elseif ($this->type == 'url') { + if (!is_image_url($this->source) && !is_url($this->source)) { + throw new Exception('Invalid image URL', 622); + } + } + if (!is_dir($this->destination)) { // Try to create the missing directory + $base_dir = add_ending_slash(PATH_PUBLIC . explode('/', preg_replace('#' . PATH_PUBLIC . '#', '', $this->destination, 1))[0]); + $base_perms = fileperms($base_dir); + $old_umask = umask(0); + $make_destination = mkdir($this->destination, $base_perms, true); + chmod($this->destination, $base_perms); + umask($old_umask); + if (!$make_destination) { + throw new Exception('Destination ' . $this->destination . ' is not a dir', 630); + } + } + if (!is_readable($this->destination)) { + throw new Exception("Can't read target destination dir", 631); + } + if (!is_writable($this->destination)) { + throw new Exception("Can't write target destination dir", 632); + } + $this->destination = add_ending_slash($this->destination); + } + + public static function getTempNam(string $destination): string + { + $tempNam = @tempnam(sys_get_temp_dir(), 'chvtemp'); + if (!$tempNam || !@is_writable($tempNam)) { + $tempNam = @tempnam($destination, 'chvtemp'); + if (!$tempNam) { + throw new Exception("Can't get a tempnam", 600); + } + } + + return $tempNam; + } + + protected function panicExtension(string $filename) + { + if ( + ends_with('.php', $filename) + || ends_with('.htaccess', $filename)) { + throw new Exception(sprintf('Unwanted extension for %s', $filename), 600); + } + $extension = get_file_extension($filename); + if (!in_array($extension, self::getEnabledImageFormats())) { + throw new Exception(sprintf('Unable to handle upload for %s', $filename), 600); + } + } + + protected function fetchSource(): void + { + $this->downstream = static::getTempNam($this->destination); + if ($this->type == 'file') { + if ($this->source['error'] !== UPLOAD_ERR_OK) { + switch ($this->source['error']) { + case UPLOAD_ERR_INI_SIZE: + throw new Exception( + 'File too big (UPLOAD_ERR_INI_SIZE)', + 601 + ); + case UPLOAD_ERR_FORM_SIZE: + throw new Exception( + 'File exceeds form max size (UPLOAD_ERR_FORM_SIZE)', + 601 + ); + case UPLOAD_ERR_PARTIAL: + throw new Exception( + 'File was partially uploaded (UPLOAD_ERR_PARTIAL)', + 601 + ); + case UPLOAD_ERR_NO_FILE: + throw new Exception( + 'No file was uploaded (UPLOAD_ERR_NO_FILE)', + 601 + ); + case UPLOAD_ERR_NO_TMP_DIR: + throw new Exception( + 'Missing temp folder (UPLOAD_ERR_NO_TMP_DIR)', + 601 + ); + case UPLOAD_ERR_CANT_WRITE: + throw new Exception( + 'System write error (UPLOAD_ERR_CANT_WRITE)', + 601 + ); + case UPLOAD_ERR_EXTENSION: + throw new Exception( + 'The upload was stopped (UPLOAD_ERR_EXTENSION)', + 601 + ); + } + } + + try { + $renamed = rename($this->source['tmp_name'], $this->downstream); + } catch (Throwable $e) { + $renamed = file_exists($this->downstream); + } + if (!$renamed) { + throw new Exception('Unable to rename tmp_name to downstream', 622); + } + } elseif ($this->type == 'url') { + fetch_url($this->source, $this->downstream); + } + $this->source_filename = basename($this->type == 'file' ? $this->source['name'] : $this->source); + } + + protected function validateSourceFile(): void + { + if (!file_exists($this->downstream)) { + throw new Exception("Can't fetch target upload source (downstream)", 600); + } + $this->source_image_fileinfo = get_image_fileinfo($this->downstream); + if (!$this->source_image_fileinfo) { + throw new Exception("Can't get target upload source info", 610); + } + if ($this->source_image_fileinfo['width'] == '' || $this->source_image_fileinfo['height'] == '') { + throw new Exception('Invalid image', 400); + } + if (!in_array($this->source_image_fileinfo['extension'], self::getAvailableImageFormats())) { + throw new Exception('Unavailable image format', 613); + } + if (!in_array($this->source_image_fileinfo['extension'], $this->options['allowed_formats'])) { + throw new Exception(sprintf('Disabled image format (%s)', $this->source_image_fileinfo['extension']), 614); + } + if (!$this->isValidImageMime($this->source_image_fileinfo['mime'])) { + throw new Exception('Invalid image mimetype', 612); + } + if (!$this->options['max_size']) { + $this->options['max_size'] = self::getDefaultOptions()['max_size']; + } + if ($this->source_image_fileinfo['size'] > $this->options['max_size']) { + throw new Exception('File too big - max ' . format_bytes($this->options['max_size']), 400); + } + if ($this->source_image_fileinfo['extension'] == 'bmp') { + $this->ImageConvert = new ImageConvert($this->downstream, 'png', $this->downstream); + $this->downstream = $this->ImageConvert->out(); + $this->source_image_fileinfo = get_image_fileinfo($this->downstream); + } + if ($this->source_image_fileinfo['extension'] == 'webp' + && is_animated_webp($this->downstream) + && ImageManagerStatic::getManager()->config['driver'] === 'gd' + ) { + throw new Exception('Animated WebP is not supported', 400); + } + + if (Settings::get('arachnid')) { + $arachnid = new Arachnid( + authorization: Settings::get('arachnid_key'), + filePath: $this->downstream + ); + if ($arachnid->isSuccess()) { + $arachnid->assertIsAllowed(); + } else { + throw new Exception('Error processing Arachnid moderation: ' . $arachnid->errorMessage(), 600); + } + } + + if (Settings::get('moderatecontent') + && ( + Settings::get('moderatecontent_block_rating') != '' || + Settings::get('moderatecontent_flag_nsfw') + ) + ) { + $moderateContent = new ModerateContent($this->downstream, $this->source_image_fileinfo); + if ($moderateContent->isSuccess()) { + $this->moderation = $moderateContent->moderation(); + } else { + throw new Exception('Error processing ModerateContent: ' . $moderateContent->errorMessage(), 610); + } + } + } + + protected static function handleFlood(): array + { + if (!getSetting('flood_uploads_protection') || Login::isAdmin()) { + return []; + } + $flood_limit = []; + foreach (['minute', 'hour', 'day', 'week', 'month'] as $v) { + $flood_limit[$v] = getSetting('flood_uploads_' . $v); + } + + try { + $db = DB::getInstance(); + $flood_db = $db->queryFetchSingle( + 'SELECT + COUNT(IF(image_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MINUTE), 1, NULL)) AS minute, + COUNT(IF(image_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 HOUR), 1, NULL)) AS hour, + COUNT(IF(image_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 DAY), 1, NULL)) AS day, + COUNT(IF(image_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 WEEK), 1, NULL)) AS week, + COUNT(IF(image_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MONTH), 1, NULL)) AS month + FROM ' . DB::getTable('images') . " WHERE image_uploader_ip='" . get_client_ip() . "' AND image_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 1 MONTH)" + ); + } catch (Exception $e) { + $flood_db = []; + } // Silence + if ($flood_db === false) { + return []; + } + $is_flood = false; + $flood_by = ''; + foreach (['minute', 'hour', 'day', 'week', 'month'] as $v) { + if ($flood_limit[$v] > 0 && ($flood_db[$v] ?? 0) >= $flood_limit[$v]) { + $flood_by = $v; + $is_flood = true; + + break; + } + } + if ($is_flood) { + if (getSetting('flood_uploads_notify') && !session()['flood_uploads_notify'][$flood_by]) { + try { + $logged_user = Login::getUser(); + $message = strtr('Flooding IP %ip', ['%ip' => get_client_ip()]) . '
'; + if ($logged_user !== []) { + $message .= 'User ' . $logged_user['name'] . '
'; + } + $message .= '
'; + $message .= 'Uploads per time period
'; + $message .= 'Minute: ' . $flood_db['minute'] . '
'; + $message .= 'Hour: ' . $flood_db['hour'] . '
'; + $message .= 'Week: ' . $flood_db['day'] . '
'; + $message .= 'Month: ' . $flood_db['week'] . '
'; + system_notification_email(['subject' => 'Flood report IP ' . get_client_ip(), 'message' => $message]); + $addValues = session()['flood_uploads_notify']; + $addValues[$flood_by] = true; + sessionVar()->put('flood_uploads_notify', $addValues); + } catch (Exception $e) { + } // Silence + } + + return [ + 'flood' => true, + 'limit' => $flood_limit[$flood_by], + 'count' => $flood_db[$flood_by], + 'by' => $flood_by + ]; + } + + return []; + } + + protected function isValidImageMime(string $mime): bool + { + return preg_match("#image\/(gif|pjpeg|jpeg|png|x-png|bmp|x-ms-bmp|x-windows-bmp|webp)$#", $mime) === 1; + } + + protected function isValidNamingOption(string $string): bool + { + return in_array($string, ['mixed', 'random', 'original']); + } +} diff --git a/app/src/Legacy/Classes/User.php b/app/src/Legacy/Classes/User.php new file mode 100644 index 0000000..9033d3c --- /dev/null +++ b/app/src/Legacy/Classes/User.php @@ -0,0 +1,616 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\Classes; + +use function Chevereto\Legacy\assertNotStopWords; +use function Chevereto\Legacy\encodeID; +use function Chevereto\Legacy\G\abbreviate_number; +use function Chevereto\Legacy\G\absolute_to_relative; +use function Chevereto\Legacy\G\datetime; +use function Chevereto\Legacy\G\datetimegmt; +use function Chevereto\Legacy\G\get_base_url; +use function Chevereto\Legacy\G\get_bytes; +use function Chevereto\Legacy\G\get_client_ip; +use function Chevereto\Legacy\G\get_public_url; +use function Chevereto\Legacy\G\is_route_available; +use function Chevereto\Legacy\G\is_url_web; +use function Chevereto\Legacy\G\linkify; +use function Chevereto\Legacy\G\redirect; +use function Chevereto\Legacy\G\rrmdir; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\str_replace_first; +use function Chevereto\Legacy\G\unlinkIfExists; +use function Chevereto\Legacy\get_redirect_url; +use function Chevereto\Legacy\get_users_image_url; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Legacy\linkify_redirector; +use function Chevereto\Legacy\system_notification_email; +use function Chevereto\Vars\env; +use Exception; + +class User +{ + public static function getSingle(mixed $var, string $by = 'id', bool $pretty = true): array + { + $user_db = DB::get('users', [$by => $var], 'AND', [], 1); + if (!is_array($user_db) + || $user_db === [] + ) { + return []; + } + $connections = Login::getUserConnections($user_db['user_id']); + $aux = []; + foreach ($connections as $connection) { + $aux[$connection['name']] = $connection; + } + $user_db['user_login'] = $aux; + $user_db['user_connections_count'] = count($connections); + foreach (['user_image_count', 'user_album_count'] as $v) { + if (is_null($user_db[$v]) || $user_db[$v] < 0) { + $user_db[$v] = 0; + } + } + $user_db['user_is_admin'] ??= false; + $user_db['user_is_manager'] ??= false; + $user_db['user_is_content_manager'] = $user_db['user_is_admin'] || $user_db['user_is_manager']; + if (!array_key_exists('user_following', $user_db)) { + $user_db['user_following'] = 0; + } + if (!array_key_exists('user_followers', $user_db)) { + $user_db['user_followers'] = 0; + } + if (isset($user_db['user_name'])) { + $user_db['user_name'] = self::sanitizeUserName($user_db['user_name']); + } + if ($pretty) { + $user_db = self::formatArray($user_db); + } + + return $user_db; + } + + public static function getPrivate(): array + { + return [ + 'id' => 0, + 'name' => _s('Private profile'), + 'username' => 'private', + 'name_short' => _s('Private'), + 'url' => get_public_url(), + 'album_count' => 0, + 'image_count' => 0, + 'image_count_label' => _n('image', 'images', 0), + 'album_count_display' => 0, + 'image_count_display' => 0, + 'is_private' => true + ]; + } + + public static function getAlbums(int|array $var): array + { + $id = is_array($var) ? $var['id'] : $var; + $user_albums = []; + $user_stream = self::getStreamAlbum($var); + if (is_array($user_stream)) { + $user_albums['stream'] = $user_stream; + } + $map = []; + $children = []; + $db = DB::getInstance(); + $db->query('SELECT * FROM ' . DB::getTable('albums') . ' WHERE album_user_id=:image_user_id ORDER BY album_parent_id ASC, album_name ASC LIMIT :limit'); + $db->bind(':limit', intval(env()['CHEVERETO_MAX_USER_ALBUMS_LIST'] ?? 300)); + $db->bind(':image_user_id', $id); + $user_albums_db = $db->fetchAll(); + if ($user_albums_db) { + $user_albums += $user_albums_db; + } + foreach ($user_albums as $k => &$v) { + $album_id = isset($v['album_id']) + ? $v['album_id'] + : 'stream'; + $map[$album_id] = $k; + $parent_id = $v['album_parent_id'] ?? null; + if (isset($v['album_image_count']) && $v['album_image_count'] < 0) { + $v['album_image_count'] = 0; + } + $children[$parent_id][$album_id] = $v['album_name']; + if (isset($parent_id)) { + asort($children[$parent_id]); + } + } + if (count($children[''] ?? []) == 0) { + return []; + } + $list = []; + foreach (array_keys($children['']) as $key) { + self::iterate((string) $key, $children, $list, $user_albums, $map, 0); + } + + return $list; + } + + private static function iterate( + string $key, + array $array, + array &$list, + array $albums, + array $map, + int $level + ): void { + $album = $albums[$map[$key]]; + $album['album_indent'] = $level; + $album['album_indent_string'] = ''; + if ($level > 0) { + $album['album_indent_string'] = str_repeat('─', $level) . ' '; + } + $album = DB::formatRow($album, 'album'); + Album::fill($album); + if ($key == 'stream') { + $list[$key] = $album; + } else { + $list[] = $album; + } + if (!isset($array[$key])) { + return; + } + $level++; + foreach (array_keys($array[$key]) as $k) { + self::iterate((string) $k, $array, $list, $albums, $map, $level); + } + } + + public static function getStreamAlbum(int|array $user): ?array + { + if (!is_array($user)) { + $user = self::getSingle($user, 'id', true); + } + if ($user !== []) { + return [ + 'album_id' => null, + 'album_id_encoded' => null, + 'album_name' => _s("%s's images", $user['username']), + 'album_user_id' => $user['id'], + 'album_privacy' => 'public', + 'album_url' => $user['url'] + ]; + } + + return null; + } + + public static function getUrl(array|string $handle) + { + $username = is_array($handle) ? ($handle[isset($handle['user_username']) ? 'user_username' : 'username'] ?? null) : $handle; + $id = is_array($handle) ? ($handle[isset($handle['user_id']) ? 'user_id' : 'id'] ?? null) : null; + $path = getSetting('root_route') === 'user' + ? '' + : getSetting('route_user') . '/'; + $url = $path . $username; + if (is_array($handle) && getSetting('website_mode') == 'personal' && $id == getSetting('website_mode_personal_uid')) { + $url = getSetting('website_mode_personal_routing') !== '/' ? getSetting('website_mode_personal_routing') : ''; + } + + return get_base_url($url); + } + + public static function getUrlAlbums(string $user_url): string + { + return rtrim($user_url, '/') . '/albums'; + } + + public static function insert(array $values): int + { + Stat::assertMax('users'); + if (!isset($values['date'])) { + $values['date'] = datetime(); + } + if (!isset($values['date_gmt'])) { + $values['date_gmt'] = datetimegmt(); + } + if (!isset($values['language'])) { + $values['language'] = getSetting('default_language'); + } + if (!isset($values['timezone'])) { + $values['timezone'] = getSetting('default_timezone'); + } + if (isset($values['name'])) { + $values['name'] = self::sanitizeUserName($values['name']); + } + if (!isset($values['registration_ip'])) { + $values['registration_ip'] = get_client_ip(); + } + if (!isset($values['palette_id'])) { + $values['palette_id'] = intval(getSetting('theme_palette')); + } + assertNotStopWords($values['name'] ?? '', $values['bio'] ?? ''); + if (!Login::isAdmin()) { + $db = DB::getInstance(); + $db->query('SELECT COUNT(*) c FROM ' . DB::getTable('users') . ' WHERE user_registration_ip=:ip AND user_status != "valid" AND user_date_gmt >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 2 DAY)'); + $db->bind(':ip', $values['registration_ip']); + if ($db->fetchSingle()['c'] > 5) { + throw new Exception('Flood detected', 666); + } + } + $user_id = DB::insert('users', $values); + if (!Login::isAdmin() && Settings::get('notify_user_signups')) { + $message = implode('
', [ + 'A new user has just signed up %user (%edit)', + '', + 'Username: %username', + 'Email: %email', + 'Status: %status', + 'IP: %registration_ip', + 'Date (GMT): %date_gmt', + '', + 'You can disable these notifications on %configure' + ]); + foreach (['username', 'email', 'status', 'registration_ip', 'date_gmt'] as $k) { + $table['%' . $k] = $values[$k] ?? ''; + } + $table['%edit'] = 'edit'; + $table['%user'] = '' . $values['username'] . ''; + $table['%configure'] = 'dashboard/settings/users'; + system_notification_email([ + 'subject' => sprintf('New user signup %s', $values['username']), + 'message' => strtr($message, $table), + ]); + } + Stat::track([ + 'action' => 'insert', + 'table' => 'users', + 'value' => '+1', + 'date_gmt' => $values['date_gmt'], + 'user_id' => $user_id, + ]); + + return $user_id; + } + + public static function update(int|string $id, array $values): int + { + if (isset($values['name'])) { + $values['name'] = self::sanitizeUserName($values['name']); + } + assertNotStopWords($values['name'] ?? '', $values['bio'] ?? ''); + + return DB::update('users', $values, ['id' => (int) $id]); + } + + public static function uploadPicture(int|array $user, string $type, array|string $source): ?array + { + $type = strtolower($type); + if (!in_array($type, ['background', 'avatar'])) { + throw new Exception('Invalid upload type', 600); + } + if (!is_array($user)) { + $user = self::getSingle($user, 'id'); + } + if ($user === []) { + throw new Exception("target user doesn't exists", 601); + } + $localPath = PATH_PUBLIC_CONTENT_IMAGES_USERS . $user['id_encoded'] . '/'; + $storagePath = ltrim(absolute_to_relative($localPath), '/'); + $image_upload = Image::upload( + $source, + $localPath, + ($type == 'avatar' ? 'av' : 'bkg') . '_' . strtotime(datetimegmt()), + ['max_size' => get_bytes(Settings::get('user_image_' . $type . '_max_filesize_mb') . ' MB')] + ); + /** @var array $uploaded */ + $uploaded = $image_upload['uploaded']; + if ($type == 'avatar') { + $max_res = ['width' => 500, 'height' => 500]; + $must_resize = $uploaded['fileinfo']['width'] > $max_res['width'] + || $uploaded['fileinfo']['height'] > $max_res['height']; + } else { + $max_res = ['width' => 1920]; + $must_resize = $uploaded['fileinfo']['width'] > $max_res['width']; + $medium = Image::resize( + $uploaded['file'], + null, + $uploaded['name'] . '.md', + [ + 'width' => 500, + 'over_resize' => true, + ] + ); + $toStorage[] = [ + 'file' => $medium['file'], + 'filename' => $medium['filename'], + 'mime' => $medium['fileinfo']['mime'], + ]; + } + if ($must_resize) { + $uploaded = Image::resize($uploaded['file'], null, null, $max_res); + } + $toStorage[] = [ + 'file' => $uploaded['file'], + 'filename' => $uploaded['filename'], + 'mime' => $uploaded['fileinfo']['mime'], + ]; + $toDelete = []; + $convert = new ImageConvert($uploaded['file'], 'jpg', $uploaded['file'], 90); + $uploaded['file'] = $convert->out(); + $user_edit = self::update($user['id'], [$type . '_filename' => $uploaded['filename']]); + $assetStorage = AssetStorage::getStorage(); + if ($user_edit !== 0) { + AssetStorage::uploadFiles($toStorage, ['keyprefix' => $storagePath]); + if (isset($user[$type])) { + $image_path = $storagePath . $user[$type]['filename']; + if ($type == 'background') { + $pathinfo = pathinfo($image_path); + $image_md_path = str_replace($pathinfo['basename'], $pathinfo['filename'] . '.md.' . $pathinfo['extension'], $image_path); + $toDelete[] = ['key' => $image_md_path]; + } + $toDelete[] = ['key' => $image_path]; + } + if ($toDelete !== []) { + AssetStorage::deleteFiles($toDelete); + } + } + if (!AssetStorage::isLocalLegacy()) { + $toUnlink = [$uploaded['file']]; + if ($type == 'background') { + $pathinfo = pathinfo($uploaded['file']); + $image_md_path = str_replace($pathinfo['basename'], $pathinfo['filename'] . '.md.' . $pathinfo['extension'], $uploaded['file']); + $toUnlink[] = $image_md_path; + } + foreach ($toDelete as $delete) { + $toUnlink[] = PATH_PUBLIC . $delete['key']; + } + foreach ($toUnlink as $remove) { + unlinkIfExists($remove); + } + } + $uploaded['fileinfo']['url'] = str_replace_first( + URL_APP_PUBLIC, + $assetStorage['url'], + $uploaded['fileinfo']['url'] + ); + + return $uploaded['fileinfo']; + } + + public static function deletePicture(int|array $user, string $deleting): bool + { + $deleting = strtolower($deleting); + if (!in_array($deleting, ['background', 'avatar'])) { + throw new Exception('Invalid delete type', 600); + } + if (!is_array($user)) { + $user = self::getSingle($user, 'id', true); + } + if ($user === []) { + throw new Exception("Target user doesn't exists", 601); + } + if (!$user[$deleting]) { + throw new Exception('user ' . $deleting . " doesn't exists", 602); + } + $localPath = PATH_PUBLIC_CONTENT_IMAGES_USERS . $user['id_encoded'] . '/'; + $storagePath = ltrim(absolute_to_relative($localPath), '/'); + $toDelete = []; + $image_path = $storagePath . $user[$deleting]['filename']; + if ($deleting == 'background') { + $pathinfo = pathinfo($image_path); + $image_md_path = str_replace($pathinfo['basename'], $pathinfo['filename'] . '.md.' . $pathinfo['extension'], $image_path); + $toDelete[] = ['key' => $image_md_path]; + } + $toDelete[] = ['key' => $image_path]; + AssetStorage::deleteFiles($toDelete); + self::update($user['id'], [$deleting . '_filename' => null]); + + return true; + } + + public static function delete(int|array $user): void + { + if (!is_array($user)) { + $user = self::getSingle($user, 'id', true); + } + if ($user === []) { + return; + } + $user_images_path = PATH_PUBLIC_CONTENT_IMAGES_USERS . $user['id_encoded']; + rrmdir($user_images_path); + $db = DB::getInstance(); + $db->query('SELECT image_id FROM ' . DB::getTable('images') . ' WHERE image_user_id=:image_user_id'); + $db->bind(':image_user_id', $user['id']); + $user_images = $db->fetchAll(); + foreach ($user_images as $user_image) { + Image::delete((int) $user_image['image_id']); + } + Notification::delete([ + 'table' => 'users', + 'user_id' => $user['id'], + ]); + Stat::track([ + 'action' => 'delete', + 'table' => 'users', + 'value' => '-1', + 'user_id' => $user['id'], + 'date_gmt' => $user['date_gmt'] + ]); + $sql = strtr('UPDATE `%table_users` SET user_likes = user_likes - COALESCE((SELECT COUNT(*) FROM `%table_likes` WHERE like_user_id = %user_id AND user_id = like_content_user_id AND like_user_id <> like_content_user_id GROUP BY like_content_user_id),"0");', [ + '%table_users' => DB::getTable('users'), + '%table_likes' => DB::getTable('likes'), + '%user_id' => $user['id'], + ]); + DB::queryExecute($sql); + $sql = strtr('UPDATE `%table_users` SET user_followers = user_followers - COALESCE((SELECT 1 FROM `%table_follows` WHERE follow_user_id = %user_id AND user_id = follow_followed_user_id AND follow_user_id <> follow_followed_user_id GROUP BY follow_followed_user_id),"0");', [ + '%table_users' => DB::getTable('users'), + '%table_follows' => DB::getTable('follows'), + '%user_id' => $user['id'], + ]); + DB::queryExecute($sql); + $sql = strtr('UPDATE `%table_users` SET user_following = user_following - COALESCE((SELECT 1 FROM `%table_follows` WHERE follow_followed_user_id = %user_id AND user_id = follow_user_id AND follow_user_id <> follow_followed_user_id GROUP BY follow_user_id),"0");', [ + '%table_users' => DB::getTable('users'), + '%table_follows' => DB::getTable('follows'), + '%user_id' => $user['id'], + ]); + DB::queryExecute($sql); + DB::delete('albums', ['user_id' => $user['id']]); + DB::delete('images', ['user_id' => $user['id']]); + DB::delete('login_connections', ['user_id' => $user['id']]); + DB::delete('login_cookies', ['user_id' => $user['id']]); + DB::delete('login_passwords', ['user_id' => $user['id']]); + DB::delete('likes', ['user_id' => $user['id']]); + DB::delete('follows', ['user_id' => $user['id'], 'followed_user_id' => $user['id']], 'OR'); + DB::delete('users', ['id' => $user['id']]); + } + + public static function statusRedirect(?string $status): void + { + if ($status === null) { + return; + } + if ($status !== 'valid') { + if ($status == 'awaiting-email') { + $status = 'email-needed'; + } + redirect('account/' . $status); + } + } + + public static function isValidUsername(string $string): bool + { + $restricted = [ + 'tag', 'tags', + 'categories', + 'profile', + 'messages', + 'map', + 'feed', + 'events', + 'notifications', + 'discover', + 'upload', + 'following', 'followers', + 'flow', 'trending', 'popular', 'fresh', 'upcoming', 'editors', 'profiles', + 'activity', 'upgrade', 'account', + 'affiliates', 'billing', + 'do', 'go', 'redirect', + 'api', 'sdk', 'plugin', 'plugins', 'tools', + 'external', + 'importer', 'import', 'exporter', 'export', + ]; + $virtual_routes = ['image', 'album']; + foreach ($virtual_routes as $k) { + $restricted[] = getSetting('route_' . $k); + } + + return preg_match('/' . getSetting('username_pattern') . '/', $string) === 1 && !in_array($string, $restricted) && !is_route_available($string) && !file_exists(PATH_PUBLIC . $string); + } + + public static function formatArray(array $object): array + { + if ($object !== []) { + $output = DB::formatRow($object); + self::fill($output); + + return $output; + } + + return $object; + } + + public static function fill(array &$user): void + { + $user['palette_id'] = (int) ($user['palette_id'] ?? 0); + $user['id_encoded'] = encodeID((int) ($user['id'] ?? 0)); + $user['image_count_display'] = isset($user['image_count']) ? abbreviate_number($user['image_count']) : 0; + $user['album_count_display'] = isset($user['album_count']) ? abbreviate_number($user['album_count']) : 0; + $user['url'] = self::getUrl($user); + $user['url_albums'] = self::getUrlAlbums($user['url']); + $user['url_liked'] = $user['url'] . '/liked'; + $user['url_following'] = $user['url'] . '/following'; + $user['url_followers'] = $user['url'] . '/followers'; + if (isset($user['website']) && !is_url_web($user['website'])) { + unset($user['website']); + } + if (isset($user['website'])) { + $user['website_safe_html'] = safe_html($user['website']); + $user['website_display'] = $user['is_admin'] ? $user['website_safe_html'] : get_redirect_url($user['website_safe_html']); + } + if (isset($user['bio'])) { + $user['bio_safe_html'] = safe_html($user['bio']); + $user['bio_linkify'] = $user['is_admin'] + ? linkify($user['bio_safe_html'], ['attr' => ['target' => '_blank']]) + : linkify_redirector($user['bio_safe_html']); + } + $user['name'] ??= ucfirst($user['username'] ?? ''); + foreach (['image_count', 'album_count'] as $v) { + $single = $v == 'image_count' ? 'image' : 'album'; + $plural = $v == 'image_count' ? 'images' : 'albums'; + $user[$v . '_label'] = _n($single, $plural, $user[$v] ?? 0); + } + $name_array = explode(' ', $user['name'] ?? ''); + $user['firstname'] = mb_strlen($name_array[0]) > 20 ? trim(mb_substr($name_array[0], 0, 20, 'UTF-8')) : $name_array[0]; + $user['firstname_html'] = safe_html(strip_tags($user['firstname'])); + $user['name_short'] = mb_strlen($user['name']) > 20 ? $user['firstname'] : $user['name']; + $user['name_html'] = safe_html(strip_tags($user['name'])); + $user['name_short_html'] = safe_html(strip_tags($user['name_short'])); + if (isset($user['avatar_filename'])) { + $avatar_file = $user['id_encoded'] . '/' . $user['avatar_filename']; + $user['avatar'] = [ + 'filename' => $user['avatar_filename'], + 'url' => get_users_image_url($avatar_file) + ]; + } + unset($user['avatar_filename']); + if (isset($user['background_filename'])) { + $background_file = $user['id_encoded'] . '/' . $user['background_filename']; + $background_path = PATH_PUBLIC_CONTENT_IMAGES_USERS . $background_file; + $pathinfo = pathinfo($background_path); + $background_md_file = $user['id_encoded'] . '/' . $pathinfo['filename'] . '.md.' . $pathinfo['extension']; + $user['background'] = [ + 'filename' => $user['background_filename'], + 'url' => get_users_image_url($user['id_encoded'] . '/' . $user['background_filename']), + 'medium' => [ + 'filename' => $pathinfo['basename'], + 'url' => get_users_image_url($background_md_file) + ] + ]; + } + unset($user['background_filename'], $user['facebook_username']); + if (isset($user['twitter_username'])) { + $user['twitter'] = [ + 'username' => $user['twitter_username'], + 'url' => 'http://twitter.com/' . $user['twitter_username'] + ]; + } + unset($user['twitter_username']); + if (!isset($user['notifications_unread'])) { + $user['notifications_unread'] = 0; + } + $user['notifications_unread_display'] = $user['notifications_unread'] > 10 ? '+10' : $user['notifications_unread']; + } + + public static function sanitizeUserName(string $name): string + { + return preg_replace('#<|>#', '', $name); + } + + public static function cleanUnconfirmed(int $limit = null): void + { + $db = DB::getInstance(); + $query = 'SELECT * FROM ' . DB::getTable('users') . ' WHERE user_status IN ("awaiting-confirmation", "awaiting-email") AND user_date_gmt <= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 2 DAY) ORDER BY user_id DESC'; + if (is_int($limit)) { + $query .= ' LIMIT ' . $limit; + } + $db->query($query); + $users = $db->fetchAll(); + foreach ($users as $user) { + $user = self::formatArray($user); + self::delete($user); + } + } +} diff --git a/app/src/Legacy/Classes/XmpMetadataExtractor.php b/app/src/Legacy/Classes/XmpMetadataExtractor.php new file mode 100644 index 0000000..bcc6753 --- /dev/null +++ b/app/src/Legacy/Classes/XmpMetadataExtractor.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + +namespace Chevereto\Legacy\Classes; + +use DOMDocument; +use JeroenDesloovere\XmpMetadataExtractor\XmpMetadataExtractor as Base; +use Throwable; + +class XmpMetadataExtractor extends Base +{ + public function extractFromContent(string $content): array + { + try { + $string = $this->getXmpXmlString($content); + if ($string == '') { + return []; + } + $doc = new DOMDocument(); + $doc->loadXML($string); + $root = $doc->documentElement; + $output = $this->convertDomNodeToArray($root); + $output['@root'] = $root->tagName; + + return $output; + } catch (Throwable $e) { + return []; + } + } + + protected function getXmpXmlString(string $content): string + { + $xmpDataStart = strpos($content, ''); + $xmpLength = $xmpDataEnd - $xmpDataStart; + + return substr($content, $xmpDataStart, $xmpLength + 12); + } +} diff --git a/app/src/Legacy/G/DB.php b/app/src/Legacy/G/DB.php new file mode 100644 index 0000000..c17659c --- /dev/null +++ b/app/src/Legacy/G/DB.php @@ -0,0 +1,458 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\G; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use function Chevereto\Vars\env; +use Exception; +use PDO; +use PDOStatement; + +class DB +{ + private array $pdo_default_attrs = []; + + private static ?self $instance; + + private array $pdo_options = []; + + public static PDO $dbh; + + public PDOStatement $query; + + public function __construct( + private string $host, + private int $port, + private string $name, + private string $user, + private string $pass, + private string $driver, + private array $pdoAttrs, + private string $tablePrefix, // @phpstan-ignore-line + ) { + if (isset(self::$dbh)) { + return; + } + $pdo_connect = $this->driver . ':host=' . $this->host . ';dbname=' . $this->name; + if (isset($this->port)) { + $pdo_connect .= ';port=' . $this->port; + } + $this->pdo_default_attrs = [ + PDO::ATTR_TIMEOUT => 30, + ]; + $this->pdo_options = $this->pdo_default_attrs + $this->pdoAttrs; + $this->pdo_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; + $this->pdo_options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET time_zone = '+00:00', NAMES 'utf8mb4'"; + self::$dbh = new PDO($pdo_connect, $this->user, $this->pass, $this->pdo_options); + self::$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + self::$instance = $this; + } + + public static function fromEnv() + { + new self( + host: env()['CHEVERETO_DB_HOST'], + port: (int) env()['CHEVERETO_DB_PORT'], + name: env()['CHEVERETO_DB_NAME'], + user: env()['CHEVERETO_DB_USER'], + pass: env()['CHEVERETO_DB_PASS'], + driver: env()['CHEVERETO_DB_DRIVER'], + pdoAttrs: json_decode( + env()['CHEVERETO_DB_PDO_ATTRS'], + true + ), + tablePrefix: env()['CHEVERETO_DB_TABLE_PREFIX'], + ); + } + + public static function hasInstance(): bool + { + return isset(self::$instance); + } + + public static function getInstance(): self + { + if (!isset(self::$instance)) { + throw new LogicException( + message('No %type% initialized') + ->withCode('%type%', static::class) + ); + } + + return self::$instance; + } + + public function setPDOAttrs(array $attributes): void + { + $this->pdo_options = $attributes; + } + + public function setPDOAttr(string $key, string $value): void + { + $this->pdo_options[$key] = $value; + } + + public function getAttr($attr): mixed + { + return self::$dbh->getAttribute($attr); + } + + public function query(string $query): void + { + $this->query = self::$dbh->prepare($query); + } + + public function errorInfo(): array + { + return self::$dbh->errorInfo(); + } + + public function bind(mixed $param, mixed $value, int $type = null): void + { + if (is_null($type)) { + switch (true) { + case is_int($value): + $type = PDO::PARAM_INT; + + break; + case is_bool($value): + $type = PDO::PARAM_BOOL; + + break; + case is_null($value): + $type = PDO::PARAM_NULL; + + break; + case is_resource($value): + $type = PDO::PARAM_LOB; + + break; + default: + $type = PDO::PARAM_STR; + + break; + } + } + $this->query->bindValue($param, $value, $type); + } + + public function exec(): bool + { + return $this->query->execute(); + } + + public function fetchColumn(): mixed + { + return $this->query->fetchColumn(); + } + + public function closeCursor(): bool + { + return $this->query->closeCursor(); + } + + public function fetchAll(int $mode = PDO::FETCH_ASSOC): array|false + { + $this->exec(); + + return $this->query->fetchAll($mode); + } + + public function fetchSingle(int $mode = PDO::FETCH_ASSOC): mixed + { + $this->exec(); + + return $this->query->fetch($mode); + } + + /** + * @param string $query Raw query to execute. + * @return int Number of rows affected. + */ + public static function queryExecute(string $query): int + { + $db = self::getInstance(); + $db->query($query); + + return $db->exec() ? $db->rowCount() : 0; + } + + /** + * @param string $query Prepared query to execute. + * @param array $binds Parameters to bind to the query `[:param => replace]`. + * @return int Number of rows affected. + */ + public static function preparedQueryExecute(string $query, array $binds): int + { + $db = self::dbPrepare($query, $binds); + + return $db->exec() ? $db->rowCount() : 0; + } + + public static function queryFetchSingle(string $query, $fetch_style = null): array|false + { + return self::queryFetch($query, 1, $fetch_style); + } + + public static function queryFetchAll(string $query, $fetch_style = null): array|false + { + return self::queryFetch($query, 0, $fetch_style); + } + + public static function queryFetch(string $query, int $limit = 1, ?int $fetch_style = null): array|false + { + $db = self::getInstance(); + $db->query($query); + if ($fetch_style === null) { + $fetch_style = PDO::FETCH_ASSOC; + } + + return $limit == 1 + ? $db->fetchSingle($fetch_style) + : $db->fetchAll($fetch_style); + } + + public function rowCount(): int + { + return $this->query->rowCount(); + } + + public function lastInsertId() + { + return self::$dbh->lastInsertId(); + } + + public function beginTransaction() + { + return self::$dbh->beginTransaction(); + } + + public function endTransaction() + { + return self::$dbh->commit(); + } + + public function cancelTransaction() + { + return self::$dbh->rollBack(); + } + + public static function getTable(string $table) + { + return env()['CHEVERETO_DB_TABLE_PREFIX'] . $table; + } + + public static function get( + array|string $table, + array|string $values, + string $clause = 'AND', + array $sort = [], + int $limit = null, + int $fetch_style = PDO::FETCH_ASSOC + ): mixed { + if (!is_array($values) && $values !== 'all') { + throw new Exception('Expecting array values, ' . gettype($values) . ' given'); + } + self::validateClause($clause, __METHOD__); + if (is_array($table)) { + $join = $table['join']; + $table = $table['table']; + } + $table = self::getTable($table); + $query = 'SELECT * FROM ' . $table; + if (isset($join)) { + $query .= ' ' . $join . ' '; + } + if (is_array($values) && !empty($values)) { + $query .= ' WHERE '; + foreach ($values as $k => $v) { + if (is_null($v)) { + $query .= '`' . $k . '` IS :' . $k . ' ' . $clause . ' '; + } else { + $query .= '`' . $k . '`=:' . $k . ' ' . $clause . ' '; + } + } + } + $query = rtrim($query, $clause . ' '); + if (!empty($sort)) { + if (!$sort['field']) { + $sort['field'] = 'date'; + } + if (!$sort['order']) { + $sort['order'] = 'desc'; + } + $query .= ' ORDER BY ' . $sort['field'] . ' ' . strtoupper($sort['order']) . ' '; + } + if ($limit && is_int($limit)) { + $query .= " LIMIT $limit"; + } + $db = self::getInstance(); + $db->query($query); + if (is_array($values)) { + foreach ($values as $k => $v) { + $db->bind(':' . $k, $v); + } + } + $fetch_style = (int) $fetch_style; + + return $limit == 1 + ? $db->fetchSingle($fetch_style) + : $db->fetchAll($fetch_style); + } + + public static function update( + string $table, + array $values, + array $wheres, + string $clause = 'AND' + ): int { + self::validateClause($clause, __METHOD__); + $table = self::getTable($table); + $query = 'UPDATE `' . $table . '` SET '; + foreach (array_keys($values) as $k) { + $query .= '`' . $k . '`=:value_' . $k . ','; + } + $query = rtrim($query, ',') . ' WHERE '; + foreach (array_keys($wheres) as $k) { + $query .= '`' . $k . '`=:where_' . $k . ' ' . $clause . ' '; + } + $query = rtrim($query, $clause . ' '); + $db = self::getInstance(); + $db->query($query); + foreach ($values as $k => $v) { + $db->bind(':value_' . $k, $v); + } + foreach ($wheres as $k => $v) { + $db->bind(':where_' . $k, $v); + } + + return $db->exec() ? $db->rowCount() : false; + } + + public static function insert(string $table, array $values): int|false + { + $table = self::getTable($table); + $table_fields = []; + $table_fields = array_keys($values); + $query = 'INSERT INTO + `' . $table . '` (`' . ltrim(implode('`,`', $table_fields), '`,`') . '`) + VALUES (' . ':' . str_replace(':', ',:', implode(':', $table_fields)) . ')'; + $db = self::getInstance(); + $db->query($query); + foreach ($values as $k => $v) { + $db->bind(':' . $k, $v); + } + + return $db->exec() + ? (int) $db->lastInsertId() + : false; + } + + public static function increment( + string $table, + array $values, + array $wheres, + string $clause = 'AND' + ): int|false { + $table = self::getTable($table); + $query = 'UPDATE `' . $table . '` SET '; + foreach ($values as $k => $v) { + if (preg_match('/^([\+\-]{1})\s*([\d]+)$/', (string) $v, $matches)) { // 1-> op 2-> number + $query .= '`' . $k . '`='; + if ($matches[1] == '+') { + $query .= '`' . $k . '`' . $matches[1] . $matches[2] . ','; + } + if ($matches[1] == '-') { + $query .= 'GREATEST(cast(`' . $k . '` AS SIGNED) - ' . $matches[2] . ', 0),'; + } + } + } + $query = rtrim($query, ',') . ' WHERE '; + foreach (array_keys($wheres) as $k) { + $query .= '`' . $k . '`=:where_' . $k . ' ' . $clause . ' '; + } + $query = rtrim($query, $clause . ' '); + $db = self::getInstance(); + $db->query($query); + foreach ($wheres as $k => $v) { + $db->bind(':where_' . $k, $v); + } + + return $db->exec() ? $db->rowCount() : false; + } + + public static function delete( + string $table, + array $values, + string $clause = 'AND' + ): int { + self::validateClause($clause, __METHOD__); + $table = self::getTable($table); + $query = 'DELETE FROM `' . $table . '` WHERE '; + foreach (array_keys($values) as $k) { + $query .= '`' . $k . '`=:' . $k . ' ' . $clause . ' '; + } + $query = rtrim($query, $clause . ' '); + $db = self::getInstance(); + $db->query($query); + foreach ($values as $k => $v) { + $db->bind(':' . $k, $v); + } + + return $db->exec() ? $db->rowCount() : 0; + } + + public static function getQueryWithTablePrefix(string $query): string + { + return strtr($query, [ + '%table_prefix%' => env()['CHEVERETO_DB_TABLE_PREFIX'] + ]); + } + + public static function dbPrepare(string $query, array $values): DB + { + $query = self::getQueryWithTablePrefix($query); + $db = self::getInstance(); + $db->query($query); + foreach ($values as $key => $value) { + $db->bind($key, $value); + } + + return $db; + } + + public static function fetchSingleQuery(string $query, array $binds, int $mode = PDO::FETCH_ASSOC): array + { + $db = self::dbPrepare($query, $binds); + $fetch = $db->fetchSingle($mode); + + return $fetch === false + ? [] + : $fetch; + } + + public static function fetchAllQuery(string $query, array $binds, int $mode = PDO::FETCH_ASSOC): array + { + $db = self::dbPrepare($query, $binds); + + return $db->exec() ? $db->fetchAll($mode) : []; + } + + private static function validateClause(string $clause, string|null $method = null) + { + $clause = strtoupper($clause); + if (!in_array($clause, ['AND', 'OR', ''])) { + throw new Exception('Expecting clause string \'AND\' or \'OR\' in ' . ($method ?? __CLASS__)); + } + } +} diff --git a/app/src/Legacy/G/Gettext.php b/app/src/Legacy/G/Gettext.php new file mode 100644 index 0000000..c577853 --- /dev/null +++ b/app/src/Legacy/G/Gettext.php @@ -0,0 +1,626 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * This class uses code that belongs or was taken from the following: + * + * David Soria Parra + * https://github.com/dsp/PHP-Gettext + * + * Jyxo, s.r.o. + * https://github.com/jyxo/php/tree/master/Jyxo/Gettext + * + * WordPress + * https://wordpress.org/ + */ + +/** + * class.gettext.php + * This class is a stand-alone implementation of gettext. + * It works with .po and .mo files and saves the result in a cached static file (by default) + */ + +namespace Chevereto\Legacy\G; + +use Exception; +use Throwable; + +/** @deprecated V4 */ +class Gettext +{ + // Magic words in the MO header + public const MO_MAGIC_1 = -569244523; //0xde120495 + + public const MO_MAGIC_2 = -1794895138; //0x950412de + + // Cache stuff + public const CACHE_FILE_SUFFIX = '.cache.php'; + + protected static $default_options = ['cache' => true, 'cache_type' => 'file', 'cache_filepath' => null, 'cache_header' => true]; + + protected $source_file; + + protected $parsed = false; + + public $translation_table = []; + + public $translation_plural = null; + + public $translation_header = null; + + private $is_cached = false; + + public function __construct($options = []) + { + $this->options = array_merge(static::$default_options, (array)$options); + $this->source_file = $this->options['file']; + + if (file_exists($this->source_file) && !is_readable($this->source_file)) { + throw new GettextException("Can't read source file", 600); + } + + $file_extension = pathinfo($this->source_file, PATHINFO_EXTENSION); + // Only allow MO and PO + if (!in_array($file_extension, ['mo', 'po'])) { + throw new GettextException('Invalid file source. This only works with .mo and .po files', 601); + } + + $this->parse_method = strtoupper($file_extension); + + if ($this->options['cache']) { + if ($this->options['cache_filepath']) { + // Custom whatever filepath cache + $this->cache_file = $this->options['cache_filepath']; + } else { + // Default cache filepath.cache.php + $this->cache_file = $this->source_file . self::CACHE_FILE_SUFFIX; + } + if (!$this->getCache()) { // No cache was found + $this->parseFile(); + } + } else { + $this->parseFile(); + } + } + + /** + * Return a translated string + * + * If the translation is not found, the original message will be returned. + * + */ + public function gettext(string $msg) + { + if (empty($msg)) { + return null; + } + if (!$this->parsed) { + $this->parseFile(); + } + + if ($this->mustFixQuotes()) { + $msg = $this->fixQuotes($msg, 'escape'); + } + + $translated = $msg; + + if (array_key_exists($msg, $this->translation_table)) { + $translated = $this->translation_table[$msg][0] ?? null; + $translated = !empty($translated) ? $translated : $msg; + } + + if ($this->mustFixQuotes()) { + $translated = $this->fixQuotes($translated, 'unescape'); + } + + return $translated; + } + + /** + * Return a translated string in it's plural form + * + * Returns the given $count (e.g second, third,...) plural form of the + * given string. If the id is not found and $num == 1 $msg is returned, + * otherwise $msg_plural + * + * @param String $msg The message to search for + * @param string $msg_plural A fallback plural form + * @param integer $count Which plural form + * + */ + public function ngettext($msg, $msg_plural, $count = 0) + { + if (empty($msg) or empty($msg_plural) or !is_numeric($count)) { + return $msg; + } + if (!$this->parsed) { + $this->parseFile(); + } + + if ($this->mustFixQuotes()) { + $msg = $this->fixQuotes($msg, 'escape'); + $msg_plural = $this->fixQuotes($msg_plural, 'escape'); + } + + $translated = $count == 1 ? $msg : $msg_plural; // Failover + + if (array_key_exists($msg, $this->translation_table)) { + $plural_index = $this->getPluralIndex($count); + $index_id = $plural_index !== false ? $plural_index : ($count - 1); + $table = $this->translation_table[$msg]; + if (array_key_exists($index_id, $table)) { + $translated = $table[$index_id]; + } + } + + if ($this->mustFixQuotes()) { + $translated = $this->fixQuotes($translated, 'unescape'); + } + + return $translated; + } + + /** + * Parse the source file + * If cache is enabled it will try to cache the result + */ + private function parseFile() + { + $parseFn = 'parse' . $this->parse_method . 'File'; + $this->$parseFn(); + $this->parsed = true; + if ($this->options['cache']) { + $this->cache('file'); + } + } + + /** + * Parse the MO file header and returns the table + * offsets as described in the file header. + * + * If an exception occurred, null is returned. This is intentionally + * as we need to get close to ext/gettext behaviour. + * + * @param resource $fp The open file handler to the MO file + * + * @return array offset + */ + private function parseMOHeader($fp) + { + $data = fread($fp, 8); + if (!$data) { + throw new GettextException("Can't fread(8) file for reading", 602); + } + $header = unpack('lmagic/lrevision', $data); + if (self::MO_MAGIC_1 != $header['magic'] && self::MO_MAGIC_2 != $header['magic']) { + return null; + } + if (0 != $header['revision']) { + return null; + } + $data = fread($fp, 4 * 5); + if (!$data) { + throw new GettextException("Can't fread(4 * 5) file for reading", 603); + } + + return unpack('lnum_strings/lorig_offset/' . 'ltrans_offset/lhash_size/lhash_offset', $data); + } + + /** + * Parse and returns the string offsets in a a table. Two table can be found in + * a mo file. The table with the translations and the table with the original + * strings. Both contain offsets to the strings in the file. + * + * If an exception occurred, null is returned. This is intentionally + * as we need to get close to ext/gettext behaviour. + * + * @param resource $fp The open file handler to the MO file + * @param int $offset The offset to the table that should be parsed + * @param int $num The number of strings to parse + * + * @return Array of offsets + */ + private function parseMOTableOffset($fp, $offset, $num) + { + if (fseek($fp, $offset, SEEK_SET) < 0) { + return null; + } + $table = []; + for ($i = 0; $i < $num; $i++) { + $data = fread($fp, 8); + $table[] = unpack('lsize/loffset', $data); + } + + return $table; + } + + /** + * Parse a string as referenced by an table. Returns an + * array with the actual string. + * + * @param resource $fp The open file handler to the MO fie + * @param array $entry The entry as parsed by parseMOTableOffset() + * + */ + private function parseMOEntry($fp, $entry): ?string + { + if (fseek($fp, $entry['offset'], SEEK_SET) < 0) { + return null; + } + if ($entry['size'] > 0) { + return fread($fp, $entry['size']) ?: null; + } + + return null; + } + + /** + * Parse the plural data found in the language + * + * @param string $header with nplurals and plural declaration + */ + private function parsePluralData($header) + { + // Base english-like plural languages + $nplurals = 2; + $formula = '(n != 1)'; + // Detect plural data. If nothing found then use general plural handling + if (preg_match('/\s*nplurals\s*\=\s*(\d+)\s*\;\s*plural\s*\=\s*(\({0,1}.*\){0,1})\s*\;/', $header, $matches)) { + $nplurals = (int) $matches[1]; + if (preg_match('/^([!n\=\<\>\&\|\?\:%\s\(\)\d]+)$/', (string) $matches[2]) === 1) { + $formula = $matches[2]; + } + } + + // Fix the plural formula + $formula = $this->parenthesizePluralFormula($formula); + + // Generate the translation_plural array + $function = str_replace('n', '$n', $formula); + + // Stock everything + $this->translation_plural = [ + 'nplurals' => $nplurals, + 'function' => $function, + ]; + } + + /** + * Adds parentheses to the inner parts of ternary operators in + * plural formulas, because PHP evaluates ternary operators from left to right + * + * @param string $formula the expression without parentheses + * @return string the formula with parentheses added + */ + private function parenthesizePluralFormula($formula) + { + $formula .= ';'; + $return = ''; + $depth = 0; + for ($i = 0; $i < strlen($formula); ++$i) { + $char = $formula[$i]; + switch ($char) { + case '?': + $return .= ' ? ('; + $depth++; + + break; + case ':': + $return .= ') : ('; + + break; + case ';': + $return .= str_repeat(')', $depth) . ';'; + $depth = 0; + + break; + default: + $return .= $char; + } + } + $return = trim(rtrim($return, ';')); // Cleaning + $return = preg_replace('/\s+/S', ' ', $return); // Extra spaces + + return str_replace('( ', '(', str_replace(' )', ')', $return)); // Remove extra space around () + } + + /** + * Get plural index + * + * @param int $count msg count + * @return int plural index + */ + public function getPluralIndex($count) + { + if (!is_callable($this->translation_plural['callable'] ?? null)) { + // So, this is how you interpeter this thing + $function = $this->translation_plural['function']; + $nplurals = $this->translation_plural['nplurals']; + $evil = "\$callable = function(\$n) {\$index = (int)$function; return \$index < $nplurals ? \$index : ($nplurals - 1);};"; + eval($evil); + /** @var callable $callable */ + $this->translation_plural['callable'] = $callable; + } + + return call_user_func($this->translation_plural['callable'], $count); + } + + private function parseHeader($header) + { + $headerTable = []; + $lines = array_map('trim', explode("\n", $header)); + foreach ($lines as $line) { + if (starts_with('msgid', $line) or starts_with('msgstr', $line)) { + continue; + } + $line = preg_replace('#\"(.*)\"#', '$1', $line); + $line = rtrim($line, '\n'); + $parts = explode(':', $line, 2); + if (!isset($parts[1])) { + continue; + } // Skip empty keys + $headerTable[trim($parts[0])] = trim($parts[1]); + } + + return $headerTable; + } + + /** + * Parse a PO entry chunk + * @param string $chunk + * + * @return Array of translation table + */ + private function parsePOEntry($chunk) + { + $chunks = explode("\n", $chunk); + foreach ($chunks as $chunk) { + if (starts_with('#', $chunk) or is_null($chunk)) { + continue; + } + if (is_null($this->translation_plural) and starts_with('"Plural-Forms:', $chunk)) { + $this->parsePluralData($chunk); + } + if (preg_match('/^msgid "(.*)"/', $chunk, $matches)) { + $msgid = $matches[1]; + } elseif (preg_match('/^msgstr "(.*)"/', $chunk, $matches)) { + $msgstr = $matches[1]; + } elseif (preg_match('/^#~ msgid "(.*)"/', $chunk, $matches)) { + $msgid = $matches[1]; + } elseif (preg_match('/^#~ msgstr "(.*)"/', $chunk, $matches)) { + $msgstr = $matches[1]; + } elseif (preg_match('/^msgstr\[([0-9])+\] "(.*)"/', $chunk, $matches)) { + if ($matches[2] == '') { + continue; + } + if (!is_array($msgstr ?? null)) { + $msgstr = []; + } + $msgstr[$matches[1]] = $matches[2]; + } + } + $msgstr ??= null; + if ($msgstr == '') { + $msgstr = null; + } + if (empty($msgid)) { + return null; + } else { + return [ + 'msgid' => $msgid, + 'msgstr' => is_null($msgstr) ? null : (array)$msgstr + ]; + } + } + + /** + * Parse binary .mo file + */ + private function parseMOFile() + { + $filesize = filesize($this->source_file); + if ($filesize < 4 * 7) { + return; + } + + $fp = @fopen($this->source_file, 'rb'); + if (!$fp) { + throw new GettextException("Can't fopen file for reading", 600); + } + + $offsets = $this->parseMOHeader($fp); + + if (null == $offsets || $filesize < 4 * ($offsets['num_strings'] + 7)) { + fclose($fp); + + return; + } + + $transTable = []; + $table = $this->parseMOTableOffset($fp, $offsets['trans_offset'], $offsets['num_strings']); + if (null == $table) { + fclose($fp); + + return; + } + + foreach ($table as $idx => $entry) { + $transTable[$idx] = $this->parseMOEntry($fp, $entry); + } + + $this->translation_header = $this->parseHeader(reset($transTable)); + + // Parse plural data + $this->parsePluralData($this->translation_header['Plural-Forms']); + + $table = $this->parseMOTableOffset($fp, $offsets['orig_offset'], $offsets['num_strings']); + + foreach ($table as $idx => $entry) { + $entry = $this->parseMOEntry($fp, $entry); + $formes = explode(chr(0), $entry); + $translation = explode(chr(0), $transTable[$idx]); + foreach ($formes as $form) { + if (empty($form)) { + continue; + } + $this->translation_table[$form] = $translation; + } + } + + fclose($fp); + } + + /** + * Parse text based .po file + */ + private function parsePOFile() + { + $linenumber = 0; + $chunks = []; + $file = file($this->source_file); + if (!$file) { + throw new GettextException("Can't read file into an array", 604); + } + foreach ($file as $line) { + if ($line == "\n" or $line == "\r\n") { + ++$linenumber; + } else { + if (!array_key_exists($linenumber, $chunks)) { + $chunks[$linenumber] = ''; + } + $chunks[$linenumber] .= $line; + } + } + $this->translation_header = $this->parseHeader(reset($chunks)); + foreach ($chunks as $chunk) { + $entry = $this->parsePOEntry($chunk); + if (!isset($entry['msgid']) or !isset($entry['msgstr'])) { + continue; + } + $this->translation_table[$entry['msgid']] = $entry['msgstr']; + } + } + + /** + * Get cached results (cached file) + * + * @return bool cache status + */ + private function getCache() + { + try { + is_readable($this->cache_file); + // Outdated cache? + $source_mtime = filemtime($this->source_file); + $cache_mtime = file_exists($this->cache_file) ? filemtime($this->cache_file) : 0; + if ($source_mtime and $cache_mtime and $source_mtime > $cache_mtime) { + return false; + } + + try { + include_once $this->cache_file; + } catch (Throwable $e) { + return false; + } + if (isset($translation_table)) { + $this->translation_table = $translation_table; + if (isset($translation_plural)) { + $this->translation_plural = $translation_plural; + } + if (isset($translation_header)) { + $this->translation_header = $translation_header; + } + $this->is_cached = true; + $this->parsed = true; + + return true; + } + } catch (Throwable $e) { + } + + $this->is_cached = false; + + return false; + } + + /** + * Cache the translation results into a file + */ + private function cache() + { + is_dir(dirname($this->cache_file)); + $fh = fopen($this->cache_file, 'w'); + if ($fh === false) { + throw new GettextException("Can't fopen cache file for writing", 601); + } + $contents = 'options['cache_header']) { + if (!is_null($this->translation_header)) { + $contents .= '$translation_header = ' . var_export($this->translation_header, true) . ';' . "\n"; + } + if (!is_null($this->translation_plural)) { + $translation_plural = $this->translation_plural; + unset($translation_plural['callable']); // Don't cache the callable reference + $contents .= '$translation_plural = ' . var_export($translation_plural, true) . ';' . "\n"; + } + } + $contents .= '$translation_table = ['; + foreach ($this->translation_table as $k => $v) { + $k = $this->parse_method == 'PO' ? $k : $this->fixQuotes($k, 'escape'); + $contents .= "\n" . ' "' . $k . '" => ['; + foreach ($v as $kk => $vv) { + $kk = $this->parse_method == 'PO' ? $kk : $this->fixQuotes($kk, 'escape'); + $vv = $this->parse_method == 'PO' ? $vv : $this->fixQuotes($vv, 'escape'); + $contents .= "\n" . ' ' . $kk . ' => "' . $vv . '",'; + } + $contents .= "\n" . ' ],'; + } + $contents .= "\n" . '];' . "\n" . '?>'; + if (!fwrite($fh, $contents)) { + throw new GettextException("Can't save translation results to cache file", 602); + } + + try { + touch($this->source_file); + } catch (Throwable $e) { + // Shhh + } + fclose($fh); + } + + private function fixQuotes($msg, $action = null) + { + if ($this->is_cached) { + return $msg; + } + switch ($action) { + case 'escape': + $msg = str_replace('"', '\"', $msg); + + break; + case 'unescape': + $msg = str_replace('\"', '"', $msg); + + break; + } + + return $msg; + } + + private function mustFixQuotes() + { + return $this->is_cached or $this->parse_method == 'PO'; + } +} + +class GettextException extends Exception +{ +} diff --git a/app/src/Legacy/G/Handler.php b/app/src/Legacy/G/Handler.php new file mode 100644 index 0000000..d00f68e --- /dev/null +++ b/app/src/Legacy/G/Handler.php @@ -0,0 +1,516 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\G; + +use function Chevere\Message\message; +use Chevere\Throwable\Exceptions\LogicException; +use Chevereto\Config\Config; +use function Chevereto\Legacy\get_captcha_component; +use function Chevereto\Legacy\getSetting; +use function Chevereto\Vars\get; +use function Chevereto\Vars\post; +use function Chevereto\Vars\request; +use function Chevereto\Vars\server; +use function Chevereto\Vars\session; +use function Chevereto\Vars\sessionVar; +use Closure; +use Exception; + +/** @deprecate V4 */ +class Handler +{ + private array $hook_template = []; + + private static string|array $route; + + private static array $route_request = []; + + private static string $route_name = ''; + + private static string $base_request = ''; + + private static array $vars = []; + + private static array $conds = []; + + private static array $routes = []; + + private static string $template_used = ''; + + private static bool $prevented_route = false; + + private static $mapped_args = []; + + private array $request_array; + + private string $relative_root; + + private string $base_url; + + private string $path_theme; + + private string $request_uri; + + private string $script_name; + + private string $valid_request; + + private string $canonical_request; + + private string $handled_request; + + private string $template; + + private array $request; + + public function __construct(bool $loadTemplate, ?Closure $before = null, ?Closure $after = null) + { + $this->relative_root = Config::host()->hostnamePath(); + $this->base_url = URL_APP_PUBLIC; + $this->path_theme = PATH_PUBLIC_LEGACY_THEME; + $this->request_uri = server()['REQUEST_URI'] ?? '/'; + $this->script_name = server()['SCRIPT_NAME'] ?? ''; + $query_string = '?' . (server()['QUERY_STRING'] ?? ''); + if (!empty(server()['QUERY_STRING'])) { + $this->request_uri = str_replace($query_string, '/', $this->request_uri); + } + $this->valid_request = '/' . ltrim(rtrim(sanitize_path_slashes($this->request_uri), '/'), '/'); + if (!empty(server()['QUERY_STRING'])) { + $this->request_uri = server()['REQUEST_URI']; + $this->valid_request .= $query_string; + } + $this->canonical_request = $this->valid_request; + if ((is_dir(PATH_PUBLIC . $this->valid_request) + || is_dir(dirname(PATH_PUBLIC) . $this->valid_request)) + && $this->valid_request !== '/' + ) { + $this->canonical_request .= '/'; + } + $this->handled_request = strtok($this->relative_root == '/' ? $this->valid_request : preg_replace('#' . $this->relative_root . '#', '/', $this->request_uri, 1), '?'); + $this->request_array = explode('/', rtrim(str_replace('//', '/', ltrim($this->handled_request, '/')), '/')); + if ($this->request_array[0] == '') { + $this->request_array[0] = '/'; + } + $this->request_array = array_values(array_filter($this->request_array, 'strlen')); + self::$base_request = $this->request_array[0]; + if (self::$base_request == 'index') { + redirect('/', 301); + } + if (self::$base_request !== '' && !empty(server()['QUERY_STRING'])) { + $fixed_qs_request = rtrim($this->relative_root, '/') . $this->handled_request; + parse_str(server()['QUERY_STRING'], $parse); + if ($parse !== []) { + $fixed_qs_request = rtrim($fixed_qs_request, '/') . '/'; + $index = -1; + foreach ($parse as $k => $v) { + $index++; + $fixed_qs_request .= $index === 0 ? '?' : '&'; + $fixed_qs_request .= urlencode($k); + if (is_string($v) && $v !== '') { + $fixed_qs_request .= '=' . urlencode($v); + } + } + } + $this->canonical_request = $fixed_qs_request; + } + if (self::$base_request == 'index.php') { + $this->canonical_request = rtrim($this->canonical_request, '/'); + redirect((sanitize_path_slashes(str_replace('index.php', '', $this->canonical_request))), 301); + } + + if ($this->relative_root !== $this->request_uri + && $this->canonical_request !== $this->request_uri + ) { + $this->baseRedirection($this->canonical_request); + } + if (in_array(self::$base_request, ['', 'index.php', '/'])) { + self::$base_request = 'index'; + } + $this->template = self::$base_request; + $this->request = $this->request_array; + self::$route_request = $this->request_array; + self::$route = $this->template !== '404' + ? ($this->request_array[0] == '/' + ? 'index' + : $this->request_array) + : '404'; + unset($this->request[0]); + $this->request = array_values($this->request); + if (is_callable($before)) { + $before($this); + } + if (($this->request[0] ?? '') === 'contact' && !self::cond('captcha_needed')) { + self::setCond( + 'captcha_needed', + getSetting('captcha') && getSetting('force_captcha_contact_page') + ); + } + if ($this->isIndex()) { + $this->processRequest(); + } + if (self::cond('captcha_needed')) { + self::setVar(...get_captcha_component()); + } + if (is_callable($after)) { + $after($this); + } + if ($loadTemplate) { + $this->loadTemplate(); + } + } + + public function handled_request(): string + { + return $this->handled_request; + } + + public function request_array(): array + { + return $this->request_array; + } + + public function template(): string + { + return $this->template; + } + + public function setTemplate(string $template): void + { + $this->template = $template; + } + + public function setPathTheme(string $path): void + { + $this->path_theme = $path; + } + + public static function baseRequest(): string + { + return self::$base_request; + } + + public static function isPreventedRoute(): bool + { + return self::$prevented_route; + } + + public static function mappedArgs(): array + { + return self::$mapped_args; + } + + public function requestArray(): array + { + return $this->request_array; + } + + public function request(): array + { + return $this->request; + } + + private function processRequest(): void + { + $route = $this->getRouteFn(self::$base_request); + if (is_callable($route)) { + $routes[self::$base_request] = $route; + } + if (is_array($routes) && array_key_exists(self::$base_request, $routes)) { + $magic = [ + 'post' => post() ? post() : null, + 'get' => get() ? get() : null, + 'request' => request() ? request() : null, + 'safe_post' => post() ? safe_html(post()) : null, + 'safe_get' => get() ? safe_html(get()) : null, + 'safe_request' => request() ? safe_html(request()) : null, + 'auth_token' => self::getAuthToken() + ]; + + self::$vars = self::$vars !== [] + ? array_merge(self::$vars, $magic) + : $magic; + if (!self::$prevented_route && is_callable($routes[self::$base_request])) { + $routes[self::$base_request]($this); + } + } else { + $this->issueError(404); + $this->request = $this->request_array; + } + if ($this->template == 404) { + self::$route = '404'; + } + self::setCond('404', $this->template == '404'); + if (isset(self::$vars['pre_doctitle'])) { + $stock_doctitle = self::$vars['doctitle']; + self::$vars['doctitle'] = self::$vars['pre_doctitle']; + if ($stock_doctitle) { + self::$vars['doctitle'] .= ' - ' . $stock_doctitle; + } + } + self::$template_used = $this->template; + } + + public function issueError(int $status): void + { + set_status_header($status); + $name = strval($status); + if ($this->cond('mapped_route')) { + self::$base_request = self::$route_request[0]; + self::$route_name = $name; + } + $this->template = $name; + } + + public function preventRoute(?string $tpl = null): void + { + if ($tpl !== null) { + $this->template = $tpl; + } + self::$prevented_route = true; + } + + public function getRouteFn(string $route_name): callable + { + if (array_key_exists($route_name, self::$routes)) { + return self::$routes[$route_name]; + } + $filename = $route_name . '.php'; + $route_file = PATH_APP_LEGACY_ROUTES . $filename; + $route_override_file = PATH_APP_LEGACY_ROUTES_OVERRIDES . $filename; + if (file_exists($route_override_file)) { + $route_file = $route_override_file; + } + if (!file_exists($route_file)) { + $route_name = getSetting('root_route'); + $route_file = PATH_APP_LEGACY_ROUTES . $route_name . ".php"; + $this->template = $route_name; + } + if (file_exists($route_file)) { + /** @var callable $route */ + $route = require $route_file; + self::$routes[$route_name] = $route; + self::$route_name = $route_name; + + return $route; + } + + throw new LogicException( + message('Missing route file %file%') + ->withCode('%file%', $route_file) + ); + } + + public function mapRoute(string $route_name, array $args = null): callable + { + $this->template = $route_name; + self::$base_request = $route_name; + self::setCond('mapped_route', true); + if (!is_null($args)) { + self::$mapped_args = $args; + } + + return $this->getRouteFn($route_name); + } + + public function isRequestLevel(int $level): bool + { + return isset($this->request_array[$level - 1]); + } + + public function baseRedirection(string $request): void + { + $request = trim(sanitize_path_slashes($request), '/'); + $url = preg_replace('{' . $this->relative_root . '}', '/', $this->base_url, 1) . $request; + redirect($url, 301); + } + + private function isIndex(): bool + { + return (bool) preg_match('{index\.php$}', ltrim($this->script_name, '/')); + } + + public function hookTemplate(array $args = []): void + { + if (in_array($args['where'], ['before', 'after']) && $args['code']) { + $this->hook_template[$args['where']] = $args['code']; + } + } + + private function loadTemplate(string $template = null): void + { + if (!is_null($template)) { + $this->template = $template; + } + $functions_basename = 'functions.php'; + $template_functions = [ + $this->path_theme . 'overrides/' . $functions_basename, + $this->path_theme . $functions_basename + ]; + foreach ($template_functions as $file) { + if (file_exists($file)) { + require $file; + + break; + } + } + $view_basename = $this->template; + $view_extension = get_file_extension($this->template); + if ($view_extension === '' || $view_extension === '0') { + $view_extension = 'php'; + if (str_ends_with($this->path_theme, '/pages/')) { + $view_extension = Config::enabled()->phpPages() + ? 'php' + : 'html'; + } + $view_basename .= '.' . $view_extension; + } + $template_file = [ + $this->path_theme . 'overrides/views/' . $view_basename, + $this->path_theme . 'overrides/' . $view_basename, + $this->path_theme . 'views/' . $view_basename, + $this->path_theme . $view_basename, + ]; + foreach ($template_file as $file) { + if (file_exists($file)) { + if ($view_extension == 'html') { + include_theme_header(); + } + if (isset($this->hook_template['before'])) { + echo $this->hook_template['before']; + } + if ($view_extension == 'php') { + require $file; + } else { + echo file_get_contents($file); + } + if (isset($this->hook_template['after'])) { + echo $this->hook_template['after']; + } + if ($view_extension == 'html') { + include_theme_footer(); + } + + return; + } + } + $end = end($template_file); + $key = key($template_file); + + throw new Exception('Missing ' . absolute_to_relative($template_file[$key]) . ' template file'); + } + + public static function getAuthToken(): string + { + $token = isset(session()['G_auth_token']) + ? session()['G_auth_token'] + : random_string(40); + sessionVar()->put('G_auth_token', $token); + + return $token; + } + + public static function checkAuthToken(string $token): bool + { + if (strlen($token) < 40) { + return false; + } + + return timing_safe_compare(session()['G_auth_token'], $token); + } + + public static function setVar(string $var, mixed $value): void + { + self::$vars[$var] = $value; + } + + public static function setVars(array $array = []): void + { + foreach ((array) $array as $var => $value) { + self::$vars[$var] = $value; + } + } + + public static function setCond(string $cond, bool $bool): void + { + self::$conds[$cond] = $bool; + } + + public static function setConds(array $array = []): void + { + foreach ((array) $array as $conds => $bool) { + self::$conds[$conds] = (bool) $bool; + } + } + + public static function hasVar(string $var): bool + { + return array_key_exists($var, self::vars()); + } + + public static function var($var): mixed + { + return self::vars()[$var] ?? null; + } + + public static function vars(): array + { + return self::$vars; + } + + public static function hasCond(string $cond): bool + { + return array_key_exists($cond, self::conds()); + } + + public static function cond(string $cond): bool + { + return self::conds()[$cond] ?? false; + } + + public static function conds(): array + { + return self::$conds; + } + + public static function updateVar(string $var, mixed $value) + { + if (is_array(self::$vars[$var]) && is_array($value)) { + $value += self::$vars[$var]; // replacement + replaced + ksort($value); + } + self::$vars[$var] = $value; + } + + public static function unsetVar(string $var): void + { + unset(self::$vars[$var]); + } + + public static function getTemplateUsed(): string + { + return self::$template_used; + } + + public static function getRoutePath(bool $full = true): string + { + if (is_array(self::$route)) { + return $full ? implode('/', self::$route) : self::$route[0]; + } else { + return self::$route; + } + } + + public static function getRouteName(): string + { + return self::$route_name; + } +} diff --git a/app/src/Legacy/G/functions-render.php b/app/src/Legacy/G/functions-render.php new file mode 100644 index 0000000..665c056 --- /dev/null +++ b/app/src/Legacy/G/functions-render.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\G; + +/** + * INCLUDE TAGS + * --------------------------------------------------------------------- + */ + +function include_theme_file($filename, $args = []) +{ + $file = PATH_PUBLIC_LEGACY_THEME . $filename; + $override = PATH_PUBLIC_LEGACY_THEME . 'overrides/' . $filename; + if (!file_exists($file)) { + $file .= '.php'; + $override .= '.php'; + } + if (file_exists($override)) { + $file = $override; + } + if (file_exists($file)) { + $GLOBALS['theme_include_args'] = $args; + require $file; + unset($GLOBALS['theme_include_args']); + } +} + +function include_theme_header() +{ + include_theme_file('header'); +} + +function include_theme_footer() +{ + include_theme_file('footer'); +} + +function get_theme_file_contents($filename) +{ + $file = PATH_PUBLIC_LEGACY_THEME . $filename; + + return file_exists($file) ? file_get_contents($file) : null; +} + +/** + * THEME DATA FUNCTIONS + * --------------------------------------------------------------------- + */ + +function get_theme_file_url($string) +{ + return URL_APP_THEME . $string; +} + +/** + * ASSETS + * --------------------------------------------------------------------- + */ + + // Returns the HTML input with the auth token +function get_input_auth_token($name = 'auth_token') +{ + return ''; +} + +/** + * NON HTML OUTPUT + * --------------------------------------------------------------------- + */ + +// Outputs the REST_API array to xml +function xml_output($data = []) +{ + error_reporting(0); + if (ob_get_level() === 0 && !ob_start('ob_gzhandler')) { + ob_start(); + } + header("Last-Modified: " . gmdate("D, d M Y H:i:s") . "GMT"); + header("Cache-Control: no-cache, must-revalidate"); + header("Pragma: no-cache"); + header("Content-Type:text/xml; charset=UTF-8"); + $out = '' . "\n"; + if ($data['status_code'] ?? false) { + $out .= "$data[status_code]\n"; + if (!$data['status_txt']) { + $data['status_txt'] = get_set_status_header_desc($data['status_code']); + } + $out .= "$data[status_txt]\n"; + } + unset($data['status_code'], $data['status_txt']); + if ($data !== []) { + foreach ($data as $key => $array) { + $out .= "<$key>\n"; + foreach ($array as $prop => $value) { + $out .= " <$prop>$value\n"; + } + $out .= "\n"; + } + } + echo $out; + die(); +} + +// Procedural function to output an array to json +function json_output($data = [], $callback = null) +{ + error_reporting(0); + if (ob_get_level() === 0 && !ob_start('ob_gzhandler')) { + ob_start(); + } + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT'); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + header('Content-type: application/json; charset=UTF-8'); + if (!check_value($data) || (check_value($callback) && preg_match('/\W/', (string) $callback))) { + set_status_header(400); + $json_fail = [ + 'status_code' => 400, + 'status_txt' => get_set_status_header_desc(400), + 'error' => [ + 'message' => 'no request data present', + 'code' => null + ] + ]; + echo json_encode($json_fail); + die(255); + } + if (isset($data['status_code']) && !isset($data['status_txt'])) { + $data['status_txt'] = get_set_status_header_desc($data['status_code']); + } + $flags = 0; + if (PHP_SAPI === 'cli') { + $flags = JSON_PRETTY_PRINT; + } + $json_encode = json_encode($data, $flags); + if (!$json_encode) { // Json failed + set_status_header(500); + $json_fail = [ + 'status_code' => 500, + 'status_txt' => get_set_status_header_desc(500), + 'error' => [ + 'message' => "data couldn't be encoded into json", + 'code' => null + ] + ]; + echo json_encode($json_fail); + die(255); + } + set_status_header($data['status_code'] ?? 200); + if (!is_null($callback)) { + print sprintf('%s(%s);', $callback, $json_encode); + } else { + print $json_encode; + } + if (PHP_SAPI === 'cli') { + echo "\n"; + } + die(255); +} diff --git a/app/src/Legacy/G/functions.php b/app/src/Legacy/G/functions.php new file mode 100644 index 0000000..d338d8a --- /dev/null +++ b/app/src/Legacy/G/functions.php @@ -0,0 +1,2615 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy\G; + + use Chevereto\Config\Config; +use function Chevereto\Vars\env; +use function Chevereto\Vars\server; +use Composer\CaBundle\CaBundle; +use CurlHandle; +use DateInterval; +use ErrorException; +use Exception; +use GdImage; +use LogicException; +use function Safe\curl_exec; +use Throwable; + +/** + * ROUTE HELPERS + * ---------------------------------------------------------------------. + */ + +/** + * @return bool True if the $route is current /route or mapped-route -> /route + */ +function is_route(string $route): bool +{ + return Handler::baseRequest() === $route; +} + +function is_route_available(string $route): bool +{ + $route_file = $route . '.php'; + + return file_exists(PATH_APP_LEGACY_ROUTES . $route_file) or file_exists(PATH_APP_LEGACY_ROUTES_OVERRIDES . $route_file); +} + +function is_prevented_route(): bool +{ + return Handler::isPreventedRoute() === true; +} + +/** + * $full=true returns route/and/sub/routes + * $full=false returns the /route base + */ +function get_route_path(bool $full = false): string +{ + return Handler::getRoutePath($full); +} + +/** + * @return string route name from name.php + */ +function get_route_name(): string +{ + return Handler::getRouteName(); +} + +function get_template_used(): string +{ + return Handler::getTemplateUsed(); +} + +/** @deprecated V4 */ +function debug(mixed $arguments) +{ + if (empty($arguments)) { + return; + } + if (PHP_SAPI !== 'cli') { + echo '
';
+    }
+    foreach (func_get_args() as $value) {
+        print_r($value);
+    }
+    if (PHP_SAPI !== 'cli') {
+        echo '
'; + } +} + +/** @deprecated V4 */ +function check_value(mixed $anything): bool +{ + // @phpstan-ignore-next-line + if ((!empty($anything) && isset($anything)) + || $anything == '0' + || (is_countable($anything) && count($anything) > 0)) { + return true; + } + + return false; +} + +/** @deprecated V4 */ +function get_global(mixed $var): mixed +{ + global ${$var}; + + return ${$var}; +} + +function is_apache(): bool +{ + return preg_match('/Apache/i', server()['SERVER_SOFTWARE'] ?? ''); +} + +function random_values(int $min, int $max, int $limit): array +{ + $min = min($min, $max); + $max = max($min, $max); + if ($min == $max) { + return [$min]; + } + $minmax_limit = abs($max - $min); + if ($limit > $minmax_limit) { + $limit = $minmax_limit; + } + $array = []; + for ($i = 0; $i < $limit; ++$i) { + $rand = rand($min, $max); + while (in_array($rand, $array)) { + $rand = mt_rand($min, $max); + } + $array[$i] = $rand; + } + + return $array; +} + +/** @deprecated V4 */ +function random_string(int $length): string +{ + switch (true) { + case function_exists('random_bytes'): + $r = random_bytes($length); + + break; + case function_exists('openssl_random_pseudo_bytes'): + $r = openssl_random_pseudo_bytes($length); + + break; + case is_readable('/dev/urandom'): + $r = file_get_contents('/dev/urandom', false, null, 0, $length); + + break; + default: + $i = 0; + $r = ''; + while ($i++ < $length) { + $r .= chr(mt_rand(0, 255)); + } + + break; + } + + return substr(bin2hex($r), 0, $length); +} + +/** @deprecated V4 */ +function timing_safe_compare(?string $safe, ?string $user): bool +{ + $safe ??= ''; + $user ??= ''; + $safe .= chr(0); + $user .= chr(0); + $safeLen = strlen($safe); + $userLen = strlen($user); + $result = $safeLen - $userLen; + for ($i = 0; $i < $userLen; ++$i) { + $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i])); + } + + return $result === 0; +} + +/** @deprecated V4 */ +function str_replace_first(string $search, string $replace, string $subject): string +{ + $pos = strpos($subject, $search); + if ($pos !== false) { + $subject = substr_replace($subject, $replace, $pos, strlen($search)); + } + + return $subject; +} + +/** @deprecated V4 */ +function str_replace_last(string $search, string $replace, string $subject): string +{ + $pos = strrpos($subject, $search); + if ($pos !== false) { + $subject = substr_replace($subject, $replace, $pos, strlen($search)); + } + + return $subject; +} + +/** @deprecated V4 */ +function starts_with(string $needle, string $haystack): bool +{ + return substr($haystack, 0, strlen($needle)) === $needle; +} + +/** @deprecated V4 */ +function ends_with(string $needle, string $haystack): bool +{ + $length = strlen($needle); + if ($length == 0) { + return true; + } + + return substr($haystack, -$length) === $needle; +} + +function array_filter_array(array $array, array $filter_keys, string $get = 'exclusion'): array +{ + $return = []; + $get = strtolower($get); + $default_get = 'exclusion'; + foreach ($filter_keys as $k => $v) { + switch ($get) { + default: + case $default_get: + $get = $default_get; + if (!array_key_exists($v, $array)) { + continue 2; + } + $return[$v] = $array[$v]; + + break; + case 'rest': + unset($array[$v]); + + break; + } + } + + return $get == $default_get ? $return : $array; +} + +function key_asort(array &$array, string $key): void +{ + $sorter = []; + $ret = []; + reset($array); + foreach ($array as $ii => $va) { + $sorter[$ii] = $va[$key]; + } + asort($sorter); + foreach ($sorter as $ii => $va) { + $ret[$ii] = $array[$ii]; + } + $array = $ret; +} + +function array_utf8encode(array &$arr): array +{ + array_walk_recursive($arr, function (&$val, $key) { + if (is_int($val)) { + $val = (string) $val; + } + if ($val !== null) { + $encoding = mb_detect_encoding($val); + if ($encoding == false) { + $val = null; + } else { + $val = mb_convert_encoding($val, 'UTF-8', $encoding); + } + } + }); + + return $arr; +} + +function array_remove_empty(array $haystack): array +{ + foreach ($haystack as $key => $value) { + if (is_array($value)) { + $haystack[$key] = array_remove_empty($haystack[$key]); + } + if (empty($haystack[$key])) { + unset($haystack[$key]); + } + } + + return $haystack; +} + +function abbreviate_number(string|int $number): string +{ + // @phpstan-ignore-next-line + $number = (0 + str_replace(',', '', (string) $number)); + if (!is_numeric($number) or $number == 0) { + return (string) $number; + } + $abbreviations = [ + 24 => 'Y', + 21 => 'Z', + 18 => 'E', + 15 => 'P', + 12 => 'T', + 9 => 'G', + 6 => 'M', + 3 => 'K', + 0 => null, + ]; + foreach ($abbreviations as $exponent => $abbreviation) { + if ($number >= pow(10, $exponent)) { + return round(floatval($number / pow(10, $exponent))) . $abbreviation; + } + } + + return (string) $number; +} + +function nullify_string(mixed &$string) +{ + if (is_string($string) and $string == '') { + $string = null; + } +} + +function hex_to_rgb(string $hex): array +{ + $hex = str_replace('#', '', $hex); + if (strlen($hex) == 3) { + $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + + return [$r, $g, $b]; +} + +function rgb_to_hex(array $rgb): string +{ + $hex = '#'; + $hex .= str_pad(dechex($rgb[0]), 2, '0', STR_PAD_LEFT); + $hex .= str_pad(dechex($rgb[1]), 2, '0', STR_PAD_LEFT); + $hex .= str_pad(dechex($rgb[2]), 2, '0', STR_PAD_LEFT); + + return $hex; +} + +function html_to_bbcode(string $text): string +{ + $htmltags = [ + '/\(.*?)\<\/b\>/is', + '/\(.*?)\<\/i\>/is', + '/\(.*?)\<\/u\>/is', + '/\(.*?)\<\/ul\>/is', + '/\(.*?)\<\/li\>/is', + '/\/is', + '/\/is', + '/\/is', + '/\
(.*?)\<\/div\>/is', + '/\
(.*?)\<\/div\>/is', + '/\
(.*?)\<\/div\>/is', + '/\
(.*?)\<\/div\>/is', + '/\(.*?)\<\/cite\>/is', + '/\(.*?)\<\/blockquote\>/is', + '/\(.*?)\<\/div\>/is', + '/\(.*?)\<\/code\>/is', + '/\/is', + '/\(.*?)\<\/strong\>/is', + '/\(.*?)\<\/em\>/is', + '/\(.*?)\<\/a\>/is', + '/\http:\/\/(.*?)\<\/a\>/is', + '/\(.*?)\<\/a\>/is', + ]; + $bbtags = [ + '[b]$1[/b]', + '[i]$1[/i]', + '[u]$1[/u]', + '[list]$1[/list]', + '[*]$1', + '$3', + '[img]$2[/img]', + ':$3', + '\[quote\]$1\[/quote\]', + '\[code\]$1\[/code\]', + '', + '', + '', + '\[quote\]$1\[/quote\]', + '$1', + '\[code\]$1\[/code\]', + "\n", + '[b]$1[/b]', + '[i]$1[/i]', + '[email=$1]$3[/email]', + '[url]$1[/url]', + '[url=$1]$3[/url]', + ]; + $text = str_replace("\n", ' ', $text); + $ntext = preg_replace($htmltags, $bbtags, $text); + $ntext = preg_replace($htmltags, $bbtags, $ntext); + if (!$ntext) { + $ntext = str_replace(['
', '
'], "\n", $text); + $ntext = str_replace(['', ''], ['[b]', '[/b]'], $ntext); + $ntext = str_replace(['', ''], ['[i]', '[/i]'], $ntext); + } + $ntext = strip_tags($ntext); + + return trim(html_entity_decode($ntext, ENT_QUOTES, 'UTF-8')); +} + +function linkify(string $text, array $options = []): string +{ + $attr = ''; + if (array_key_exists('attr', $options)) { + foreach ($options['attr'] as $key => $value) { + if (true === is_array($value)) { + $value = array_pop($value); + } + $attr .= sprintf(' %s="%s"', $key, $value); + } + } + $options['attr'] = $attr; + $ignoreTags = ['head', 'link', 'a', 'script', 'style', 'code', 'pre', 'select', 'textarea', 'button']; + $chunks = preg_split('/(<.+?>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE); + $openTag = null; + for ($i = 0; $i < count($chunks); ++$i) { + if ($i % 2 === 0) { // even numbers are text + // Only process this chunk if there are no unclosed $ignoreTags + if (null === $openTag) { + $chunks[$i] = linkify_urls($chunks[$i], $options); + $chunks[$i] = linkify_emails($chunks[$i], $options); + } + } else { // odd numbers are tags + // Only process this tag if there are no unclosed $ignoreTags + if (null === $openTag) { + // Check whether this tag is contained in $ignoreTags and is not self-closing + if (preg_match('`<(' . implode('|', $ignoreTags) . ').*(?$`is', (string) $chunks[$i], $matches)) { + $openTag = $matches[1]; + } + } else { + // Otherwise, check whether this is the closing tag for $openTag. + if (preg_match('``i', (string) $chunks[$i], $matches)) { + $openTag = null; + } + } + } + } + + return implode($chunks); +} + +function linkify_emails(string $text, array $options = ['attr' => '']): string +{ + $pattern = '~(?xi) + \b + (?' . $match[0] . '
'; + }; + + return preg_replace_callback($pattern, $callback, $text); +} + +function linkify_urls(string $text, array $options = ['attr' => '']) +{ + $pattern = '~(?xi) + (?: + ((ht|f)tps?://) # scheme:// + | # or + www\d{0,3}\. # "www.", "www1.", "www2." ... "www999." + | # or + www\- # "www-" + | # or + [a-z0-9.\-]+\.[a-z]{2,4}(?=/) # looks like domain name followed by a slash + ) + (?: # Zero or more: + [^\s()<>]+ # Run of non-space, non-()<> + | # or + \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels + )* + (?: # End with: + \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels + | # or + [^\s`!\-()\[\]{};:\'".,<>?«»“”‘’] # not a space or one of these punct chars + ) + ~'; + $callback = function ($match) use ($options) { + $caption = $match[0]; + $pattern = '~^(ht|f)tps?://~'; + if (0 === preg_match($pattern, (string) $match[0])) { + $match[0] = 'http://' . $match[0]; + } + if (is_callable($options['callback'] ?? null)) { + $cb = $options['callback']($match[0], $caption, $options); + if (!is_null($cb)) { + return $cb; + } + } + + return '' . $caption . ''; + }; + + return preg_replace_callback($pattern, $callback, $text); +} + +function linkify_safe(string $text, array $options = []) +{ + $options = array_merge([ + 'attr' => [ + 'rel' => 'nofollow', + 'target' => '_blank' + ] + ], $options); + + return linkify($text, $options); +} + +/** @deprecated V4 */ +function errorsAsExceptions( + int $severity, + string $message, + string $file, + int $line +): void { + throw new ErrorException($message, 0, $severity, $file, $line); +} + +function writeToStderr(string $message): void +{ + fwrite(fopen('php://stderr', 'wb'), $message . "\n"); +} + +/** @deprecated V4 */ +function exception_to_error(Throwable $e, bool $print = true): string +{ + $errorId = random_string(16); + $isDocker = env()['CHEVERETO_SERVICING'] === 'docker'; + $device = $isDocker ? 'stderr' : 'error_log'; + $debug_level = Config::system()->debugLevel(); + if (!in_array($debug_level, [0, 1, 2, 3])) { + $debug_level = 1; + } + $internal_code = 500; + $internal_error = 'Aw, snap! ' . get_set_status_header_desc($internal_code); + $table = [ + 0 => "debug is disabled", + 1 => "debug @ $device", + 2 => "debug @ print", + 3 => "debug @ print,$device", + ]; + $internal_error .= ' [' . $table[$debug_level] . '] - https://chv.to/v4debug'; + $message = [$internal_error, '', '** errorId #' . $errorId . ' **']; + $previous = $e; + $messageStock = []; + $i = 0; + do { + $code = $previous->getCode(); + $messageStock[$i] = [$previous->getMessage(), safe_html($previous->getMessage())]; + $message[] = '>> ' . get_class($e) . " [$code]: %message_$i%"; + $message[] = 'At ' . absolute_to_relative($previous->getFile()) . ':' . $previous->getLine() . "\n"; + $i++; + } while ($previous = $previous->getPrevious()); + $stack = 'Stack trace:'; + $message[] = $stack; + $rtn = ''; + $count = 0; + foreach ($e->getTrace() as $frame) { + $args = ''; + if (isset($frame['args'])) { + $args = []; + foreach ($frame['args'] as $arg) { + switch (true) { + case is_string($arg): + if (file_exists($arg)) { + $arg = absolute_to_relative($arg); + } + $args[] = "'" . $arg . "'"; + + break; + case is_array($arg): + $args[] = 'Array'; + + break; + case is_null($arg): + $args[] = 'NULL'; + + break; + case is_bool($arg): + $args[] = ($arg) ? 'true' : 'false'; + + break; + case is_object($arg): + $args[] = get_class($arg); + + break; + case is_resource($arg): + $args[] = get_resource_type($arg); + + break; + default: + $args[] = $arg; + + break; + } + } + $args = join(', ', $args); + } + $rtn .= sprintf( + "#%s %s(%s): %s(%s)\n", + $count, + isset($frame['file']) ? absolute_to_relative($frame['file']) : 'unknown file', + isset($frame['line']) ? $frame['line'] : 'unknown line', + (isset($frame['class'])) ? $frame['class'] . $frame['type'] . $frame['function'] : $frame['function'], + $args + ); + ++$count; + } + $message[] = $rtn; + $messageEcho = nl2br(implode("\n", $message)); + $messageLog = "\n" . strip_tags(nl2br(implode("\n", $message))); + foreach ($messageStock as $pos => $safeMessage) { + $messageEcho = strtr($messageEcho, ["%message_$pos%" => $safeMessage[1]]); + $messageLog = strtr($messageLog, ["%message_$pos%" => $safeMessage[0]]); + } + set_status_header($internal_code); + if ($print && in_array($debug_level, [2, 3])) { + echo PHP_SAPI !== 'cli' ? $messageEcho : $messageLog; + } + if (in_array($debug_level, [1, 3])) { + error_log($messageLog); + } + if ($isDocker) { + writeToStderr($messageLog); + } + + return $errorId; +} + +function datetimegmt(?string $format = null): string +{ + return gmdate(!is_null($format) ? $format : 'Y-m-d H:i:s'); +} + +function datetime(?string $format = null): string +{ + return date(!is_null($format) ? $format : 'Y-m-d H:i:s'); +} + +function datetime_tz(string $tz, ?string $format = null): string +{ + $date = date_create('now', timezone_open($tz)); + + return date_format($date, !is_null($format) ? $format : 'Y-m-d H:i:s'); +} + +function is_valid_timezone(string $tzid): bool +{ + $valid = []; + $tza = timezone_abbreviations_list(); + foreach ($tza as $zone) { + foreach ($zone as $item) { + $valid[$item['timezone_id']] = true; + } + } + unset($valid['']); + + return $valid[$tzid] ?? false; +} + +function datetimegmt_convert_tz(string $datetimegmt, string $tz): string +{ + if (!is_valid_timezone($tz)) { + return $datetimegmt; + } + $date = new \DateTime($datetimegmt . '+00'); + $date->setTimezone(new \DateTimeZone($tz)); + + return $date->format('Y-m-d H:i:s'); +} + +/** + * Returns the difference between two UTC dates in the given format (default seconds) + * @return integer `$new (current) - $old` + */ +function datetime_diff( + string $oldDatetime, + ?string $newDatetime = null, + string $format = 's' +): int { + if (!in_array($format, ['s', 'm', 'h', 'd'])) { + $format = 's'; + } + if ($newDatetime == null) { + $newDatetime = datetimegmt(); + } + $tz = new \DateTimeZone('UTC'); + $oldDateTime = new \DateTime($oldDatetime, $tz); + $newDateTime = new \DateTime($newDatetime, $tz); + $diff = $newDateTime->getTimestamp() - $oldDateTime->getTimestamp(); // In seconds + $timeconstant = [ + 's' => 1, + 'm' => 60, + 'h' => 3600, + 'd' => 86400, + ]; + + return intval($diff / $timeconstant[$format]); +} + +function datetime_add(string $datetime, string $add) +{ + return datetime_alter($datetime, $add, 'add'); +} + +function datetime_sub(string $datetime, string $sub) +{ + return datetime_alter($datetime, $sub, 'sub'); +} + +function datetime_modify(string $datetime, string $var) +{ + return datetime_alter($datetime, $var, 'modify'); +} + +function datetime_alter(string $datetime, string $var, $action = 'add'): string +{ + if (!in_array($action, ['add', 'sub', 'modify'])) { + return $datetime; + } + $DateTime = new \DateTime($datetime); + if ($action == 'modify') { + $DateTime->$action($var); + } else { + try { + $interval = new DateInterval($var); + } catch (Throwable $e) { + return $datetime; + } + $DateTime->$action($interval); + } + + return $DateTime->format('Y-m-d H:i:s'); +} + +function dateinterval(string $duration): DateInterval|bool +{ + try { + return new DateInterval($duration); + } catch (Exception $e) { + } + + return false; +} + +function get_client_ip(): string +{ + $key = env()['CHEVERETO_HEADER_CLIENT_IP']; + $key = $key === '' + ? 'REMOTE_ADDR' + : 'HTTP_' . strtoupper(str_replace('-', '_', $key)); + + return server()[$key] ?? ''; +} + +function get_client_languages(): array +{ + $acceptedLanguages = server()['HTTP_ACCEPT_LANGUAGE'] ?? ''; + preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})*)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $acceptedLanguages, $lang_parse); + $langs = $lang_parse[1]; + $ranks = $lang_parse[4]; + $lang2pref = []; + for ($i = 0; $i < count($langs); ++$i) { + $lang2pref[$langs[$i]] = (float) (!empty($ranks[$i]) ? $ranks[$i] : 1); + } + $cmpLangs = function ($a, $b) use ($lang2pref) { + if ($lang2pref[$a] > $lang2pref[$b]) { + return -1; + } elseif ($lang2pref[$a] < $lang2pref[$b]) { + return 1; + } elseif (strlen($a) > strlen($b)) { + return -1; + } elseif (strlen($a) < strlen($b)) { + return 1; + } else { + return 0; + } + }; + if (is_callable($cmpLangs)) { + uksort($lang2pref, $cmpLangs); + } + + return $lang2pref; +} + +/** + * Parses a user agent string into its important parts. + * + * @author Jesse G. Donat + * + * @see https://github.com/donatj/PhpUserAgent + * @see http://donatstudios.com/PHP-Parser-HTTP_USER_AGENT + * + */ +function parse_user_agent(?string $u_agent = null): array +{ + if (is_null($u_agent) && isset(server()['HTTP_USER_AGENT'])) { + $u_agent = server()['HTTP_USER_AGENT']; + } + $platform = null; + $browser = null; + $version = null; + $empty = ['platform' => $platform, 'browser' => $browser, 'version' => $version]; + + if (!$u_agent) { + return $empty; + } + + if (preg_match('/\((.*?)\)/im', (string) $u_agent, $parent_matches)) { + preg_match_all('/(?PAndroid|CrOS|iPhone|iPad|Linux|Macintosh|Windows(\ Phone\ OS)?|Silk|linux-gnu|BlackBerry|PlayBook|Nintendo\ (WiiU?|3DS)|Xbox) + (?:\ [^;]*)? + (?:;|$)/imx', $parent_matches[1], $result, PREG_PATTERN_ORDER); + + $priority = ['Android', 'Xbox']; + $result['platform'] = array_unique($result['platform']); + if (count($result['platform']) > 1) { + if ($keys = array_intersect($priority, $result['platform'])) { + $platform = reset($keys); + } else { + $platform = $result['platform'][0]; + } + } elseif (isset($result['platform'][0])) { + $platform = $result['platform'][0]; + } + } + + if ($platform == 'linux-gnu') { + $platform = 'Linux'; + } elseif ($platform == 'CrOS') { + $platform = 'Chrome OS'; + } + + preg_match_all( + '%(?PCamino|Kindle(\ Fire\ Build)?|Firefox|Iceweasel|Safari|MSIE|Trident/.*rv|AppleWebKit|Chrome|IEMobile|Opera|OPR|Silk|Lynx|Midori|Version|Wget|curl|NintendoBrowser|PLAYSTATION\ (\d|Vita)+) + (?:\)?;?) + (?:(?:[:/ ])(?P[0-9A-Z.]+)|/(?:[A-Z]*))%ix', + $u_agent, + $result, + PREG_PATTERN_ORDER + ); + if (!isset($result['browser'][0]) || !isset($result['version'][0])) { + return $empty; + } + $browser = $result['browser'][0]; + $version = $result['version'][0]; + $find = function ($search, &$key) use ($result) { + $xkey = array_search(strtolower($search), array_map('strtolower', $result['browser'])); + if ($xkey !== false) { + $key = $xkey; + + return true; + } + + return false; + }; + $key = 0; + if ($browser == 'Iceweasel') { + $browser = 'Firefox'; + } elseif ($find('Playstation Vita', $key)) { + $platform = 'PlayStation Vita'; + $browser = 'Browser'; + } elseif ($find('Kindle Fire Build', $key) || $find('Silk', $key)) { + $browser = $result['browser'][$key] == 'Silk' ? 'Silk' : 'Kindle'; + $platform = 'Kindle Fire'; + if (!($version = $result['version'][$key]) || !is_numeric($version[0])) { + $version = $result['version'][array_search('Version', $result['browser'])]; + } + } elseif ($find('NintendoBrowser', $key) || $platform == 'Nintendo 3DS') { + $browser = 'NintendoBrowser'; + $version = $result['version'][$key]; + } elseif ($find('Kindle', $key)) { + $browser = $result['browser'][$key]; + $platform = 'Kindle'; + $version = $result['version'][$key]; + } elseif ($find('OPR', $key)) { + $browser = 'Opera Next'; + $version = $result['version'][$key]; + } elseif ($find('Opera', $key)) { + $browser = 'Opera'; + $find('Version', $key); + $version = $result['version'][$key]; + } elseif ($find('Chrome', $key)) { + $browser = 'Chrome'; + $version = $result['version'][$key]; + } elseif ($find('Midori', $key)) { + $browser = 'Midori'; + $version = $result['version'][$key]; + } elseif ($browser == 'AppleWebKit') { + if (($platform == 'Android' && !($key = 0))) { + $browser = 'Android Browser'; + } elseif ($platform == 'BlackBerry' || $platform == 'PlayBook') { + $browser = 'BlackBerry Browser'; + } elseif ($find('Safari', $key)) { + $browser = 'Safari'; + } + + $find('Version', $key); + + $version = $result['version'][$key]; + } elseif ($browser == 'MSIE' || strpos($browser, 'Trident') !== false) { + if ($find('IEMobile', $key)) { + $browser = 'IEMobile'; + } else { + $browser = 'MSIE'; + $key = 0; + } + $version = $result['version'][$key]; + } elseif ($key = preg_grep("/playstation \d/i", array_map('strtolower', $result['browser']))) { + $key = reset($key); + + $platform = 'PlayStation ' . preg_replace('/[^\d]/i', '', $key); + $browser = 'NetFront'; + } + + return ['platform' => $platform, 'browser' => $browser, 'version' => $version]; +} + +function is_real_email_address(string $email): bool +{ + $valid = true; + $atIndex = strrpos($email, '@'); + if (is_bool($atIndex) && $atIndex === false) { + $valid = false; + } else { + $domain = substr($email, $atIndex + 1); + $local = substr($email, 0, $atIndex); + $localLen = strlen($local); + $domainLen = strlen($domain); + if ($localLen < 1 || $localLen > 64) { + $valid = false; + } elseif ($domainLen < 1 || $domainLen > 255) { + $valid = false; + } elseif ($local[0] == '.' || $local[$localLen - 1] == '.') { + $valid = false; + } elseif (preg_match('/\\.\\./', $local)) { + $valid = false; + } elseif (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) { + $valid = false; + } elseif (preg_match('/\\.\\./', $domain)) { + $valid = false; + } elseif (!preg_match( + '/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', + str_replace('\\\\', '', $local) + )) { + if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace('\\\\', '', $local))) { + $valid = false; + } + } + if ($valid && !(checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A'))) { + $valid = false; + } + } + + return $valid; +} + +function is_valid_hex_color(string $string, bool $prefix = true): bool +{ + return preg_match( + '/#' + . ($prefix ? '?' : '') + . '([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})/', + $string + ) === 1; +} + +function is_valid_ip(string $ip): bool +{ + return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); +} + +function remove_spaces(string $string): string +{ + return str_replace(' ', '', $string); +} + +function sanitize_path_slashes(string $path): string +{ + return preg_replace('#/+#', '/', $path); +} + +function sanitize_directory_separator(string $path): string +{ + return preg_replace('#' . DIRECTORY_SEPARATOR . '+#', DIRECTORY_SEPARATOR, $path); +} + +function sanitize_relative_path(string $path): string +{ + $path = forward_slash($path); + $path = sanitize_path_slashes($path); + $path = preg_replace('#(\.+/)+#', '', $path); + + return sanitize_path_slashes($path); +} + +function rrmdir(string $dir): void +{ + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != '.' && $object != '..') { + if (is_dir($dir . '/' . $object)) { + rrmdir($dir . '/' . $object); + } else { + unlinkIfExists($dir . '/' . $object); + } + } + } + rmdir($dir); + } +} + +/** + * This function was stolen from chyrp.net (MIT). + */ +function sanitize_string( + string $string, + bool $force_lowercase = true, + bool $only_alphanumerics = false, + int $truncate = 100 +): string { + $strip = [ + '~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '=', '+', '{', + '}', '\\', '|', ';', ':', '\'', "'", '‘', '’', '“', '”', '–', '—', + '—', '–', ',', '<', '.', '>', '/', '?', + ]; + $clean = trim(str_replace($strip, '', strip_tags($string))); + $clean = preg_replace('/\s+/', '-', $clean); + $clean = ($only_alphanumerics ? preg_replace('/[^a-zA-Z0-9]/', '', $clean) : $clean); + $clean = ($truncate ? substr($clean, 0, $truncate) : $clean); + + return $force_lowercase + ? ( + function_exists('mb_strtolower') + ? + mb_strtolower($clean, 'UTF-8') + : strtolower($clean) + ) + : $clean; +} + +/** + * Original PHP code by Chirp Internet: www.chirp.com.au */ +function truncate( + string $string, + int $limit, + ?string $break = null, + string $pad = '...' +): string { + $encoding = 'UTF-8'; + if (mb_strlen($string, $encoding) <= $limit) { + return $string; + } + if (is_null($break) or $break == '') { + $string = trim(mb_substr($string, 0, $limit - strlen($pad), $encoding)) . $pad; + } else { + if (false !== ($breakpoint = strpos($string, $break, $limit))) { + if ($breakpoint < mb_strlen($string, $encoding) - 1) { + $string = mb_substr($string, 0, $breakpoint, $encoding) . $pad; + } + } + } + + return $string; +} + +function unaccent_string(string $string): string +{ + if (function_exists('mb_detect_encoding')) { + $utf8 = strtolower(mb_detect_encoding($string)) == 'utf-8'; + } else { + $length = strlen($string); + $utf8 = true; + for ($i = 0; $i < $length; ++$i) { + $c = ord($string[$i]); + if ($c < 0x80) { + $n = 0; + } // 0bbbbbbb + elseif (($c & 0xE0) == 0xC0) { + $n = 1; + } // 110bbbbb + elseif (($c & 0xF0) == 0xE0) { + $n = 2; + } // 1110bbbb + elseif (($c & 0xF8) == 0xF0) { + $n = 3; + } // 11110bbb + elseif (($c & 0xFC) == 0xF8) { + $n = 4; + } // 111110bb + elseif (($c & 0xFE) == 0xFC) { + $n = 5; + } // 1111110b + else { + return ''; + } // Does not match any model + for ($j = 0; $j < $n; ++$j) { // n bytes matching 10bbbbbb follow ? + if ((++$i == $length) + || ((ord($string[$i]) & 0xC0) != 0x80) + ) { + $utf8 = false; + + break; + } + } + } + } + if (!$utf8) { + $string = mb_convert_encoding($string, 'UTF-8'); + } + $transliteration = [ + 'IJ' => 'I', 'Ö' => 'O', 'Œ' => 'O', 'Ü' => 'U', 'ä' => 'a', 'æ' => 'a', + 'ij' => 'i', 'ö' => 'o', 'œ' => 'o', 'ü' => 'u', 'ß' => 's', 'ſ' => 's', + 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', + 'Æ' => 'A', 'Ā' => 'A', 'Ą' => 'A', 'Ă' => 'A', 'Ç' => 'C', 'Ć' => 'C', + 'Č' => 'C', 'Ĉ' => 'C', 'Ċ' => 'C', 'Ď' => 'D', 'Đ' => 'D', 'È' => 'E', + 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ē' => 'E', 'Ę' => 'E', 'Ě' => 'E', + 'Ĕ' => 'E', 'Ė' => 'E', 'Ĝ' => 'G', 'Ğ' => 'G', 'Ġ' => 'G', 'Ģ' => 'G', + 'Ĥ' => 'H', 'Ħ' => 'H', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', + 'Ī' => 'I', 'Ĩ' => 'I', 'Ĭ' => 'I', 'Į' => 'I', 'İ' => 'I', 'Ĵ' => 'J', + 'Ķ' => 'K', 'Ľ' => 'K', 'Ĺ' => 'K', 'Ļ' => 'K', 'Ŀ' => 'K', 'Ł' => 'L', + 'Ñ' => 'N', 'Ń' => 'N', 'Ň' => 'N', 'Ņ' => 'N', 'Ŋ' => 'N', 'Ò' => 'O', + 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ø' => 'O', 'Ō' => 'O', 'Ő' => 'O', + 'Ŏ' => 'O', 'Ŕ' => 'R', 'Ř' => 'R', 'Ŗ' => 'R', 'Ś' => 'S', 'Ş' => 'S', + 'Ŝ' => 'S', 'Ș' => 'S', 'Š' => 'S', 'Ť' => 'T', 'Ţ' => 'T', 'Ŧ' => 'T', + 'Ț' => 'T', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ū' => 'U', 'Ů' => 'U', + 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U', 'Ŵ' => 'W', 'Ŷ' => 'Y', + 'Ÿ' => 'Y', 'Ý' => 'Y', 'Ź' => 'Z', 'Ż' => 'Z', 'Ž' => 'Z', 'à' => 'a', + 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ā' => 'a', 'ą' => 'a', 'ă' => 'a', + 'å' => 'a', 'ç' => 'c', 'ć' => 'c', 'č' => 'c', 'ĉ' => 'c', 'ċ' => 'c', + 'ď' => 'd', 'đ' => 'd', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', + 'ē' => 'e', 'ę' => 'e', 'ě' => 'e', 'ĕ' => 'e', 'ė' => 'e', 'ƒ' => 'f', + 'ĝ' => 'g', 'ğ' => 'g', 'ġ' => 'g', 'ģ' => 'g', 'ĥ' => 'h', 'ħ' => 'h', + 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ī' => 'i', 'ĩ' => 'i', + 'ĭ' => 'i', 'į' => 'i', 'ı' => 'i', 'ĵ' => 'j', 'ķ' => 'k', 'ĸ' => 'k', + 'ł' => 'l', 'ľ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'ŀ' => 'l', 'ñ' => 'n', + 'ń' => 'n', 'ň' => 'n', 'ņ' => 'n', 'ʼn' => 'n', 'ŋ' => 'n', 'ò' => 'o', + 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ø' => 'o', 'ō' => 'o', 'ő' => 'o', + 'ŏ' => 'o', 'ŕ' => 'r', 'ř' => 'r', 'ŗ' => 'r', 'ś' => 's', 'š' => 's', + 'ť' => 't', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ū' => 'u', 'ů' => 'u', + 'ű' => 'u', 'ŭ' => 'u', 'ũ' => 'u', 'ų' => 'u', 'ŵ' => 'w', 'ÿ' => 'y', + 'ý' => 'y', 'ŷ' => 'y', 'ż' => 'z', 'ź' => 'z', 'ž' => 'z', 'Α' => 'A', + 'Ά' => 'A', 'Ἀ' => 'A', 'Ἁ' => 'A', 'Ἂ' => 'A', 'Ἃ' => 'A', 'Ἄ' => 'A', + 'Ἅ' => 'A', 'Ἆ' => 'A', 'Ἇ' => 'A', 'ᾈ' => 'A', 'ᾉ' => 'A', 'ᾊ' => 'A', + 'ᾋ' => 'A', 'ᾌ' => 'A', 'ᾍ' => 'A', 'ᾎ' => 'A', 'ᾏ' => 'A', 'Ᾰ' => 'A', + 'Ᾱ' => 'A', 'Ὰ' => 'A', 'ᾼ' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', + 'Ε' => 'E', 'Έ' => 'E', 'Ἐ' => 'E', 'Ἑ' => 'E', 'Ἒ' => 'E', 'Ἓ' => 'E', + 'Ἔ' => 'E', 'Ἕ' => 'E', 'Ὲ' => 'E', 'Ζ' => 'Z', 'Η' => 'I', 'Ή' => 'I', + 'Ἠ' => 'I', 'Ἡ' => 'I', 'Ἢ' => 'I', 'Ἣ' => 'I', 'Ἤ' => 'I', 'Ἥ' => 'I', + 'Ἦ' => 'I', 'Ἧ' => 'I', 'ᾘ' => 'I', 'ᾙ' => 'I', 'ᾚ' => 'I', 'ᾛ' => 'I', + 'ᾜ' => 'I', 'ᾝ' => 'I', 'ᾞ' => 'I', 'ᾟ' => 'I', 'Ὴ' => 'I', 'ῌ' => 'I', + 'Θ' => 'T', 'Ι' => 'I', 'Ί' => 'I', 'Ϊ' => 'I', 'Ἰ' => 'I', 'Ἱ' => 'I', + 'Ἲ' => 'I', 'Ἳ' => 'I', 'Ἴ' => 'I', 'Ἵ' => 'I', 'Ἶ' => 'I', 'Ἷ' => 'I', + 'Ῐ' => 'I', 'Ῑ' => 'I', 'Ὶ' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', + 'Ν' => 'N', 'Ξ' => 'K', 'Ο' => 'O', 'Ό' => 'O', 'Ὀ' => 'O', 'Ὁ' => 'O', + 'Ὂ' => 'O', 'Ὃ' => 'O', 'Ὄ' => 'O', 'Ὅ' => 'O', 'Ὸ' => 'O', 'Π' => 'P', + 'Ρ' => 'R', 'Ῥ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Ύ' => 'Y', + 'Ϋ' => 'Y', 'Ὑ' => 'Y', 'Ὓ' => 'Y', 'Ὕ' => 'Y', 'Ὗ' => 'Y', 'Ῠ' => 'Y', + 'Ῡ' => 'Y', 'Ὺ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'P', 'Ω' => 'O', + 'Ώ' => 'O', 'Ὠ' => 'O', 'Ὡ' => 'O', 'Ὢ' => 'O', 'Ὣ' => 'O', 'Ὤ' => 'O', + 'Ὥ' => 'O', 'Ὦ' => 'O', 'Ὧ' => 'O', 'ᾨ' => 'O', 'ᾩ' => 'O', 'ᾪ' => 'O', + 'ᾫ' => 'O', 'ᾬ' => 'O', 'ᾭ' => 'O', 'ᾮ' => 'O', 'ᾯ' => 'O', 'Ὼ' => 'O', + 'ῼ' => 'O', 'α' => 'a', 'ά' => 'a', 'ἀ' => 'a', 'ἁ' => 'a', 'ἂ' => 'a', + 'ἃ' => 'a', 'ἄ' => 'a', 'ἅ' => 'a', 'ἆ' => 'a', 'ἇ' => 'a', 'ᾀ' => 'a', + 'ᾁ' => 'a', 'ᾂ' => 'a', 'ᾃ' => 'a', 'ᾄ' => 'a', 'ᾅ' => 'a', 'ᾆ' => 'a', + 'ᾇ' => 'a', 'ὰ' => 'a', 'ᾰ' => 'a', 'ᾱ' => 'a', 'ᾲ' => 'a', 'ᾳ' => 'a', + 'ᾴ' => 'a', 'ᾶ' => 'a', 'ᾷ' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', + 'ε' => 'e', 'έ' => 'e', 'ἐ' => 'e', 'ἑ' => 'e', 'ἒ' => 'e', 'ἓ' => 'e', + 'ἔ' => 'e', 'ἕ' => 'e', 'ὲ' => 'e', 'ζ' => 'z', 'η' => 'i', 'ή' => 'i', + 'ἠ' => 'i', 'ἡ' => 'i', 'ἢ' => 'i', 'ἣ' => 'i', 'ἤ' => 'i', 'ἥ' => 'i', + 'ἦ' => 'i', 'ἧ' => 'i', 'ᾐ' => 'i', 'ᾑ' => 'i', 'ᾒ' => 'i', 'ᾓ' => 'i', + 'ᾔ' => 'i', 'ᾕ' => 'i', 'ᾖ' => 'i', 'ᾗ' => 'i', 'ὴ' => 'i', 'ῂ' => 'i', + 'ῃ' => 'i', 'ῄ' => 'i', 'ῆ' => 'i', 'ῇ' => 'i', 'θ' => 't', 'ι' => 'i', + 'ί' => 'i', 'ϊ' => 'i', 'ΐ' => 'i', 'ἰ' => 'i', 'ἱ' => 'i', 'ἲ' => 'i', + 'ἳ' => 'i', 'ἴ' => 'i', 'ἵ' => 'i', 'ἶ' => 'i', 'ἷ' => 'i', 'ὶ' => 'i', + 'ῐ' => 'i', 'ῑ' => 'i', 'ῒ' => 'i', 'ῖ' => 'i', 'ῗ' => 'i', 'κ' => 'k', + 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'k', 'ο' => 'o', 'ό' => 'o', + 'ὀ' => 'o', 'ὁ' => 'o', 'ὂ' => 'o', 'ὃ' => 'o', 'ὄ' => 'o', 'ὅ' => 'o', + 'ὸ' => 'o', 'π' => 'p', 'ρ' => 'r', 'ῤ' => 'r', 'ῥ' => 'r', 'σ' => 's', + 'ς' => 's', 'τ' => 't', 'υ' => 'y', 'ύ' => 'y', 'ϋ' => 'y', 'ΰ' => 'y', + 'ὐ' => 'y', 'ὑ' => 'y', 'ὒ' => 'y', 'ὓ' => 'y', 'ὔ' => 'y', 'ὕ' => 'y', + 'ὖ' => 'y', 'ὗ' => 'y', 'ὺ' => 'y', 'ῠ' => 'y', 'ῡ' => 'y', 'ῢ' => 'y', + 'ῦ' => 'y', 'ῧ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'p', 'ω' => 'o', + 'ώ' => 'o', 'ὠ' => 'o', 'ὡ' => 'o', 'ὢ' => 'o', 'ὣ' => 'o', 'ὤ' => 'o', + 'ὥ' => 'o', 'ὦ' => 'o', 'ὧ' => 'o', 'ᾠ' => 'o', 'ᾡ' => 'o', 'ᾢ' => 'o', + 'ᾣ' => 'o', 'ᾤ' => 'o', 'ᾥ' => 'o', 'ᾦ' => 'o', 'ᾧ' => 'o', 'ὼ' => 'o', + 'ῲ' => 'o', 'ῳ' => 'o', 'ῴ' => 'o', 'ῶ' => 'o', 'ῷ' => 'o', 'А' => 'A', + 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'E', + 'Ж' => 'Z', 'З' => 'Z', 'И' => 'I', 'Й' => 'I', 'К' => 'K', 'Л' => 'L', + 'М' => 'M', 'Н' => 'N', 'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', + 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'K', 'Ц' => 'T', 'Ч' => 'C', + 'Ш' => 'S', 'Щ' => 'S', 'Ы' => 'Y', 'Э' => 'E', 'Ю' => 'Y', 'Я' => 'Y', + 'а' => 'A', 'б' => 'B', 'в' => 'V', 'г' => 'G', 'д' => 'D', 'е' => 'E', + 'ё' => 'E', 'ж' => 'Z', 'з' => 'Z', 'и' => 'I', 'й' => 'I', 'к' => 'K', + 'л' => 'L', 'м' => 'M', 'н' => 'N', 'о' => 'O', 'п' => 'P', 'р' => 'R', + 'с' => 'S', 'т' => 'T', 'у' => 'U', 'ф' => 'F', 'х' => 'K', 'ц' => 'T', + 'ч' => 'C', 'ш' => 'S', 'щ' => 'S', 'ы' => 'Y', 'э' => 'E', 'ю' => 'Y', + 'я' => 'Y', 'ð' => 'd', 'Ð' => 'D', 'þ' => 't', 'Þ' => 'T', 'ა' => 'a', + 'ბ' => 'b', 'გ' => 'g', 'დ' => 'd', 'ე' => 'e', 'ვ' => 'v', 'ზ' => 'z', + 'თ' => 't', 'ი' => 'i', 'კ' => 'k', 'ლ' => 'l', 'მ' => 'm', 'ნ' => 'n', + 'ო' => 'o', 'პ' => 'p', 'ჟ' => 'z', 'რ' => 'r', 'ს' => 's', 'ტ' => 't', + 'უ' => 'u', 'ფ' => 'p', 'ქ' => 'k', 'ღ' => 'g', 'ყ' => 'q', 'შ' => 's', + 'ჩ' => 'c', 'ც' => 't', 'ძ' => 'd', 'წ' => 't', 'ჭ' => 'c', 'ხ' => 'k', + 'ჯ' => 'j', 'ჰ' => 'h', 'ḩ' => 'h', 'ừ' => 'u', 'ế' => 'e', 'ả' => 'a', + 'ị' => 'i', 'ậ' => 'a', 'ệ' => 'e', 'ỉ' => 'i', 'ộ' => 'o', 'ồ' => 'o', + 'ề' => 'e', 'ơ' => 'o', 'ạ' => 'a', 'ẵ' => 'a', 'ư' => 'u', 'ắ' => 'a', + 'ằ' => 'a', 'ầ' => 'a', 'ḑ' => 'd', 'Ḩ' => 'H', 'Ḑ' => 'D', + 'ş' => 's', 'ţ' => 't', 'ễ' => 'e', + ]; + $string = str_replace(array_keys($transliteration), array_values($transliteration), $string); + if (strpos($string = htmlentities($string, ENT_QUOTES, 'UTF-8'), '&') !== false) { + $string = html_entity_decode(preg_replace('~&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|tilde|uml);~i', '$1', $string), ENT_QUOTES, 'UTF-8'); + } + + return $string; +} + +function safe_html(mixed $var, int $flag = ENT_QUOTES): string|array|null +{ + if (!is_array($var)) { + return $var === null + ? null + : htmlspecialchars((string) $var, $flag, 'UTF-8'); + } + $safe_array = []; + foreach ($var as $k => $v) { + $safe_array[$k] = is_array($v) + ? safe_html($v) + : ( + $v === null + ? null + : htmlspecialchars((string) $v, $flag, 'UTF-8') + ); + } + + return $safe_array; +} + +function format_bytes(mixed $bytes, int $round = 1): string +{ + if (!is_numeric($bytes)) { + return ''; + } + if ($bytes < 1000) { + return "$bytes B"; + } + $units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + foreach ($units as $k => $v) { + $multiplier = pow(1000, $k + 1); + $threshold = $multiplier * 1000; + if ($bytes < $threshold) { + $size = round($bytes / $multiplier, $round); + + return "$size $v"; + } + } + + return ''; +} + +function get_bytes(string $size, ?int $cut = null): int +{ + if ($cut == null) { + $suffix = substr($size, -3); + $suffix = preg_match('/([A-Za-z]){3}/', $suffix) ? $suffix : substr($size, -2); + } else { + $suffix = substr($size, $cut); + } + $number = (int) str_replace($suffix, '', $size); + $suffix = strtoupper($suffix); + + $units = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; // Default dec units + + if (strlen($suffix) == 3) { // Convert units to bin + foreach ($units as &$unit) { + $split = str_split($unit); + $unit = $split[0] . 'I' . $split[1]; + } + } + + if (strlen($suffix) == 1) { + $suffix .= 'B'; // Adds missing "B" for shorthand ini notation (Turns 1G into 1GB) + } + if (!in_array($suffix, $units)) { + return $number; + } + $pow_factor = array_search($suffix, $units) + 1; + + return $number * pow(strlen($suffix) == 2 ? 1000 : 1024, $pow_factor); +} + +function bytes_to_mb(int $bytes): float +{ + return round($bytes / pow(10, 6)); +} + +function get_ini_bytes(string $size): int +{ + return get_bytes($size, -1); +} + +function add_trailing_slashes(string $string): string +{ + return add_ending_slash(add_starting_slash($string)); +} + +function add_starting_slash(string $string): string +{ + return '/' . ltrim($string, '/'); +} + +function add_ending_slash(string $string): string +{ + return rtrim($string, '/') . '/'; +} + +function filter_string_polyfill(string $string): string +{ + $str = preg_replace('/\x00|<[^>]*>?/', '', $string); + + return str_replace(["'", '"'], [''', '"'], $str); +} + +function seoUrlfy(string $text): string +{ + $prepare = $text; + $prepare = preg_replace('/[\\\\\/\~\&\!\'\"\?]+/', '', $prepare); + $prepare = preg_replace('/[\s-]+/', ' ', $prepare); + $prepare = str_replace(' ', '-', trim($prepare)); + $prepare = strip_tags($prepare); + + return urlencode($prepare); +} + +function forward_slash(string $string): string +{ + return str_replace('\\', '/', $string); +} + +function relative_to_absolute(string $filepath): string +{ + return str_replace(Config::host()->hostnamePath(), PATH_PUBLIC, forward_slash($filepath)); +} + +function relative_to_url(string $filepath, ?string $root_url = null): string +{ + if (!check_value($root_url)) { + $root_url = URL_APP_PUBLIC; + } + + return str_replace(Config::host()->hostnamePath(), $root_url, forward_slash($filepath)); +} + +function url_to_relative(string $url, ?string $root_url = null): string +{ + if (!check_value($root_url)) { + $root_url = URL_APP_PUBLIC; + } + + return str_replace_first($root_url, Config::host()->hostnamePath(), $url); +} + +function absolute_to_relative(string $filepath): string +{ + return str_replace_first(PATH_PUBLIC, Config::host()->hostnamePath(), forward_slash($filepath)); +} + +function absolute_to_url(string $filepath, ?string $root_url = null) +{ + if (!check_value($root_url)) { + $root_url = URL_APP_PUBLIC; + } + if (PATH_PUBLIC === Config::host()->hostnamePath()) { + return $root_url . ltrim($filepath, '/'); + } + + return str_replace_first(PATH_PUBLIC, $root_url, forward_slash($filepath)); +} + +function url_to_absolute(string $url, ?string $root_url = null) +{ + if (!check_value($root_url)) { + $root_url = URL_APP_PUBLIC; + } + + return str_replace($root_url, PATH_PUBLIC, $url); +} + +function get_app_version(bool $full = true): string +{ + if ($full) { + return APP_VERSION; + } else { + preg_match('/\d\.\d/', APP_VERSION, $return); + + return $return[0]; + } +} + +/** + * @deprecated + */ +function get_app_setting(string $key): mixed +{ + $settingsToEnv = [ + 'asset_storage_account_id' => 'CHEVERETO_ASSET_STORAGE_ACCOUNT_ID', + 'asset_storage_account_name' => 'CHEVERETO_ASSET_STORAGE_ACCOUNT_NAME', + 'asset_storage_bucket' => 'CHEVERETO_ASSET_STORAGE_BUCKET', + 'asset_storage_key' => 'CHEVERETO_ASSET_STORAGE_KEY', + 'asset_storage_name' => 'CHEVERETO_ASSET_STORAGE_NAME', + 'asset_storage_region' => 'CHEVERETO_ASSET_STORAGE_REGION', + 'asset_storage_secret' => 'CHEVERETO_ASSET_STORAGE_SECRET', + 'asset_storage_server' => 'CHEVERETO_ASSET_STORAGE_SERVER', + 'asset_storage_service' => 'CHEVERETO_ASSET_STORAGE_SERVICE', + 'asset_storage_type' => 'CHEVERETO_ASSET_STORAGE_TYPE', + 'asset_storage_url' => 'CHEVERETO_ASSET_STORAGE_URL', + 'db_driver' => 'CHEVERETO_DB_DRIVER', + 'db_host' => 'CHEVERETO_DB_HOST', + 'db_name' => 'CHEVERETO_DB_NAME', + 'db_pass' => 'CHEVERETO_DB_PASS', + 'db_pdo_attrs' => 'CHEVERETO_DB_PDO_ATTRS', + 'db_port' => 'CHEVERETO_DB_PORT', + 'db_table_prefix' => 'CHEVERETO_DB_TABLE_PREFIX', + 'db_user' => 'CHEVERETO_DB_USER', + 'debug_level' => 'CHEVERETO_DEBUG_LEVEL', + 'disable_php_pages' => 'CHEVERETO_DISABLE_PHP_PAGES', + 'disable_update_http' => 'CHEVERETO_DISABLE_UPDATE_HTTP', + 'disable_update_cli' => 'CHEVERETO_DISABLE_UPDATE_CLI', + 'error_log' => 'CHEVERETO_ERROR_LOG', + 'hostname_path' => 'CHEVERETO_HOSTNAME_PATH', + 'hostname' => 'CHEVERETO_HOSTNAME', + 'https' => 'CHEVERETO_HTTPS', + 'image_formats_available' => 'CHEVERETO_IMAGE_FORMATS_AVAILABLE', + 'image_library' => 'CHEVERETO_IMAGE_LIBRARY', + 'session_save_handler' => 'CHEVERETO_SESSION_SAVE_HANDLER', + 'session_save_path' => 'CHEVERETO_SESSION_SAVE_PATH', + ]; + $settingEnv = $settingsToEnv[$key] ?? null; + $env = null; + if (isset($settingEnv) && array_key_exists($settingEnv, $_ENV)) { + $env = getenv($settingEnv); + if ($env === false) { + $env = null; + } else { + switch ($key) { + case 'https': + case 'disable_php_pages': + case 'disable_update_http': + case 'disable_update_cli': + return boolval($env); + + case 'image_formats_available': + return explode(',', $env); + } + } + } + + return $env ?? get_global('settings')[$key] ?? null; +} + +function get_public_url(string $path = ''): string +{ + return get_base_url($path, true); +} + +function get_base_url(string $path = '', bool $public = false): string +{ + $path = sanitize_relative_path($path); + + $base = Config::host()->hostnamePath(); + if ($public) { + $base = URL_APP_PUBLIC; + } + + return $base . ltrim($path, '/'); +} + +function get_current_url(bool $safe = true, array $removeQs = [], bool $protocol = false) +{ + $request_uri = server()['REQUEST_URI'] ?? ''; + $request_path = rtrim(strtok($request_uri, '?') ?: '', '/'); + if ((server()['QUERY_STRING'] ?? false) && $removeQs !== []) { + parse_str(server()['QUERY_STRING'], $parse); + foreach ($removeQs as $v) { + unset($parse[$v]); + } + $querystring = $parse !== [] ? http_build_query($parse) : ''; + $request_uri = $request_path; + if ($querystring !== '') { + $request_uri .= '/?' . $querystring; + } + } + $path = preg_replace('#' . Config::host()->hostnamePath() . '#', '', rtrim($request_uri, '/') . '/', 1); + + return get_base_url(rtrim($path, '/'), $protocol); +} + +function hasEnvDbInfo(): bool +{ + $has = true; + foreach (['HOST', 'PORT', 'NAME', 'USER', 'PASS', 'DRIVER', 'PDO_ATTRS'] as $prop) { + $value = env()['CHEVERETO_DB_' . $prop] ?? ''; + if ($value === '') { + $has = false; + + break; + } + } + + return $has; +} + +function get_regex_match( + string $regex, + string $subject, + string $delimiter = '/', + ?int $key = null +): mixed { + preg_match($delimiter . $regex . $delimiter, $subject, $matches); + if (array_key_exists($key, $matches)) { + return $matches[$key]; + } else { + return $matches; + } +} + +/** @deprecated V4 */ +function logger(string $message): void +{ + if (PHP_SAPI !== 'cli') { + return; + } + fwrite(fopen('php://stdout', 'r+'), $message); +} + +function curlProgress(int $download_size = 0, int $downloaded = 0): void +{ + if ($download_size == 0) { + return; + } + logger(progress_bar($downloaded, $download_size, ' download')); +} + +function progress_bar(int $done, int $total, string $info = "", int $width = 50): string +{ + $perc = (int) round(($done * 100) / $total); + $bar = (int) round(($width * $perc) / 100); + + return sprintf(" %s%%[%s>%s]%s\r", $perc, str_repeat("=", $bar), str_repeat(" ", $width - $bar), $info); +} + +function curlResolveCa(CurlHandle $ch): void +{ + curl_setopt($ch, CURLOPT_CAINFO, CaBundle::getBundledCaBundlePath()); +} + +/** + * Fetch the contents from an URL + * if $file is set the downloaded file will be saved there. + */ +function fetch_url(string $url, string $file = '', array $options = []): string +{ + $showProgress = PHP_SAPI === 'cli' && ($options['progress'] ?? false); + if ($url === '') { + throw new Exception('Missing url'); + } + if (ini_get('allow_url_fopen') !== '1' && !function_exists('curl_init')) { + throw new Exception("cURL isn't installed and allow_url_fopen is disabled. Can't perform HTTP requests."); + } + $fn = (!function_exists('curl_init') ? 'fgc' : 'curl'); + if ($fn == 'curl') { + $ch = curl_init(); + curlResolveCa($ch); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_AUTOREFERER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, PHP_SAPI === 'cli' ? 0 : 120); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FAILONERROR, 0); + curl_setopt($ch, CURLOPT_ENCODING, 'gzip'); + curl_setopt($ch, CURLOPT_VERBOSE, 0); + if ($showProgress) { + curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 'Chevereto\Legacy\G\curlProgress'); + curl_setopt($ch, CURLOPT_NOPROGRESS, 0); + } + if (!empty($options)) { + foreach ($options as $k => $v) { + if (!is_int($k)) { + continue; + } + curl_setopt($ch, $k, $v); + } + } + if ($file !== '') { + $out = fopen($file, 'wb'); + if (!$out) { + throw new Exception("Can't open file for read and write"); + } + curl_setopt($ch, CURLOPT_FILE, $out); + curl_exec($ch); + fclose($out); + } else { + $contents = curl_exec($ch); + } + if ($showProgress) { + logger("\n"); + } + if (curl_errno($ch)) { + $curl_error = curl_error($ch); + curl_close($ch); + + throw new Exception('Curl error ' . $curl_error); + } + if ($file == '') { + curl_close($ch); + + return $contents; + } + } else { + $context = stream_context_create([ + 'http' => ['ignore_errors' => true, 'follow_location' => false], + ]); + $contents = file_get_contents($url, false, $context); + if (!$contents) { + throw new Exception("Can't fetch target URL (file_get_contents)"); + } + if ($file !== '') { + if (file_put_contents($file, $contents) === false) { + throw new Exception("Can't fetch target URL (file_put_contents)"); + } + } else { + return $contents; + } + } + + return $contents ?? ''; +} + +function getUrlHeaders(string $url, array $options = []): array +{ + $ch = curl_init(); + curlResolveCa($ch); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_NOBODY, 1); + curl_setopt($ch, CURLOPT_AUTOREFERER, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36'); + if (is_array($options)) { + foreach ($options as $k => $v) { + curl_setopt($ch, $k, $v); + } + } + $raw = curl_exec($ch); + if (curl_errno($ch)) { + $return['error'] = curl_error($ch); + $return['http_code'] = 500; + } else { + $return = curl_getinfo($ch); + $return['raw'] = $raw; + } + curl_close($ch); + + return $return; +} + +function get_execution_time(): float +{ + return microtime(true) - TIME_EXECUTION_START; +} + +function bcrypt_cost(float $time = 0.2, int $cost = 9): int +{ + do { + ++$cost; + $inicio = microtime(true); + password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]); + $fin = microtime(true); + } while (($fin - $inicio) < $time); + + return $cost; +} + +function is_integer(mixed $var, array $range = []): bool +{ + $options = []; + if (!empty($range) && is_array($range)) { + foreach (['min', 'max'] as $k) { + if (!isset($range[$k])) { + continue; + } + if (is_int($range[$k])) { + $options['options'][$k . '_range'] = $range[$k]; + } + } + } + + return filter_var($var, FILTER_VALIDATE_INT, $options) !== false; +} + +function is_url_web(string $string) +{ + return is_url($string, ['http', 'https']); +} + +function is_url(mixed $string, array $protocols = []): bool +{ + if (!is_string($string)) { + return false; + } + if (strlen($string) !== strlen(mb_convert_encoding($string, 'UTF-8'))) { + return false; + } + + $parsed_url = parse_url($string) ?: []; + if (count($parsed_url) < 2) { // At least scheme and host + return false; + } + $schemes = $protocols !== [] + ? $protocols + : ['http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'svn', 'tel', 'fax', 'xmpp']; + if (!in_array(strtolower($parsed_url['scheme'] ?? ''), $schemes)) { // Must be a valid scheme + return false; + } + if (!array_key_exists('host', $parsed_url)) { // Host must be there + return false; + } + + return true; +} + +function is_https(string $string): bool +{ + return strpos($string, 'https://') !== false; +} + +function is_valid_url(string $string): bool +{ + if (!is_url($string)) { + return false; + } + $url = preg_replace('/^https/', 'http', $string, 1); + if (function_exists('curl_init')) { + $ch = curl_init(); + curlResolveCa($ch); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_NOBODY, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, 0); + curl_setopt($ch, CURLOPT_AUTOREFERER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, PHP_SAPI === 'cli' ? 0 : 120); + $result = curl_exec($ch); + curl_close($ch); + + return $result !== false; + } elseif ((bool) ini_get('allow_url_fopen')) { + $result = file_get_contents($url); + + return $result !== false; + } + + throw new LogicException('Unable to check if URL is valid'); +} + +function is_image_url(mixed $string): bool +{ + if (!is_string($string)) { + return false; + } + + return preg_match('/(?:ftp|https?):\/\/(\w+:\w+@)?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(:[0-9]{1,4}){0,1}|(?:[\w\-]+\.)+[a-z]{2,6})(?:\/[^\/#\?]+)+\.(?:jpe?g|gif|png|bmp|webp)/i', $string) === 1; +} + +function is_development_env(): bool +{ + return false; +} + +function is_windows_os(): bool +{ + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; +} + +function is_animated_image($filename): bool +{ + switch (get_file_extension($filename)) { + case 'gif': + return is_animated_gif($filename); + + case 'png': + return is_animated_png($filename); + + case 'webp': + return is_animated_webp($filename); + } + + return false; +} + +function is_animated_gif($filename): bool +{ + $fh = fopen($filename, 'rb'); + if (!$fh) { + return false; + } + $count = 0; + while (!feof($fh) && $count < 2) { + $chunk = fread($fh, 1024 * 100); + $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches); + } + fclose($fh); + + return $count > 1; +} + +function is_animated_png(string $filename): bool +{ + $img_bytes = file_get_contents($filename); + if ($img_bytes) { + if (strpos( + substr($img_bytes, 0, strpos($img_bytes, 'IDAT')), + 'acTL' + ) !== false) { + return true; + } + } + + return false; +} + +function is_animated_webp(string $filename): bool +{ + $result = false; + $fh = fopen($filename, "rb"); + fseek($fh, 12); + if (fread($fh, 4) === 'VP8X') { + fseek($fh, 20); + $myByte = fread($fh, 1); + $result = ((ord($myByte) >> 1) & 1) ? true : false; + } + fclose($fh); + + return $result; +} + +/** @deprecated V4 */ +function is_writable(string $path): bool +{ + if (\is_writable($path)) { + return true; + } + $testFile = sprintf('%s/%s.tmp', $path, uniqid('data_write_test_')); + $testFile = str_replace('//', '/', $testFile); + + try { + $handle = fopen($testFile, 'w'); + fclose($handle); + } catch (Throwable $e) { + return false; + } + + return unlinkIfExists($testFile); +} + +function get_mimetype(string $file): string +{ + if (function_exists('finfo_open')) { + return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file); + } else { + if (function_exists('mime_content_type')) { + return mime_content_type($file); + } else { + return extension_to_mime(get_file_extension($file)); + } + } +} + +function mime_to_extension(string $mime): string +{ + return [ + 'image/x-windows-bmp' => 'bmp', + 'image/x-ms-bmp' => 'bmp', + 'image/bmp' => 'bmp', + 'image/gif' => 'gif', + 'image/pjpeg' => 'jpeg', + 'image/jpeg' => 'jpeg', + 'image/x-png' => 'png', + 'image/png' => 'png', + 'image/x-tiff' => 'tiff', + 'image/tiff' => 'tiff', + 'image/x-icon' => 'ico', + 'image/vnd.microsoft.icon' => 'ico', + 'image/webp' => 'webp', + ][$mime] ?? ''; +} + +function extension_to_mime(string $ext): string +{ + return [ + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'ico' => 'image/vnd.microsoft.icon', + 'webp' => 'image/webp', + ][$ext] ?? ''; +} + +function get_image_fileinfo(string $file): array +{ + clearstatcache(true, $file); + $info = getimagesize($file); + $filesize = filesize($file); + if (!$info || $filesize === false) { + return []; + } + $mime = strtolower($info['mime']); + $extension = mime_to_extension($mime); + + return [ + 'filename' => basename($file), // image.jpg + 'name' => basename($file, '.' . get_file_extension($file)), // image + 'width' => intval($info[0]), + 'height' => intval($info[1]), + 'ratio' => $info[0] / $info[1], + 'size' => intval($filesize), + 'size_formatted' => format_bytes($filesize), + 'mime' => $mime, + 'extension' => $extension, + 'bits' => $info['bits'] ?? '', + 'channels' => $info['channels'] ?? '', + 'url' => absolute_to_url($file), + 'md5' => md5_file($file), + ]; +} + +function get_file_extension(string $file): string +{ + return strtolower(pathinfo($file, PATHINFO_EXTENSION)); +} + +function get_filename(string $file): string +{ + return basename($file); +} + +function get_basename_without_extension(string $filename): string +{ + $extension = pathinfo($filename, PATHINFO_EXTENSION); + $filename = basename($filename); + + return str_replace_last(".$extension", '', $filename); +} + +function get_pathname_without_extension(string $filename): string +{ + $extension = pathinfo($filename, PATHINFO_EXTENSION); + + return str_replace_last(".$extension", '', $filename); +} + +function change_pathname_extension(string $filename, string $extension): string +{ + $chop = get_pathname_without_extension($filename); + if ($chop == $filename) { + return $filename; + } + + return "$chop.$extension"; +} + +/** + * @param string $method: original | random | mixed | id + * @param string $filename: name of the original file. + * @deprecated V4 + */ +function get_filename_by_method(string $method, string $filename): string +{ + $max_length = 200; // Safe limit, ideally this should be 255 - 4 + $extension = get_file_extension($filename); + $clean_filename = substr($filename, 0, -(strlen($extension) + 1)); + $clean_filename = unaccent_string($clean_filename); // change áéíóú to aeiou + $clean_filename = preg_replace('/\s+/', '-', $clean_filename); // change all spaces with dash + $clean_filename = trim($clean_filename, '-'); // get rid of those ugly dashes + $clean_filename = preg_replace('/[^\.\w\d-]/i', '', $clean_filename); // remove any non alphanumeric, non underscore, non hyphen and non dot + if (strlen($clean_filename) == 0) { + $clean_filename = random_string(32); + } + $unlimited_filename = $clean_filename; // No max_length limit + $capped_filename = substr($clean_filename, 0, $max_length); // 1->200 + + switch ($method) { + default: + case 'original': + $name = $capped_filename; + + break; + case 'random': + $name = random_string(32); + + break; + case 'mixed': + $mixed_chars_length = 16; + $mixed_chars = random_string($mixed_chars_length); + if (strlen($capped_filename) + $mixed_chars_length > $max_length) { + // Bring the scissors Morty + $capped_filename = substr($capped_filename, 0, $max_length - $mixed_chars_length - strlen($capped_filename)); + // Well done Morty you little piece of shit + } + $name = $capped_filename . $mixed_chars; + + break; + case 'id': + $name = $unlimited_filename; + + break; + } + + return $name . '.' . $extension; // 200 + 4 +} + +/** @deprecated V4 */ +function name_unique_file( + string $path, + string $filename, + string $method = 'original' +): string { + $file = $path . get_filename_by_method($method, $filename); + if ($method == 'id') { + return $file; + } + while (file_exists($file)) { + if ($method == 'original') { + $method = 'mixed'; + } + $file = $path . get_filename_by_method($method, $filename); + } + + return $file; +} + +/** @deprecated V4 */ +function imagefilteropacity(GdImage &$img, ?int $opacity): bool +{ + if (!isset($opacity)) { + return false; + } + $opacity /= 100; + $w = imagesx($img); + $h = imagesy($img); + imagealphablending($img, false); + $minalpha = 127; + for ($x = 0; $x < $w; ++$x) { + for ($y = 0; $y < $h; ++$y) { + $alpha = (imagecolorat($img, $x, $y) >> 24) & 0xFF; + if ($alpha < $minalpha) { + $minalpha = $alpha; + } + } + } + for ($x = 0; $x < $w; ++$x) { + for ($y = 0; $y < $h; ++$y) { + $colorxy = imagecolorat($img, $x, $y); + $alpha = ($colorxy >> 24) & 0xFF; + if ($minalpha !== 127) { + $alpha = 127 + 127 * $opacity * ($alpha - 127) / (127 - $minalpha); + } else { + $alpha += 127 * $opacity; + } + $alphacolorxy = imagecolorallocatealpha($img, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha); + if (!imagesetpixel($img, $x, $y, $alphacolorxy)) { + return false; + } + } + } + + return true; +} + +/** @deprecated V4 */ +function image_allocate_transparency(GdImage $image, string $extension): void +{ + if ($extension == 'png') { + imagealphablending($image, false); + imagesavealpha($image, true); + } else { + imagetruecolortopalette($image, true, 255); + imagesavealpha($image, false); + } +} + +/** @deprecated V4 */ +function image_copy_transparency(GdImage $image_source, GdImage $image_target) +{ + $transparent_index = imagecolortransparent($image_source); + $palletsize = imagecolorstotal($image_source); + if ($transparent_index >= 0 and $transparent_index < $palletsize) { + $transparent_color = imagecolorsforindex($image_source, $transparent_index); + $transparent_index = imagecolorallocatealpha($image_target, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue'], 127); + imagefill($image_target, 0, 0, $transparent_index); + imagecolortransparent($image_target, $transparent_index); + } else { + $color = imagecolorallocatealpha($image_target, 0, 0, 0, 127); + imagefill($image_target, 0, 0, $color); + } +} + +/** @deprecated V4 */ +function get_mask_bit_shift(int $bits, string $mask) +{ + if ($bits == 16) { + // 555 + if ($mask == 0x7c00) { + return 7; + } + if ($mask == 0x03e0) { + return 2; + } + // 656 + if ($mask == 0xf800) { + return 8; + } + if ($mask == 0x07e0) { + return 3; + } + } else { + if ($mask == 0xff000000) { + return 24; + } + if ($mask == 0x00ff0000) { + return 16; + } + if ($mask == 0x0000ff00) { + return 8; + } + } + + return 0; +} + +/** @deprecated V4 */ +function imagecreatefrombmp(string $file): GdImage|bool +{ + if (!($fh = fopen($file, 'rb'))) { + trigger_error('imagecreatefrombmp: Can not open ' . $file, E_USER_WARNING); + + return false; + } + $meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14)); + if ($meta['type'] != 19778) { + trigger_error('imagecreatefrombmp: ' . $file . ' is not a bitmap!', E_USER_WARNING); + + return false; + } + $meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40)); + $bytes_read = 40; + if ($meta['headersize'] > $bytes_read) { + $meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12)); + $bytes_read += 12; + } else { + if ($meta['bits'] == 16) { + $meta['rMask'] = 0x7c00; + $meta['gMask'] = 0x03e0; + $meta['bMask'] = 0x001f; + } elseif ($meta['bits'] > 16) { + $meta['rMask'] = 0x00ff0000; + $meta['gMask'] = 0x0000ff00; + $meta['bMask'] = 0x000000ff; + } + } + $meta['bytes'] = $meta['bits'] / 8; + $meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4))); + if ($meta['decal'] == 4) { + $meta['decal'] = 0; + } + if ($meta['imagesize'] < 1) { + $meta['imagesize'] = $meta['filesize'] - $meta['offset']; + if ($meta['imagesize'] < 1) { + $meta['imagesize'] = @filesize($file) - $meta['offset']; + if ($meta['imagesize'] < 1) { + trigger_error('imagecreatefrombmp: Can not obtain filesize of ' . $file . '!', E_USER_WARNING); + + return false; + } + } + } + $meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors']; + $palette = []; + if ($meta['bits'] < 16) { + $palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4)); + if ($palette[1] < 0) { + foreach ($palette as $i => $color) { + $palette[$i] = $color + 16777216; + } + } + } + if ($meta['headersize'] > $bytes_read) { + fread($fh, $meta['headersize'] - $bytes_read); + } + $im = imagecreatetruecolor($meta['width'], $meta['height']); + $data = fread($fh, $meta['imagesize']); + $p = 0; + $vide = chr(0); + $y = $meta['height'] - 1; + $error = 'imagecreatefrombmp: ' . $file . ' has not enough data!'; + while ($y >= 0) { + $x = 0; + while ($x < $meta['width']) { + switch ($meta['bits']) { + case 32: + if (!($part = substr($data, $p, 4))) { + trigger_error($error, E_USER_WARNING); + + return $im; + } + $color = unpack('V', $part); + $color[1] = (($color[1] & $meta['rMask']) >> get_mask_bit_shift(32, $meta['rMask'])) * 65536 + (($color[1] & $meta['gMask']) >> get_mask_bit_shift(32, $meta['gMask'])) * 256 + (($color[1] & $meta['bMask']) >> get_mask_bit_shift(32, $meta['bMask'])); + + break; + case 24: + if (!($part = substr($data, $p, 3))) { + trigger_error($error, E_USER_WARNING); + + return $im; + } + $color = unpack('V', $part . $vide); + $color[1] = (($color[1] & $meta['rMask']) >> get_mask_bit_shift(24, $meta['rMask'])) * 65536 + (($color[1] & $meta['gMask']) >> get_mask_bit_shift(24, $meta['gMask'])) * 256 + (($color[1] & $meta['bMask']) >> get_mask_bit_shift(24, $meta['bMask'])); + + break; + case 16: + if (!($part = substr($data, $p, 2))) { + trigger_error($error, E_USER_WARNING); + + return $im; + } + $color = unpack('v', $part); + $color[1] = (($color[1] & $meta['rMask']) >> get_mask_bit_shift(16, $meta['rMask'])) * 65536 + (($color[1] & $meta['gMask']) >> get_mask_bit_shift(16, $meta['gMask'])) * 256 + (($color[1] & $meta['bMask']) << 3); + + break; + case 8: + $color = unpack('n', $vide . substr($data, $p, 1)); + $color[1] = $palette[$color[1] + 1]; + + break; + case 4: + $color = unpack('n', $vide . substr($data, intval(floor($p)), 1)); + $color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F; + $color[1] = $palette[$color[1] + 1]; + + break; + case 1: + $color = unpack('n', $vide . substr($data, intval(floor($p)), 1)); + switch (($p * 8) % 8) { + case 0: + $color[1] = $color[1] >> 7; + + break; + case 1: + $color[1] = ($color[1] & 0x40) >> 6; + + break; + case 2: + $color[1] = ($color[1] & 0x20) >> 5; + + break; + case 3: + $color[1] = ($color[1] & 0x10) >> 4; + + break; + case 4: + $color[1] = ($color[1] & 0x8) >> 3; + + break; + case 5: + $color[1] = ($color[1] & 0x4) >> 2; + + break; + case 6: + $color[1] = ($color[1] & 0x2) >> 1; + + break; + case 7: + $color[1] = ($color[1] & 0x1); + + break; + } + $color[1] = $palette[$color[1] + 1]; + + break; + default: + trigger_error('imagecreatefrombmp: ' . $file . ' has ' . $meta['bits'] . ' bits and this is not supported!', E_USER_WARNING); + + return false; + } + imagesetpixel($im, $x, $y, $color[1]); + ++$x; + $p += $meta['bytes']; + } + --$y; + $p += $meta['decal']; + } + fclose($fh); + + return $im; +} + +function json_prepare(): void +{ + if (is_development_env()) { + return; + } + if (server()['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') { + json_output(['status_code' => 400]); + } +} + +function json_error(Throwable $e): array +{ + $message = $e->getMessage(); + $code = $e->getCode(); + + return [ + 'status_code' => 400, + 'error' => [ + 'message' => $message, + 'code' => $code, + ], + ]; +} + +function redirect(string $to = '', int $status = 301): void +{ + if (PHP_SAPI === 'cli') { + echo sprintf("> Redirection to $to (%s)", (string) $status) . "\n"; + if (!defined('PHPUNIT_CHEVERETO_TESTSUITE')) { + die(); + } + } + if (!is_url_web($to)) { + $to = get_base_url($to); + } + $to = preg_replace('|[^a-z0-9-~+_.?#=&;,/:%!]|i', '', $to); + if (php_sapi_name() != 'cgi-fcgi') { + set_status_header($status); + } + header("Location: $to"); + if (!defined('PHPUNIT_CHEVERETO_TESTSUITE')) { + die(); + } +} + +function set_status_header(int $code): void +{ + if (headers_sent()) { + return; + } + $desc = get_set_status_header_desc($code); + if (empty($desc)) { + return; + } + $protocol = server()['SERVER_PROTOCOL'] ?? 'HTTP/1.1'; + if ('HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol) { + $protocol = 'HTTP/1.0'; + } + $set_status_header = "$protocol $code $desc"; + header($set_status_header, true, $code); +} + +function get_set_status_header_desc(int $code): string +{ + $codes_to_desc = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 510 => 'Not Extended', + ]; + if (!isset($codes_to_desc[$code])) { + throw new LogicException('Invalid HTTP status code'); + } + + return $codes_to_desc[$code]; +} + +function clean_header_comment(string $string): string +{ + return trim(preg_replace('/\s*(?:\*\/|\?>).*/', '', $string)); +} + +/** + * function xml2array. + * + * This function is part of the PHP manual. + * + * The PHP manual text and comments are covered by the Creative Commons + * Attribution 3.0 License, copyright (c) the PHP Documentation Group + * + * @author k dot antczak at livedata dot pl + * @date 2011-04-22 06:08 UTC + * + * @see http://www.php.net/manual/en/ref.simplexml.php#103617 + * + * @license http://www.php.net/license/index.php#doc-lic + * @license http://creativecommons.org/licenses/by/3.0/ + * @license CC-BY-3.0 + */ +function xml2array(object $xmlObject, array $out = []) +{ + foreach ((array) $xmlObject as $index => $node) { + $out[$index] = (is_object($node)) ? xml2array($node) : $node; + } + + return $out; +} + +function get_domain(string $domain, bool $debug = false): string +{ + $original = $domain = strtolower($domain); + if (filter_var($domain, FILTER_VALIDATE_IP)) { + return $domain; + } + $debug ? print('» Parsing: ' . $original) : false; + $arr = array_slice(array_filter(explode('.', $domain, 4), function ($value) { + return $value !== 'www'; + }), 0); //rebuild array indexes + if (count($arr) > 2) { + $count = count($arr); + $_sub = explode('.', $count === 4 ? $arr[3] : $arr[2]); + $debug ? print(" (parts count: {$count})") : false; + if (count($_sub) === 2) { // two level TLD + $removed = array_shift($arr); + if ($count === 4) { // got a subdomain acting as a domain + $removed = array_shift($arr); + } + $debug ? print("
\n" . '[*] Two level TLD: ' . join('.', $_sub) . ' ') : false; + } elseif (count($_sub) === 1) { // one level TLD + $removed = array_shift($arr); //remove the subdomain + if (strlen($_sub[0]) === 2 && $count === 3) { // TLD domain must be 2 letters + array_unshift($arr, $removed); + } else { + // non country TLD according to IANA + $tlds = [ + 'aero', + 'arpa', + 'asia', + 'biz', + 'cat', + 'com', + 'coop', + 'edu', + 'gov', + 'info', + 'jobs', + 'mil', + 'mobi', + 'museum', + 'name', + 'net', + 'org', + 'post', + 'pro', + 'tel', + 'travel', + 'xxx', + ]; + if (count($arr) > 2 && in_array($_sub[0], $tlds) !== false) { //special TLD don't have a country + array_shift($arr); + } + } + $debug ? print("
\n" . '[*] One level TLD: ' . join('.', $_sub) . ' ') : false; + } else { // more than 3 levels, something is wrong + for ($i = count($_sub); $i > 1; --$i) { + $removed = array_shift($arr); + } + $debug ? print("
\n" . '[*] Three level TLD: ' . join('.', $_sub) . ' ') : false; + } + } elseif (count($arr) === 2) { + $arr0 = array_shift($arr); + if ( + strpos(join('.', $arr), '.') === false + && in_array($arr[0], ['localhost', 'test', 'invalid']) === false + ) { // not a reserved domain + $debug ? print("
\n" . 'Seems invalid domain: ' . join('.', $arr) . ' re-adding: ' . $arr0 . ' ') : false; + // seems invalid domain, restore it + array_unshift($arr, $arr0); + } + } + $debug ? print("
\n" . '« Done parsing: ' . $original . ' as ' . join('.', $arr) . "
\n") : false; + + return join('.', $arr); +} + +function getQsParams(): array +{ + $a = []; + foreach (explode("&", server()["QUERY_STRING"]) as $q) { + $p = explode('=', $q, 2); + $a[$p[0]] = isset($p[1]) ? $p[1] : ''; + } + + return $a; +} + +function unlinkIfExists(string $filename): bool +{ + if (is_dir($filename)) { + throw new LogicException(sprintf('Filename %s is a dir', $filename)); + } + if (file_exists($filename)) { + return unlink($filename); + } + + return true; +} + +function dsq_hmacsha1($data, $key) +{ + $blocksize = 64; + $hashfunc = 'sha1'; + if (strlen($key) > $blocksize) { + $key = pack('H*', $hashfunc($key)); + } + $key = str_pad($key, $blocksize, chr(0x00)); + $ipad = str_repeat(chr(0x36), $blocksize); + $opad = str_repeat(chr(0x5c), $blocksize); + $hmac = pack('H*', $hashfunc(($key ^ $opad) . pack('H*', $hashfunc(($key ^ $ipad) . $data)))); + + return bin2hex($hmac); +} diff --git a/app/src/Legacy/functions-render.php b/app/src/Legacy/functions-render.php new file mode 100644 index 0000000..63d594f --- /dev/null +++ b/app/src/Legacy/functions-render.php @@ -0,0 +1,1093 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevereto\Legacy; + +use Chevereto\Config\Config; +use Chevereto\Legacy\Classes\Album; +use Chevereto\Legacy\Classes\Login; +use Chevereto\Legacy\Classes\Settings; +use Chevereto\Legacy\Classes\User; +use function Chevereto\Legacy\G\absolute_to_url; +use function Chevereto\Legacy\G\add_trailing_slashes; +use function Chevereto\Legacy\G\array_filter_array; +use function Chevereto\Legacy\G\check_value; +use function Chevereto\Legacy\G\dsq_hmacsha1; +use function Chevereto\Legacy\G\get_base_url; +use function Chevereto\Legacy\G\get_current_url; +use function Chevereto\Legacy\G\get_public_url; +use function Chevereto\Legacy\G\get_route_name; +use function Chevereto\Legacy\G\get_route_path; +use function Chevereto\Legacy\G\get_set_status_header_desc; +use Chevereto\Legacy\G\Handler; +use function Chevereto\Legacy\G\include_theme_file; +use function Chevereto\Legacy\G\json_output; +use function Chevereto\Legacy\G\json_prepare; +use function Chevereto\Legacy\G\safe_html; +use function Chevereto\Legacy\G\sanitize_path_slashes; +use function Chevereto\Legacy\G\set_status_header; +use function Chevereto\Legacy\G\str_replace_first; +use function Chevereto\Legacy\G\url_to_relative; +use function Chevereto\Vars\cookie; +use function Chevereto\Vars\env; +use function Chevereto\Vars\server; +use LogicException; + +function get_email_body_str($file) +{ + ob_start(); + include_theme_file($file); + $mail_body = ob_get_contents(); + ob_end_clean(); + + return $mail_body; +} + +function get_theme_inline_code($file, $type = null) +{ + if (!isset($type)) { + $type = pathinfo(rtrim($file, '.php'), PATHINFO_EXTENSION); + } + include_theme_file($file); +} +function show_theme_inline_code($file, $type = null) +{ + include_theme_file($file); +} + +function get_theme_file_url($file, $options = []) +{ + $filepath = PATH_PUBLIC_LEGACY_THEME . $file; + $filepath_override = PATH_PUBLIC_LEGACY_THEME . 'overrides/' . $file; + if (file_exists($filepath_override)) { + $filepath = $filepath_override; + } + + return get_static_url($filepath, $options); +} +function get_static_url($filepath, $options = []) +{ + $options = array_merge(['versionize' => true], $options); + $return = absolute_to_url( + $filepath, + URL_APP_PUBLIC === URL_APP_PUBLIC_STATIC + ? Config::host()->hostnamePath() + : URL_APP_PUBLIC_STATIC + ); + if ($options['versionize']) { + $return = versionize_src($return); + } + + return $return; +} +function theme_file_exists($var) +{ + return file_exists(PATH_PUBLIC_LEGACY_THEME . $var); +} + +function get_html_tags() +{ + $palette = Handler::var('theme_palette_handle'); + $device = 'device-' . (Handler::cond('mobile_device') ? 'mobile' : 'nonmobile'); + $nsfwBlur = 'unsafe-blur-' . (getSetting('theme_nsfw_blur') ? 'on' : 'off'); + $classes = strtr( + '%device %palette %nsfwBlur', + [ + '%device' => $device, + '%palette' => 'palette-' . $palette, + '%nsfwBlur' => $nsfwBlur, + ] + ); + if (getSetting('captcha')) { + $classes .= ' recaptcha recaptcha--' . getSetting('captcha_api'); + } + if (Handler::cond('show_powered_by_footer')) { + $classes .= ' powered-by-footer'; + } + + return get_lang_html_tags() . ' class="' . $classes . '" data-palette="' . $palette . '"'; +} + +function get_lang_html_tags() +{ + $lang = get_language_used(); + + return 'xml:lang="' . $lang['base'] . '" lang="' . $lang['base'] . '" dir="' . $lang['dir'] . '"'; +} + +function get_select_options_html($arr, $selected) +{ + $html = ''; + foreach ($arr as $k => $v) { + $selected = is_bool($selected) ? ($selected ? 1 : 0) : $selected; + $html .= '' . "\n"; + } + + return $html; +} +function get_checkbox_html($options = []) +{ + if (!array_key_exists('name', $options)) { + return 'ERR:CHECKBOX_NAME_MISSING'; + } + $options = array_merge([ + 'value_checked' => 1, + 'value_unchecked' => 0, + 'label' => $options['name'], + 'checked' => false, + ], $options); + $tooltip = isset($options['tooltip']) ? (' rel="tooltip" title="' . $options['tooltip'] . '"') : null; + + return '
' . "\n" . + ' ' . "\n" . + '
'; +} +function get_captcha_component($id = 'g-recaptcha') +{ + return match (getSetting('captcha_api')) { + '2', 'hcaptcha' => ['captcha_html', strtr('
', ['%id' => $id])], + '3' => ['recaptcha_invisible_html', get_captcha_invisible_html()], + default => throw new LogicException('Invalid captcha API'), + }; +} +function get_captcha_invisible_html() +{ + return ''; +} + +function get_share_links(array $share_element = []) +{ + if (function_exists('get_share_links')) { + return \get_share_links($share_element); + } + $share_element = array_merge([ + 'HTML' => '', + 'referer' => get_public_url(), + 'url' => '__url__', + 'image' => '__image__', + 'title' => '__title__', + ], $share_element); + if (!isset($share_element['twitter'])) { + $share_element['twitter'] = getSetting('twitter_account'); + } + $elements = []; + foreach ($share_element as $key => $value) { + if (is_null($value)) { + continue; + } + $elements[$key] = rawurlencode($value); + } + global $share_links_networks; + include_theme_file('custom_hooks/share_links'); + if (!isset($share_links_networks)) { + $share_links_networks = [ + 'share' => [ + 'url' => 'share:title=%TITLE%&url=%URL%', + 'label' => _s('Share'), + 'mobileonly' => true, + ], + 'mail' => [ + 'url' => 'mailto:?subject=%TITLE%&body=%URL%', + 'label' => 'Email', + ], + 'facebook' => [ + 'url' => 'http://www.facebook.com/share.php?u=%URL%', + 'label' => 'Facebook', + ], + 'twitter' => [ + 'url' => 'https://twitter.com/intent/tweet?original_referer=%URL%&url=%URL%&text=%TITLE%' . ($share_element['twitter'] ? '&via=%TWITTER%' : null), + 'label' => 'Twitter', + ], + 'whatsapp' => [ + 'url' => 'whatsapp://send?text=%TITLE% - ' . _s('view on %s', safe_html(getSetting('website_name'))) . ': %URL%', + 'label' => 'WhatsApp', + 'mobileonly' => true, + ], + 'telegram' => [ + 'url' => 'https://t.me/share/url?url=%URL%&text=%TITLE%', + 'label' => 'Telegram', + 'mobileonly' => true, + ], + 'weixin' => [ + 'url' => 'https://api.qrserver.com/v1/create-qr-code/?size=154x154&data=%URL%', + 'label' => '分享到微信', + ], + 'weibo' => [ + 'url' => 'https://service.weibo.com/share/share.php?url=%URL%&title=%TITLE%&pic=%PHOTO_URL%&searchPic=true', + 'label' => '分享到微博', + ], + 'qzone' => [ + 'url' => 'https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=%URL%&pics=%PHOTO_URL%&title=%TITLE%', + 'label' => '分享到QQ空间', + 'icon' => 'star' + ], + 'qq' => [ + 'url' => 'https://connect.qq.com/widget/shareqq/index.html?url=%URL%&summary=%DESCRIPTION%&title=%TITLE%&pics=%PHOTO_URL%', + 'label' => '分享到QQ', + ], + 'reddit' => [ + 'url' => 'http://reddit.com/submit?url=%URL%', + 'label' => 'reddit', + ], + 'vk' => [ + 'url' => 'http://vk.com/share.php?url=%URL%', + 'label' => 'VK', + ], + 'blogger' => [ + 'url' => 'http://www.blogger.com/blog-this.g?n=%TITLE%&source=&b=%HTML%', + 'label' => 'Blogger', + ], + 'tumblr' => [ + 'url' => 'http://www.tumblr.com/share/photo?source=%PHOTO_URL%&caption=%TITLE%&clickthru=%URL%&title=%TITLE%', + 'label' => 'Tumblr.', + ], + 'pinterest' => [ + 'url' => 'http://www.pinterest.com/pin/create/bookmarklet/?media=%PHOTO_URL%&url=%URL%&is_video=false&description=%DESCRIPTION%&title=%TITLE%', + 'label' => 'Pinterest', + ], + ]; + } + $return = []; + $search = ['%URL%', '%TITLE%', '%DESCRIPTION%', '%HTML%', '%PHOTO_URL%', '%TWITTER%']; + $replace = ['url', 'title', 'description', 'HTML', 'image', 'twitter']; + foreach ($share_links_networks as $key => $value) { + for ($i = 0; $i < count($replace); ++$i) { + if (array_key_exists($replace[$i], $elements)) { + $replace[$i] = $elements[$replace[$i]]; + } + } + $value['url'] = str_replace($search, $replace, $value['url']); + $icon = "fab"; + switch ($key) { + case 'share': + $icon = 'fas'; + $key = 'share'; + + break; + case 'mail': + $icon = 'fas'; + $key = 'at'; + + break; + case 'qzone': + $icon = 'fas'; + + break; + } + $iconKey = $value['icon'] ?? $key; + $return[] = '