diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index dad0aec9e1..9c8e20ee5f 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -75,3 +75,35 @@ jobs: - name: Run PHPStan run: composer run phpstan + + phpmd: + name: PHPMD (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + operating-system: ['ubuntu-latest'] + php: ['8.4'] + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Setup PHP with composer and extensions + uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php + with: + php-version: ${{ matrix.php }} + coverage: none + tools: none + + - name: Clone addon repository + run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon + + - name: Install Composer dependencies + uses: "ramsey/composer-install@v2" + + - name: Run PHPMD + run: vendor/bin/phpmd src/ text .phpmd-ruleset.xml --color diff --git a/.gitignore b/.gitignore index bfc5094d9a..b60e96e8df 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ venv/ #Ignore cache files .php_cs.cache .php-cs-fixer.cache +.phpmd.result-cache.php #ignore avatar picture cache path /avatar diff --git a/.phpmd-ruleset.xml b/.phpmd-ruleset.xml new file mode 100644 index 0000000000..3067bea712 --- /dev/null +++ b/.phpmd-ruleset.xml @@ -0,0 +1,26 @@ + + + + PHPMD ruleset for friendica code. + + + + 3 + + + + + + 3 + + + + + + diff --git a/.phpmd-ruleset.xml.license b/.phpmd-ruleset.xml.license new file mode 100644 index 0000000000..985c307f25 --- /dev/null +++ b/.phpmd-ruleset.xml.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/.woodpecker/.phpmd_check.yml b/.woodpecker/.phpmd_check.yml new file mode 100644 index 0000000000..f03e44eec9 --- /dev/null +++ b/.woodpecker/.phpmd_check.yml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +# The phpmd check is just triggered for PRs and pushes to non-stable branches of Friendica +when: + branch: + exclude: [ stable ] + event: [ pull_request, push ] + +steps: + restore_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + restore: true + cache_key: "{{ .Repo.Name }}_php${PHP_MAJOR_VERSION}_{{ arch }}_{{ os }}" + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + + composer_install: + image: friendicaci/php8.3:php8.3.3 + commands: + - mkdir addon # create empty addon folder to appease composer + - export COMPOSER_HOME=.composer + - ./bin/composer.phar install --prefer-dist + + rebuild_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + rebuild: true + cache_key: "{{ .Repo.Name }}_php${PHP_MAJOR_VERSION}_{{ arch }}_{{ os }}" + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + + phpmd: + image: friendicaci/php8.3:php8.3.3 + commands: + - ./bin/composer.phar run phpmd diff --git a/composer.json b/composer.json index 35cd757aec..a2c9eea3c9 100644 --- a/composer.json +++ b/composer.json @@ -70,7 +70,7 @@ "pragmarx/google2fa": "^5.0", "pragmarx/recovery": "^0.2", "psr/clock": "^1.0", - "psr/container": "^2.0", + "psr/container": "^1.1|^2.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.1", "seld/cli-prompt": "^1.0", @@ -154,12 +154,14 @@ "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.3", "php-mock/php-mock-phpunit": "^2.10", + "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9" }, "scripts": { "test": "phpunit", "test:unit": "phpunit -c tests/phpunit.xml --testsuite unit", + "phpmd": "phpmd src/ text .phpmd-ruleset.xml --color --cache", "phpstan": "phpstan analyze --memory-limit 1024M --configuration .phpstan.neon", "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './view/asset/*' -print0 | xargs -0 -n1 php -l", "docker:translate": "docker run --rm -v $PWD:/data -w /data friendicaci/transifex bin/run_xgettext.sh", diff --git a/composer.lock b/composer.lock index 3086ed7be9..e12cc6533c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ee8f9186d271b65b83c2ddbd12c5c03", + "content-hash": "b77bf714197f04022a5feb001bf07852", "packages": [ { "name": "asika/simple-console", @@ -3139,6 +3139,9 @@ "psr", "psr-6" ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, "time": "2016-08-06T20:24:11+00:00" }, { @@ -3187,27 +3190,22 @@ }, { "name": "psr/container", - "version": "2.0.2", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -3232,7 +3230,11 @@ "container-interop", "psr" ], - "time": "2021-11-05T16:47:00+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/event-dispatcher", @@ -3480,6 +3482,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -3697,16 +3702,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.2", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", "shasum": "" }, "require": { @@ -3714,12 +3719,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -3743,6 +3748,9 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3757,7 +3765,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/event-dispatcher", @@ -4591,6 +4599,151 @@ } ], "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, { "name": "dms/phpunit-arraysubset-asserts", "version": "v0.3.1", @@ -4976,6 +5129,69 @@ ], "time": "2024-03-05T20:51:40+00:00" }, + { + "name": "pdepend/pdepend", + "version": "2.16.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -5293,6 +5509,89 @@ ], "time": "2024-02-11T07:24:16+00:00" }, + { + "name": "phpmd/phpmd", + "version": "2.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.16.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "dev", + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2023-12-11T08:22:20+00:00" + }, { "name": "phpstan/phpstan", "version": "2.0.1", @@ -6647,6 +6946,559 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "symfony/config", + "version": "v5.4.46", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "977c88a02d7d3f16904a81907531b19666a08e78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/977c88a02d7d3f16904a81907531b19666a08e78", + "reference": "977c88a02d7d3f16904a81907531b19666a08e78", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v5.4.46" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-30T07:58:02+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v5.4.48", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "e5ca16dee39ef7d63e552ff0bf0a2526a1142c92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e5ca16dee39ef7d63e552ff0bf0a2526a1142c92", + "reference": "e5ca16dee39ef7d63e552ff0bf0a2526a1142c92", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22", + "symfony/service-contracts": "^1.1.6|^2" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<5.3", + "symfony/finder": "<4.4", + "symfony/proxy-manager-bridge": "<4.4", + "symfony/yaml": "<4.4.26" + }, + "provide": { + "psr/container-implementation": "1.0", + "symfony/service-implementation": "1.0|2.0" + }, + "require-dev": { + "symfony/config": "^5.3|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/yaml": "^4.4.26|^5.0|^6.0" + }, + "suggest": { + "symfony/config": "", + "symfony/expression-language": "For using expressions in service container configuration", + "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", + "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", + "symfony/yaml": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.48" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-20T10:51:57+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-22T13:05:35+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.3", diff --git a/src/Model/APContact.php b/src/Model/APContact.php index a364344a51..9c20b5b283 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -162,6 +162,8 @@ class APContact DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES); } + $local_owner = []; + if (DI::baseUrl()->isLocalUrl($url) && ($local_uid = User::getIdForURL($url))) { try { $data = Transmitter::getProfile($local_uid); @@ -205,6 +207,15 @@ class APContact return $fetched_contact; } + return self::compactProfile($apcontact, $compacted, $url, $fetched_contact, $webfinger, $local_owner); + } + + /** + * @param array|bool $fetched_contact + * @param array|bool $local_owner + */ + private static function compactProfile(array $apcontact, array $compacted, string $url, $fetched_contact, bool $webfinger, $local_owner): array + { $apcontact['url'] = $compacted['@id']; $apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid', '@value'); $apcontact['type'] = str_replace('as:', '', JsonLD::fetchElement($compacted, '@type')); diff --git a/src/Model/GServer.php b/src/Model/GServer.php index 20f79f8de7..efbe706844 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -817,6 +817,25 @@ class GServer } // Count the number of known contacts from this server + self::countNumberOfKnownContacts((int) $id, $serverdata); + + if (in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) { + self::discoverRelay($url); + } + + if (!empty($systemactor)) { + $contact = Contact::getByURL($systemactor, true, ['gsid', 'baseurl', 'id', 'network', 'url', 'name']); + DI::logger()->debug('Fetched system actor', ['url' => $url, 'gsid' => $id, 'contact' => $contact]); + } + + return $ret; + } + + /** + * Count the number of known contacts from this server + */ + private static function countNumberOfKnownContacts(int $id, array $serverdata): void + { if (!empty($id) && !in_array($serverdata['network'], [Protocol::PHANTOM, Protocol::FEED])) { $apcontacts = DBA::count('apcontact', ['gsid' => $id]); $contacts = DBA::count('contact', ['uid' => 0, 'gsid' => $id, 'failed' => false]); @@ -842,17 +861,6 @@ class GServer } } } - - if (in_array($serverdata['network'], [Protocol::DFRN, Protocol::DIASPORA])) { - self::discoverRelay($url); - } - - if (!empty($systemactor)) { - $contact = Contact::getByURL($systemactor, true, ['gsid', 'baseurl', 'id', 'network', 'url', 'name']); - DI::logger()->debug('Fetched system actor', ['url' => $url, 'gsid' => $id, 'contact' => $contact]); - } - - return $ret; } /** diff --git a/src/Model/Item.php b/src/Model/Item.php index 00435e2d4d..53184bb1d7 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -580,52 +580,6 @@ class Item } } - /** - * Check if the item array is a duplicate - * - * @param array $item Item record - * @return boolean is it a duplicate? - */ - private static function isDuplicate(array $item): bool - { - // Checking if there is already an item with the same guid - $condition = ['guid' => $item['guid'], 'network' => $item['network'], 'uid' => $item['uid']]; - if (Post::exists($condition)) { - DI::logger()->notice('Found already existing item', $condition); - return true; - } - - $condition = [ - 'uri-id' => $item['uri-id'], 'uid' => $item['uid'], - 'network' => [$item['network'], Protocol::DFRN] - ]; - if (Post::exists($condition)) { - DI::logger()->notice('duplicated item with the same uri found.', $condition); - return true; - } - - // On Friendica and Diaspora the GUID is unique - if (in_array($item['network'], [Protocol::DFRN, Protocol::DIASPORA])) { - $condition = ['guid' => $item['guid'], 'uid' => $item['uid']]; - if (Post::exists($condition)) { - DI::logger()->notice('duplicated item with the same guid found.', $condition); - return true; - } - } - - /* - * Check for already added items. - * There is a timing issue here that sometimes creates double postings. - * An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this. - */ - if (($item['uid'] == 0) && Post::exists(['uri-id' => $item['uri-id'], 'uid' => 0])) { - DI::logger()->notice('Global item already stored.', ['uri-id' => $item['uri-id'], 'network' => $item['network']]); - return true; - } - - return false; - } - /** * Check if the item array is valid * @@ -694,42 +648,6 @@ class Item return true; } - /** - * Return the id of the given item array if it has been stored before - * - * @param array $item Item record - * @return integer Item id or zero on error - */ - private static function getDuplicateID(array $item): int - { - if (empty($item['network']) || in_array($item['network'], Protocol::FEDERATED)) { - $condition = [ - '`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?)', - $item['uri-id'], - $item['uid'], - Protocol::ACTIVITYPUB, - Protocol::DIASPORA, - Protocol::DFRN - ]; - $existing = Post::selectFirst(['id', 'network'], $condition); - if (DBA::isResult($existing)) { - // We only log the entries with a different user id than 0. Otherwise we would have too many false positives - if ($item['uid'] != 0) { - DI::logger()->notice('Item already existed for user', [ - 'uri-id' => $item['uri-id'], - 'uid' => $item['uid'], - 'network' => $item['network'], - 'existing_id' => $existing['id'], - 'existing_network' => $existing['network'] - ]); - } - - return $existing['id']; - } - } - return 0; - } - /** * Fetch the uri-id of the parent for the given uri-id * @@ -750,106 +668,6 @@ class Item return self::getParent($thread_parent['thr-parent-id']); } - /** - * Fetch top-level parent data for the given item array - * - * @param array $item - * @return array item array with parent data - * @throws \Exception - */ - private static function getTopLevelParent(array $item): array - { - $fields = [ - 'uid', 'uri', 'parent-uri', 'id', 'deleted', - 'uri-id', 'parent-uri-id', 'restrictions', 'verb', - 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', - 'wall', 'private', 'origin', 'author-id' - ]; - $condition = ['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => $item['uid']]; - $params = ['order' => ['id' => false]]; - $parent = Post::selectFirst($fields, $condition, $params); - - if (!DBA::isResult($parent) && Post::exists(['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => 0])) { - $stored = Item::storeForUserByUriId($item['thr-parent-id'], $item['uid'], ['post-reason' => Item::PR_COMPLETION]); - if (!$stored && ($item['thr-parent-id'] != $item['parent-uri-id'])) { - $stored = Item::storeForUserByUriId($item['parent-uri-id'], $item['uid'], ['post-reason' => Item::PR_COMPLETION]); - } - if ($stored) { - DI::logger()->info('Stored thread parent item for user', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid'], 'stored' => $stored]); - $parent = Post::selectFirst($fields, $condition, $params); - } - } - - if (!DBA::isResult($parent)) { - DI::logger()->notice('item parent was not found - ignoring item', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); - return []; - } - - if (self::hasRestrictions($item, $parent['author-id'], $parent['restrictions'])) { - DI::logger()->notice('Restrictions apply - ignoring item', ['restrictions' => $parent['restrictions'], 'verb' => $parent['verb'], 'uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); - return []; - } - - if ($parent['uri-id'] == $parent['parent-uri-id']) { - return $parent; - } - - $condition = [ - 'uri-id' => $parent['parent-uri-id'], - 'parent-uri-id' => $parent['parent-uri-id'], - 'uid' => $parent['uid'] - ]; - $params = ['order' => ['id' => false]]; - $toplevel_parent = Post::selectFirst($fields, $condition, $params); - - if (!DBA::isResult($toplevel_parent) && $item['origin']) { - $stored = Item::storeForUserByUriId($item['parent-uri-id'], $item['uid'], ['post-reason' => Item::PR_COMPLETION]); - DI::logger()->info('Stored parent item for user', ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid'], 'stored' => $stored]); - $toplevel_parent = Post::selectFirst($fields, $condition, $params); - } - - if (!DBA::isResult($toplevel_parent)) { - DI::logger()->notice('item top level parent was not found - ignoring item', ['parent-uri-id' => $parent['parent-uri-id'], 'uid' => $parent['uid']]); - return []; - } - - return $toplevel_parent; - } - - /** - * Get the gravity for the given item array - * - * @param array $item - * @return integer gravity - */ - private static function getGravity(array $item): int - { - $activity = DI::activity(); - - if (isset($item['gravity'])) { - return intval($item['gravity']); - } elseif ($item['parent-uri-id'] === $item['uri-id']) { - return self::GRAVITY_PARENT; - } elseif ($activity->match($item['verb'], Activity::POST)) { - return self::GRAVITY_COMMENT; - } elseif ($activity->match($item['verb'], Activity::FOLLOW)) { - return self::GRAVITY_ACTIVITY; - } elseif ($activity->match($item['verb'], Activity::ANNOUNCE)) { - return self::GRAVITY_ACTIVITY; - } - - DI::logger()->info('Unknown gravity for verb', ['verb' => $item['verb']]); - return self::GRAVITY_UNKNOWN; // Should not happen - } - - private static function prepareOriginPost(array $item): array - { - $item = DI::contentItem()->initializePost($item); - $item = DI::contentItem()->finalizePost($item, false); - - return $item; - } - /** * Inserts item record * @@ -860,6 +678,14 @@ class Item */ public static function insert(array $item, int $notify = 0, bool $post_local = true): int { + $itemHelper = new ItemHelper( + DI::contentItem(), + DI::activity(), + DI::logger(), + DI::dba(), + DI::baseUrl(), + ); + $orig_item = $item; $priority = Worker::PRIORITY_HIGH; @@ -868,7 +694,7 @@ class Item // If it is a posting where users should get notifications, then define it as wall posting if ($notify) { - $item = self::prepareOriginPost($item); + $item = $itemHelper->prepareOriginPost($item); if (is_int($notify) && in_array($notify, Worker::PRIORITIES)) { $priority = $notify; @@ -881,23 +707,7 @@ class Item $item['network'] = trim(($item['network'] ?? '') ?: Protocol::PHANTOM); } - $uid = intval($item['uid']); - - $item['guid'] = self::guid($item, $notify); - $item['uri'] = substr(trim($item['uri'] ?? '') ?: self::newURI($item['guid']), 0, 255); - - // Store URI data - $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); - - // Backward compatibility: parent-uri used to be the direct parent uri. - // If it is provided without a thr-parent, it probably is the old behavior. - if (empty($item['thr-parent']) || empty($item['parent-uri'])) { - $item['thr-parent'] = trim($item['thr-parent'] ?? $item['parent-uri'] ?? $item['uri']); - $item['parent-uri'] = $item['thr-parent']; - } - - $item['thr-parent-id'] = ItemURI::getIdByURI($item['thr-parent']); - $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); + $item = $itemHelper->prepareItemData($item, (bool) $notify); // Store conversation data $source = $item['source'] ?? ''; @@ -910,14 +720,14 @@ class Item * We have to check several networks since Friendica posts could be repeated * via Diaspora. */ - $duplicate = self::getDuplicateID($item); + $duplicate = $itemHelper->getDuplicateID($item); if ($duplicate) { return $duplicate; } // Additional duplicate checks /// @todo Check why the first duplication check returns the item number and the second a 0 - if (self::isDuplicate($item)) { + if ($itemHelper->isDuplicate($item)) { return 0; } @@ -927,44 +737,7 @@ class Item $defined_permissions = isset($item['allow_cid']) && isset($item['allow_gid']) && isset($item['deny_cid']) && isset($item['deny_gid']) && isset($item['private']); - $item['wall'] = intval($item['wall'] ?? 0); - $item['extid'] = trim($item['extid'] ?? ''); - $item['author-name'] = trim($item['author-name'] ?? ''); - $item['author-link'] = trim($item['author-link'] ?? ''); - $item['author-avatar'] = trim($item['author-avatar'] ?? ''); - $item['owner-name'] = trim($item['owner-name'] ?? ''); - $item['owner-link'] = trim($item['owner-link'] ?? ''); - $item['owner-avatar'] = trim($item['owner-avatar'] ?? ''); - $item['received'] = (isset($item['received']) ? DateTimeFormat::utc($item['received']) : DateTimeFormat::utcNow()); - $item['created'] = (isset($item['created']) ? DateTimeFormat::utc($item['created']) : $item['received']); - $item['edited'] = (isset($item['edited']) ? DateTimeFormat::utc($item['edited']) : $item['created']); - $item['changed'] = (isset($item['changed']) ? DateTimeFormat::utc($item['changed']) : $item['created']); - $item['commented'] = (isset($item['commented']) ? DateTimeFormat::utc($item['commented']) : $item['created']); - $item['title'] = substr(trim($item['title'] ?? ''), 0, 255); - $item['location'] = trim($item['location'] ?? ''); - $item['coord'] = trim($item['coord'] ?? ''); - $item['visible'] = (isset($item['visible']) ? intval($item['visible']) : 1); - $item['deleted'] = 0; - $item['verb'] = trim($item['verb'] ?? ''); - $item['object-type'] = trim($item['object-type'] ?? ''); - $item['object'] = trim($item['object'] ?? ''); - $item['target-type'] = trim($item['target-type'] ?? ''); - $item['target'] = trim($item['target'] ?? ''); - $item['plink'] = substr(trim($item['plink'] ?? ''), 0, 255); - $item['allow_cid'] = trim($item['allow_cid'] ?? ''); - $item['allow_gid'] = trim($item['allow_gid'] ?? ''); - $item['deny_cid'] = trim($item['deny_cid'] ?? ''); - $item['deny_gid'] = trim($item['deny_gid'] ?? ''); - $item['private'] = intval($item['private'] ?? self::PUBLIC); - $item['body'] = trim($item['body'] ?? ''); - $item['raw-body'] = trim($item['raw-body'] ?? $item['body']); - $item['app'] = trim($item['app'] ?? ''); - $item['origin'] = intval($item['origin'] ?? 0); - $item['postopts'] = trim($item['postopts'] ?? ''); - $item['resource-id'] = trim($item['resource-id'] ?? ''); - $item['event-id'] = intval($item['event-id'] ?? 0); - $item['inform'] = trim($item['inform'] ?? ''); - $item['file'] = trim($item['file'] ?? ''); + $uid = intval($item['uid']); // Communities aren't working with the Diaspora protocol if (($uid != 0) && ($item['network'] == Protocol::DIASPORA)) { @@ -975,33 +748,7 @@ class Item } } - // Items cannot be stored before they happen ... - if ($item['created'] > DateTimeFormat::utcNow()) { - $item['created'] = DateTimeFormat::utcNow(); - } - - // We haven't invented time travel by now. - if ($item['edited'] > DateTimeFormat::utcNow()) { - $item['edited'] = DateTimeFormat::utcNow(); - } - - $item['plink'] = ($item['plink'] ?? '') ?: DI::baseUrl() . '/display/' . urlencode($item['guid']); - - $item['gravity'] = self::getGravity($item); - - $default = [ - 'url' => $item['author-link'], 'name' => $item['author-name'], - 'photo' => $item['author-avatar'], 'network' => $item['network'] - ]; - $item['author-id'] = ($item['author-id'] ?? 0) ?: Contact::getIdForURL($item['author-link'], 0, null, $default); - - $default = [ - 'url' => $item['owner-link'], 'name' => $item['owner-name'], - 'photo' => $item['owner-avatar'], 'network' => $item['network'] - ]; - $item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default); - - $item['post-reason'] = self::getPostReason($item); + $item = $itemHelper->validateItemData($item); // Ensure that there is an avatar cache Contact::checkAvatarCache($item['author-id']); @@ -1022,55 +769,14 @@ class Item } if ($item['gravity'] !== self::GRAVITY_PARENT) { - $toplevel_parent = self::getTopLevelParent($item); + $toplevel_parent = $itemHelper->getTopLevelParent($item); if (empty($toplevel_parent)) { return 0; } - $parent_id = $toplevel_parent['id']; - $item['parent-uri'] = $toplevel_parent['uri']; - $item['parent-uri-id'] = $toplevel_parent['uri-id']; - $item['deleted'] = $toplevel_parent['deleted']; - $item['wall'] = $toplevel_parent['wall']; - - // Reshares have to keep their permissions to allow groups to work - if (!$defined_permissions && (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE))) { - // Don't store the permissions on pure AP posts - $store_permissions = ($item['network'] != Protocol::ACTIVITYPUB) || $item['origin'] || !empty($item['diaspora_signed_text']); - $item['allow_cid'] = $store_permissions ? $toplevel_parent['allow_cid'] : ''; - $item['allow_gid'] = $store_permissions ? $toplevel_parent['allow_gid'] : ''; - $item['deny_cid'] = $store_permissions ? $toplevel_parent['deny_cid'] : ''; - $item['deny_gid'] = $store_permissions ? $toplevel_parent['deny_gid'] : ''; - } - + $parent_id = (int) $toplevel_parent['id']; + $item = $itemHelper->handleToplevelParent($item, $toplevel_parent, $defined_permissions); $parent_origin = $toplevel_parent['origin']; - - // Don't federate received participation messages - if ($item['verb'] != Activity::FOLLOW) { - $item['wall'] = $toplevel_parent['wall']; - } else { - $item['wall'] = false; - // Participations are technical messages, so they are set to "seen" automatically - $item['unseen'] = false; - } - - /* - * If the parent is private, force privacy for the entire conversation - * This differs from the above settings as it subtly allows comments from - * email correspondents to be private even if the overall thread is not. - */ - if (!$defined_permissions && $toplevel_parent['private']) { - $item['private'] = $toplevel_parent['private']; - } - - // If its a post that originated here then tag the thread as "mention" - if ($item['origin'] && $item['uid']) { - DBA::update('post-thread-user', ['mention' => true], ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); - DI::logger()->info('tagged thread as mention', ['parent' => $parent_id, 'parent-uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); - } - - // Update the contact relations - Contact\Relation::store($toplevel_parent['author-id'], $item['author-id'], $item['created']); } else { $parent_id = 0; $parent_origin = $item['origin']; @@ -1344,6 +1050,11 @@ class Item DI::logger()->notice('created item', ['post-id' => $post_user_id, 'uid' => $item['uid'], 'network' => $item['network'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]); + return self::handleCreatedItem($orig_item, $post_user_id, $uid, $notify, $copy_permissions, $parent_origin, $priority, $notify_type, $inserted, $source); + } + + private static function handleCreatedItem(array $orig_item, int $post_user_id, int $uid, int $notify, bool $copy_permissions, $parent_origin, int $priority, string $notify_type, bool $inserted, $source): int + { $posted_item = Post::selectFirst(self::ITEM_FIELDLIST, ['post-user-id' => $post_user_id]); if (!DBA::isResult($posted_item)) { // On failure store the data into a spool file so that the "SpoolPost" worker can try again later. @@ -1484,33 +1195,6 @@ class Item return $post_user_id; } - private static function hasRestrictions(array $item, int $author_id, int $restrictions = null): bool - { - if (empty($restrictions) || ($author_id == $item['author-id'])) { - return false; - } - - // We only have to apply restrictions if the post originates from our server or is federated. - // Every other time we can trust the remote system. - if (!in_array($item['network'], Protocol::FEDERATED) && !$item['origin']) { - return false; - } - - if (($restrictions & self::CANT_REPLY) && ($item['verb'] == Activity::POST)) { - return true; - } - - if (($restrictions & self::CANT_ANNOUNCE) && ($item['verb'] == Activity::ANNOUNCE)) { - return true; - } - - if (($restrictions & self::CANT_LIKE) && in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE, Activity::ATTEND, Activity::ATTENDMAYBE, Activity::ATTENDNO])) { - return true; - } - - return false; - } - private static function reshareChannelPost(int $uri_id, int $reshare_id = 0) { if (!DI::config()->get('system', 'allow_relay_channels')) { @@ -1890,7 +1574,7 @@ class Item * * @param int $uriid * @param int $uid - * @return int + * @return int|null */ private static function GetOriginUidForUriId(int $uriid, int $uid) { diff --git a/src/Model/ItemHelper.php b/src/Model/ItemHelper.php new file mode 100644 index 0000000000..9b585fd544 --- /dev/null +++ b/src/Model/ItemHelper.php @@ -0,0 +1,404 @@ +itemContent = $itemContent; + $this->activity = $activity; + $this->logger = $logger; + $this->database = $database; + $this->baseUrl = $baseURL->__toString(); + } + + public function prepareOriginPost(array $item): array + { + $item = $this->itemContent->initializePost($item); + $item = $this->itemContent->finalizePost($item, false); + + return $item; + } + + public function prepareItemData(array $item, bool $notify): array + { + $item['guid'] = Item::guid($item, $notify); + $item['uri'] = substr(trim($item['uri'] ?? '') ?: Item::newURI($item['guid']), 0, 255); + + // Store URI data + $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); + + // Backward compatibility: parent-uri used to be the direct parent uri. + // If it is provided without a thr-parent, it probably is the old behavior. + if (empty($item['thr-parent']) || empty($item['parent-uri'])) { + $item['thr-parent'] = trim($item['thr-parent'] ?? $item['parent-uri'] ?? $item['uri']); + $item['parent-uri'] = $item['thr-parent']; + } + + $item['thr-parent-id'] = ItemURI::getIdByURI($item['thr-parent']); + $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); + + return $item; + } + + /** + * Return the id of the given item array if it has been stored before + * + * @param array $item Item record + * @return integer Item id or zero on error + */ + public function getDuplicateID(array $item): int + { + if (empty($item['network']) || in_array($item['network'], Protocol::FEDERATED)) { + $condition = [ + '`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?)', + $item['uri-id'], + $item['uid'], + Protocol::ACTIVITYPUB, + Protocol::DIASPORA, + Protocol::DFRN + ]; + + $existing = Post::selectFirst(['id', 'network'], $condition); + + if ($this->database->isResult($existing)) { + // We only log the entries with a different user id than 0. Otherwise we would have too many false positives + if ($item['uid'] != 0) { + $this->logger->notice('Item already existed for user', [ + 'uri-id' => $item['uri-id'], + 'uid' => $item['uid'], + 'network' => $item['network'], + 'existing_id' => $existing['id'], + 'existing_network' => $existing['network'] + ]); + } + + return $existing['id']; + } + } + return 0; + } + + /** + * Check if the item array is a duplicate + * + * @private + * + * @param array $item Item record + * @return boolean is it a duplicate? + */ + public function isDuplicate(array $item): bool + { + // Checking if there is already an item with the same guid + $condition = ['guid' => $item['guid'], 'network' => $item['network'], 'uid' => $item['uid']]; + if (Post::exists($condition)) { + $this->logger->notice('Found already existing item', $condition); + return true; + } + + $condition = [ + 'uri-id' => $item['uri-id'], 'uid' => $item['uid'], + 'network' => [$item['network'], Protocol::DFRN] + ]; + if (Post::exists($condition)) { + $this->logger->notice('duplicated item with the same uri found.', $condition); + return true; + } + + // On Friendica and Diaspora the GUID is unique + if (in_array($item['network'], [Protocol::DFRN, Protocol::DIASPORA])) { + $condition = ['guid' => $item['guid'], 'uid' => $item['uid']]; + if (Post::exists($condition)) { + $this->logger->notice('duplicated item with the same guid found.', $condition); + return true; + } + } + + /* + * Check for already added items. + * There is a timing issue here that sometimes creates double postings. + * An unique index would help - but the limitations of MySQL (maximum size of index values) prevent this. + */ + if (($item['uid'] == 0) && Post::exists(['uri-id' => $item['uri-id'], 'uid' => 0])) { + $this->logger->notice('Global item already stored.', ['uri-id' => $item['uri-id'], 'network' => $item['network']]); + return true; + } + + return false; + } + + public function validateItemData(array $item): array + { + $item['wall'] = intval($item['wall'] ?? 0); + $item['extid'] = trim($item['extid'] ?? ''); + $item['author-name'] = trim($item['author-name'] ?? ''); + $item['author-link'] = trim($item['author-link'] ?? ''); + $item['author-avatar'] = trim($item['author-avatar'] ?? ''); + $item['owner-name'] = trim($item['owner-name'] ?? ''); + $item['owner-link'] = trim($item['owner-link'] ?? ''); + $item['owner-avatar'] = trim($item['owner-avatar'] ?? ''); + $item['received'] = (isset($item['received']) ? DateTimeFormat::utc($item['received']) : DateTimeFormat::utcNow()); + $item['created'] = (isset($item['created']) ? DateTimeFormat::utc($item['created']) : $item['received']); + $item['edited'] = (isset($item['edited']) ? DateTimeFormat::utc($item['edited']) : $item['created']); + $item['changed'] = (isset($item['changed']) ? DateTimeFormat::utc($item['changed']) : $item['created']); + $item['commented'] = (isset($item['commented']) ? DateTimeFormat::utc($item['commented']) : $item['created']); + $item['title'] = substr(trim($item['title'] ?? ''), 0, 255); + $item['location'] = trim($item['location'] ?? ''); + $item['coord'] = trim($item['coord'] ?? ''); + $item['visible'] = (isset($item['visible']) ? intval($item['visible']) : 1); + $item['deleted'] = 0; + $item['verb'] = trim($item['verb'] ?? ''); + $item['object-type'] = trim($item['object-type'] ?? ''); + $item['object'] = trim($item['object'] ?? ''); + $item['target-type'] = trim($item['target-type'] ?? ''); + $item['target'] = trim($item['target'] ?? ''); + $item['plink'] = substr(trim($item['plink'] ?? ''), 0, 255); + $item['allow_cid'] = trim($item['allow_cid'] ?? ''); + $item['allow_gid'] = trim($item['allow_gid'] ?? ''); + $item['deny_cid'] = trim($item['deny_cid'] ?? ''); + $item['deny_gid'] = trim($item['deny_gid'] ?? ''); + $item['private'] = intval($item['private'] ?? Item::PUBLIC); + $item['body'] = trim($item['body'] ?? ''); + $item['raw-body'] = trim($item['raw-body'] ?? $item['body']); + $item['app'] = trim($item['app'] ?? ''); + $item['origin'] = intval($item['origin'] ?? 0); + $item['postopts'] = trim($item['postopts'] ?? ''); + $item['resource-id'] = trim($item['resource-id'] ?? ''); + $item['event-id'] = intval($item['event-id'] ?? 0); + $item['inform'] = trim($item['inform'] ?? ''); + $item['file'] = trim($item['file'] ?? ''); + + // Items cannot be stored before they happen ... + if ($item['created'] > DateTimeFormat::utcNow()) { + $item['created'] = DateTimeFormat::utcNow(); + } + + // We haven't invented time travel by now. + if ($item['edited'] > DateTimeFormat::utcNow()) { + $item['edited'] = DateTimeFormat::utcNow(); + } + + $item['plink'] = ($item['plink'] ?? '') ?: $this->baseUrl . '/display/' . urlencode($item['guid']); + + $item['gravity'] = $this->getGravity($item); + + if ($item['gravity'] === Item::GRAVITY_UNKNOWN) { + $this->logger->info('Unknown gravity for verb', ['verb' => $item['verb']]); + } + + $default = [ + 'url' => $item['author-link'], 'name' => $item['author-name'], + 'photo' => $item['author-avatar'], 'network' => $item['network'] + ]; + $item['author-id'] = ($item['author-id'] ?? 0) ?: Contact::getIdForURL($item['author-link'], 0, null, $default); + + $default = [ + 'url' => $item['owner-link'], 'name' => $item['owner-name'], + 'photo' => $item['owner-avatar'], 'network' => $item['network'] + ]; + $item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default); + + $item['post-reason'] = Item::getPostReason($item); + + return $item; + } + + /** + * Fetch top-level parent data for the given item array + * + * @param array $item + * @return array item array with parent data + * @throws \Exception + */ + public function getTopLevelParent(array $item): array + { + $fields = [ + 'uid', 'uri', 'parent-uri', 'id', 'deleted', + 'uri-id', 'parent-uri-id', 'restrictions', 'verb', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', + 'wall', 'private', 'origin', 'author-id' + ]; + $condition = ['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => $item['uid']]; + $params = ['order' => ['id' => false]]; + $parent = Post::selectFirst($fields, $condition, $params); + + if (!$this->database->isResult($parent) && Post::exists(['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => 0])) { + $stored = Item::storeForUserByUriId($item['thr-parent-id'], $item['uid'], ['post-reason' => Item::PR_COMPLETION]); + if (!$stored && ($item['thr-parent-id'] != $item['parent-uri-id'])) { + $stored = Item::storeForUserByUriId($item['parent-uri-id'], $item['uid'], ['post-reason' => Item::PR_COMPLETION]); + } + if ($stored) { + $this->logger->info('Stored thread parent item for user', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid'], 'stored' => $stored]); + $parent = Post::selectFirst($fields, $condition, $params); + } + } + + if (!$this->database->isResult($parent)) { + $this->logger->notice('item parent was not found - ignoring item', ['uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); + return []; + } + + if ($this->hasRestrictions($item, $parent['author-id'], $parent['restrictions'])) { + $this->logger->notice('Restrictions apply - ignoring item', ['restrictions' => $parent['restrictions'], 'verb' => $parent['verb'], 'uri-id' => $item['uri-id'], 'thr-parent-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); + return []; + } + + if ($parent['uri-id'] == $parent['parent-uri-id']) { + return $parent; + } + + $condition = [ + 'uri-id' => $parent['parent-uri-id'], + 'parent-uri-id' => $parent['parent-uri-id'], + 'uid' => $parent['uid'] + ]; + $params = ['order' => ['id' => false]]; + $toplevel_parent = Post::selectFirst($fields, $condition, $params); + + if (!$this->database->isResult($toplevel_parent) && $item['origin']) { + $stored = Item::storeForUserByUriId($item['parent-uri-id'], $item['uid'], ['post-reason' => Item::PR_COMPLETION]); + $this->logger->info('Stored parent item for user', ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid'], 'stored' => $stored]); + $toplevel_parent = Post::selectFirst($fields, $condition, $params); + } + + if (!$this->database->isResult($toplevel_parent)) { + $this->logger->notice('item top level parent was not found - ignoring item', ['parent-uri-id' => $parent['parent-uri-id'], 'uid' => $parent['uid']]); + return []; + } + + return $toplevel_parent; + } + + public function handleToplevelParent(array $item, array $toplevel_parent, bool $defined_permissions): array + { + $parent_id = (int) $toplevel_parent['id']; + $item['parent-uri'] = $toplevel_parent['uri']; + $item['parent-uri-id'] = $toplevel_parent['uri-id']; + $item['deleted'] = $toplevel_parent['deleted']; + $item['wall'] = $toplevel_parent['wall']; + + // Reshares have to keep their permissions to allow groups to work + if (!$defined_permissions && (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE))) { + // Don't store the permissions on pure AP posts + $store_permissions = ($item['network'] != Protocol::ACTIVITYPUB) || $item['origin'] || !empty($item['diaspora_signed_text']); + $item['allow_cid'] = $store_permissions ? $toplevel_parent['allow_cid'] : ''; + $item['allow_gid'] = $store_permissions ? $toplevel_parent['allow_gid'] : ''; + $item['deny_cid'] = $store_permissions ? $toplevel_parent['deny_cid'] : ''; + $item['deny_gid'] = $store_permissions ? $toplevel_parent['deny_gid'] : ''; + } + + // Don't federate received participation messages + if ($item['verb'] != Activity::FOLLOW) { + $item['wall'] = $toplevel_parent['wall']; + } else { + $item['wall'] = false; + // Participations are technical messages, so they are set to "seen" automatically + $item['unseen'] = false; + } + + /* + * If the parent is private, force privacy for the entire conversation + * This differs from the above settings as it subtly allows comments from + * email correspondents to be private even if the overall thread is not. + */ + if (!$defined_permissions && $toplevel_parent['private']) { + $item['private'] = $toplevel_parent['private']; + } + + // If its a post that originated here then tag the thread as "mention" + if ($item['origin'] && $item['uid']) { + $this->database->update('post-thread-user', ['mention' => true], ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); + $this->logger->info('tagged thread as mention', ['parent' => $parent_id, 'parent-uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); + } + + // Update the contact relations + Contact\Relation::store($toplevel_parent['author-id'], $item['author-id'], $item['created']); + + return $item; + } + + private function hasRestrictions(array $item, int $author_id, int $restrictions = null): bool + { + if (empty($restrictions) || ($author_id == $item['author-id'])) { + return false; + } + + // We only have to apply restrictions if the post originates from our server or is federated. + // Every other time we can trust the remote system. + if (!in_array($item['network'], Protocol::FEDERATED) && !$item['origin']) { + return false; + } + + if (($restrictions & Item::CANT_REPLY) && ($item['verb'] == Activity::POST)) { + return true; + } + + if (($restrictions & Item::CANT_ANNOUNCE) && ($item['verb'] == Activity::ANNOUNCE)) { + return true; + } + + if (($restrictions & Item::CANT_LIKE) && in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE, Activity::ATTEND, Activity::ATTENDMAYBE, Activity::ATTENDNO])) { + return true; + } + + return false; + } + + /** + * Get the gravity for the given item array + * + * @return int gravity + */ + private function getGravity(array $item): int + { + if (isset($item['gravity'])) { + return intval($item['gravity']); + } elseif ($item['parent-uri-id'] === $item['uri-id']) { + return Item::GRAVITY_PARENT; + } elseif ($this->activity->match($item['verb'], Activity::POST)) { + return Item::GRAVITY_COMMENT; + } elseif ($this->activity->match($item['verb'], Activity::FOLLOW)) { + return Item::GRAVITY_ACTIVITY; + } elseif ($this->activity->match($item['verb'], Activity::ANNOUNCE)) { + return Item::GRAVITY_ACTIVITY; + } + + return Item::GRAVITY_UNKNOWN; // Should not happen + } +} diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 890ae4f529..712b8954d4 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -807,25 +807,45 @@ class Transmitter } } + $data = self::filterReceiverData($data, $item['author-link']); + + $receivers = ['to' => array_values($data['to']), 'cc' => array_values($data['cc']), 'bto' => array_values($data['bto']), 'bcc' => array_values($data['bcc']), 'audience' => array_values($data['audience'])]; + + if (!$blindcopy) { + unset($receivers['bto']); + unset($receivers['bcc']); + } + + if (!$blindcopy && count($receivers['audience']) == 1) { + $receivers['audience'] = $receivers['audience'][0]; + } elseif (!$receivers['audience']) { + unset($receivers['audience']); + } + + return $receivers; + } + + private static function filterReceiverData(array $data, string $author_link): array + { $data['to'] = array_unique($data['to']); $data['cc'] = array_unique($data['cc']); $data['bto'] = array_unique($data['bto']); $data['bcc'] = array_unique($data['bcc']); $data['audience'] = array_unique($data['audience']); - if (($key = array_search($item['author-link'], $data['to'])) !== false) { + if (($key = array_search($author_link, $data['to'])) !== false) { unset($data['to'][$key]); } - if (($key = array_search($item['author-link'], $data['cc'])) !== false) { + if (($key = array_search($author_link, $data['cc'])) !== false) { unset($data['cc'][$key]); } - if (($key = array_search($item['author-link'], $data['bto'])) !== false) { + if (($key = array_search($author_link, $data['bto'])) !== false) { unset($data['bto'][$key]); } - if (($key = array_search($item['author-link'], $data['bcc'])) !== false) { + if (($key = array_search($author_link, $data['bcc'])) !== false) { unset($data['bcc'][$key]); } @@ -859,20 +879,7 @@ class Transmitter } } - $receivers = ['to' => array_values($data['to']), 'cc' => array_values($data['cc']), 'bto' => array_values($data['bto']), 'bcc' => array_values($data['bcc']), 'audience' => array_values($data['audience'])]; - - if (!$blindcopy) { - unset($receivers['bto']); - unset($receivers['bcc']); - } - - if (!$blindcopy && count($receivers['audience']) == 1) { - $receivers['audience'] = $receivers['audience'][0]; - } elseif (!$receivers['audience']) { - unset($receivers['audience']); - } - - return $receivers; + return $data; } /** diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index fa1ea3b302..2f9be305f1 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -9,6 +9,8 @@ namespace Friendica\Protocol; use DOMDocument; use DOMElement; +use DOMNode; +use DOMNodeList; use DOMXPath; use Friendica\App; use Friendica\Contact\LocalRelationship\Entity\LocalRelationship; @@ -67,12 +69,12 @@ class Feed return []; } + $basepath = ''; + if (!empty($contact['poll'])) { - $basepath = $contact['poll']; + $basepath = (string) $contact['poll']; } elseif (!empty($contact['url'])) { - $basepath = $contact['url']; - } else { - $basepath = ''; + $basepath = (string) $contact['url']; } $doc = new DOMDocument(); @@ -287,6 +289,77 @@ class Feed $total_items = $max_items; } + $postings = self::importOlderEntries($entries, $total_items, $header, $author, $contact, $importer, $xpath, $atomns, $basepath, $dryRun); + + if (!empty($postings)) { + $min_posting = DI::config()->get('system', 'minimum_posting_interval', 0); + $total = count($postings); + if ($total > 1) { + // Posts shouldn't be delayed more than a day + $interval = min(1440, self::getPollInterval($contact)); + $delay = max(round(($interval * 60) / $total), 60 * $min_posting); + DI::logger()->info('Got posting delay', ['delay' => $delay, 'interval' => $interval, 'items' => $total, 'cid' => $contact['id'], 'url' => $contact['url']]); + } else { + $delay = 0; + } + + $post_delay = 0; + + foreach ($postings as $posting) { + if ($delay > 0) { + $publish_time = time() + $post_delay; + $post_delay += $delay; + } else { + $publish_time = time(); + } + + $last_publish = DI::pConfig()->get($posting['item']['uid'], 'system', 'last_publish', 0, true); + $next_publish = max($last_publish + (60 * $min_posting), time()); + if ($publish_time < $next_publish) { + $publish_time = $next_publish; + } + $publish_at = date(DateTimeFormat::MYSQL, $publish_time); + + if (Post\Delayed::add($posting['item']['uri'], $posting['item'], $posting['notify'], Post\Delayed::PREPARED, $publish_at, $posting['taglist'], $posting['attachments'])) { + DI::pConfig()->set($posting['item']['uid'], 'system', 'last_publish', $publish_time); + } + } + } + + if (!$dryRun && DI::config()->get('system', 'adjust_poll_frequency')) { + self::adjustPollFrequency($contact, $creation_dates); + } + + return ['header' => $author, 'items' => $items]; + } + + private static function getTitleFromItemOrEntry(array $item, DOMXPath $xpath, string $atomns, ?DOMNode $entry): string + { + $title = (string) $item['title']; + + if (empty($title)) { + $title = XML::getFirstNodeValue($xpath, $atomns . ':title/text()', $entry); + } + + if (empty($title)) { + $title = XML::getFirstNodeValue($xpath, 'title/text()', $entry); + } + + if (empty($title)) { + $title = XML::getFirstNodeValue($xpath, 'rss:title/text()', $entry); + } + + if (empty($title)) { + $title = XML::getFirstNodeValue($xpath, 'itunes:title/text()', $entry); + } + + $title = trim(html_entity_decode($title, ENT_QUOTES, 'UTF-8')); + + return $title; + } + + private static function importOlderEntries(DOMNodeList $entries, int $total_items, array $header, array $author, array $contact, array $importer, DOMXPath $xpath, string $atomns, string $basepath, bool $dryRun): array + { $postings = []; // Importing older entries first @@ -386,23 +459,7 @@ class Feed } } - if (empty($item['title'])) { - $item['title'] = XML::getFirstNodeValue($xpath, $atomns . ':title/text()', $entry); - } - - if (empty($item['title'])) { - $item['title'] = XML::getFirstNodeValue($xpath, 'title/text()', $entry); - } - - if (empty($item['title'])) { - $item['title'] = XML::getFirstNodeValue($xpath, 'rss:title/text()', $entry); - } - - if (empty($item['title'])) { - $item['title'] = XML::getFirstNodeValue($xpath, 'itunes:title/text()', $entry); - } - - $item['title'] = trim(html_entity_decode($item['title'], ENT_QUOTES, 'UTF-8')); + $item['title'] = self::getTitleFromItemOrEntry($item, $xpath, $atomns, $entry); $published = XML::getFirstNodeValue($xpath, $atomns . ':published/text()', $entry); @@ -705,46 +762,7 @@ class Feed } } - if (!empty($postings)) { - $min_posting = DI::config()->get('system', 'minimum_posting_interval', 0); - $total = count($postings); - if ($total > 1) { - // Posts shouldn't be delayed more than a day - $interval = min(1440, self::getPollInterval($contact)); - $delay = max(round(($interval * 60) / $total), 60 * $min_posting); - DI::logger()->info('Got posting delay', ['delay' => $delay, 'interval' => $interval, 'items' => $total, 'cid' => $contact['id'], 'url' => $contact['url']]); - } else { - $delay = 0; - } - - $post_delay = 0; - - foreach ($postings as $posting) { - if ($delay > 0) { - $publish_time = time() + $post_delay; - $post_delay += $delay; - } else { - $publish_time = time(); - } - - $last_publish = DI::pConfig()->get($posting['item']['uid'], 'system', 'last_publish', 0, true); - $next_publish = max($last_publish + (60 * $min_posting), time()); - if ($publish_time < $next_publish) { - $publish_time = $next_publish; - } - $publish_at = date(DateTimeFormat::MYSQL, $publish_time); - - if (Post\Delayed::add($posting['item']['uri'], $posting['item'], $posting['notify'], Post\Delayed::PREPARED, $publish_at, $posting['taglist'], $posting['attachments'])) { - DI::pConfig()->set($posting['item']['uid'], 'system', 'last_publish', $publish_time); - } - } - } - - if (!$dryRun && DI::config()->get('system', 'adjust_poll_frequency')) { - self::adjustPollFrequency($contact, $creation_dates); - } - - return ['header' => $author, 'items' => $items]; + return $postings; } /** diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index b89c94b993..2cfc48efbc 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2025.02-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-09 20:13+0000\n" +"POT-Creation-Date: 2025-02-11 07:41+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -45,7 +45,7 @@ msgid "Item not found." msgstr "" #: mod/item.php:447 mod/message.php:54 mod/message.php:100 mod/notes.php:34 -#: mod/photos.php:132 mod/photos.php:624 src/Model/Event.php:506 +#: mod/photos.php:131 mod/photos.php:623 src/Model/Event.php:506 #: src/Module/Attach.php:40 src/Module/BaseApi.php:90 #: src/Module/BaseNotifications.php:83 src/Module/BaseSettings.php:38 #: src/Module/Calendar/Event/API.php:75 src/Module/Calendar/Event/Form.php:70 @@ -289,8 +289,8 @@ msgstr "" msgid "Please wait" msgstr "" -#: mod/message.php:189 mod/message.php:343 mod/photos.php:655 -#: mod/photos.php:775 mod/photos.php:1052 mod/photos.php:1093 +#: mod/message.php:189 mod/message.php:343 mod/photos.php:654 +#: mod/photos.php:774 mod/photos.php:1051 mod/photos.php:1093 #: mod/photos.php:1149 mod/photos.php:1229 #: src/Module/Calendar/Event/Form.php:236 src/Module/Contact/Advanced.php:118 #: src/Module/Contact/Profile.php:376 @@ -377,7 +377,7 @@ msgstr "" msgid "Save" msgstr "" -#: mod/photos.php:51 mod/photos.php:114 mod/photos.php:534 +#: mod/photos.php:50 mod/photos.php:113 mod/photos.php:533 #: src/Model/Event.php:498 src/Model/Profile.php:211 #: src/Module/Calendar/Export.php:60 src/Module/Calendar/Show.php:62 #: src/Module/Feed.php:52 src/Module/HCard.php:37 @@ -389,100 +389,100 @@ msgstr "" msgid "User not found." msgstr "" -#: mod/photos.php:88 src/Module/BaseProfile.php:53 +#: mod/photos.php:87 src/Module/BaseProfile.php:53 #: src/Module/Profile/Photos.php:372 msgid "Photo Albums" msgstr "" -#: mod/photos.php:89 src/Module/Profile/Photos.php:373 +#: mod/photos.php:88 src/Module/Profile/Photos.php:373 #: src/Module/Profile/Photos.php:393 msgid "Recent Photos" msgstr "" -#: mod/photos.php:91 mod/photos.php:823 src/Module/Profile/Photos.php:375 +#: mod/photos.php:90 mod/photos.php:822 src/Module/Profile/Photos.php:375 #: src/Module/Profile/Photos.php:395 msgid "Upload New Photos" msgstr "" -#: mod/photos.php:103 src/Module/BaseSettings.php:60 +#: mod/photos.php:102 src/Module/BaseSettings.php:60 #: src/Module/Profile/Photos.php:356 msgid "everybody" msgstr "" -#: mod/photos.php:139 +#: mod/photos.php:138 msgid "Contact information unavailable" msgstr "" -#: mod/photos.php:168 +#: mod/photos.php:167 msgid "Album not found." msgstr "" -#: mod/photos.php:224 +#: mod/photos.php:223 msgid "Album successfully deleted" msgstr "" -#: mod/photos.php:226 +#: mod/photos.php:225 msgid "Album was empty." msgstr "" -#: mod/photos.php:257 +#: mod/photos.php:256 msgid "Failed to delete the photo." msgstr "" -#: mod/photos.php:501 +#: mod/photos.php:500 msgid "a photo" msgstr "" -#: mod/photos.php:501 +#: mod/photos.php:500 #, php-format msgid "%1$s was tagged in %2$s by %3$s" msgstr "" -#: mod/photos.php:538 src/Module/Conversation/Community.php:148 +#: mod/photos.php:537 src/Module/Conversation/Community.php:148 #: src/Module/Directory.php:34 src/Module/Profile/Photos.php:290 #: src/Module/Search/Index.php:50 msgid "Public access denied." msgstr "" -#: mod/photos.php:543 +#: mod/photos.php:542 msgid "No photos selected" msgstr "" -#: mod/photos.php:671 +#: mod/photos.php:670 #, php-format msgid "The maximum accepted image size is %s" msgstr "" -#: mod/photos.php:678 +#: mod/photos.php:677 msgid "Upload Photos" msgstr "" -#: mod/photos.php:682 mod/photos.php:771 +#: mod/photos.php:681 mod/photos.php:770 msgid "New album name: " msgstr "" -#: mod/photos.php:683 +#: mod/photos.php:682 msgid "or select existing album:" msgstr "" -#: mod/photos.php:684 +#: mod/photos.php:683 msgid "Do not show a status post for this upload" msgstr "" -#: mod/photos.php:687 mod/photos.php:1048 src/Content/Conversation.php:391 +#: mod/photos.php:686 mod/photos.php:1047 src/Content/Conversation.php:391 #: src/Module/Calendar/Event/Form.php:239 src/Module/Post/Edit.php:174 msgid "Permissions" msgstr "" -#: mod/photos.php:752 +#: mod/photos.php:751 msgid "Do you really want to delete this photo album and all its photos?" msgstr "" -#: mod/photos.php:753 mod/photos.php:776 +#: mod/photos.php:752 mod/photos.php:775 msgid "Delete Album" msgstr "" -#: mod/photos.php:754 mod/photos.php:854 src/Content/Conversation.php:406 +#: mod/photos.php:753 mod/photos.php:853 src/Content/Conversation.php:406 #: src/Module/Contact/Follow.php:158 src/Module/Contact/Revoke.php:92 #: src/Module/Contact/Unfollow.php:112 #: src/Module/Media/Attachment/Browser.php:64 @@ -492,99 +492,99 @@ msgstr "" msgid "Cancel" msgstr "" -#: mod/photos.php:780 +#: mod/photos.php:779 msgid "Edit Album" msgstr "" -#: mod/photos.php:781 +#: mod/photos.php:780 msgid "Drop Album" msgstr "" -#: mod/photos.php:785 +#: mod/photos.php:784 msgid "Show Newest First" msgstr "" -#: mod/photos.php:787 +#: mod/photos.php:786 msgid "Show Oldest First" msgstr "" -#: mod/photos.php:808 src/Module/Profile/Photos.php:343 +#: mod/photos.php:807 src/Module/Profile/Photos.php:343 msgid "View Photo" msgstr "" -#: mod/photos.php:840 +#: mod/photos.php:839 msgid "Permission denied. Access to this item may be restricted." msgstr "" -#: mod/photos.php:842 +#: mod/photos.php:841 msgid "Photo not available" msgstr "" -#: mod/photos.php:852 +#: mod/photos.php:851 msgid "Do you really want to delete this photo?" msgstr "" -#: mod/photos.php:853 mod/photos.php:1053 +#: mod/photos.php:852 mod/photos.php:1052 msgid "Delete Photo" msgstr "" -#: mod/photos.php:951 +#: mod/photos.php:950 msgid "View photo" msgstr "" -#: mod/photos.php:953 +#: mod/photos.php:952 msgid "Edit photo" msgstr "" -#: mod/photos.php:954 +#: mod/photos.php:953 msgid "Delete photo" msgstr "" -#: mod/photos.php:955 +#: mod/photos.php:954 msgid "Use as profile photo" msgstr "" -#: mod/photos.php:962 +#: mod/photos.php:961 msgid "Private Photo" msgstr "" -#: mod/photos.php:968 +#: mod/photos.php:967 msgid "View Full Size" msgstr "" -#: mod/photos.php:1021 +#: mod/photos.php:1020 msgid "Tags: " msgstr "" -#: mod/photos.php:1024 +#: mod/photos.php:1023 msgid "[Select tags to remove]" msgstr "" -#: mod/photos.php:1039 +#: mod/photos.php:1038 msgid "New album name" msgstr "" -#: mod/photos.php:1040 +#: mod/photos.php:1039 msgid "Caption" msgstr "" -#: mod/photos.php:1041 +#: mod/photos.php:1040 msgid "Add a Tag" msgstr "" -#: mod/photos.php:1041 +#: mod/photos.php:1040 msgid "Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping" msgstr "" -#: mod/photos.php:1042 +#: mod/photos.php:1041 msgid "Do not rotate" msgstr "" -#: mod/photos.php:1043 +#: mod/photos.php:1042 msgid "Rotate CW (right)" msgstr "" -#: mod/photos.php:1044 +#: mod/photos.php:1043 msgid "Rotate CCW (left)" msgstr "" @@ -646,7 +646,7 @@ msgstr "" msgid "Map" msgstr "" -#: src/App.php:442 +#: src/App.php:451 msgid "Apologies but the website is unavailable at the moment." msgstr "" @@ -752,17 +752,17 @@ msgstr "" msgid "Close" msgstr "" -#: src/App/Router.php:286 +#: src/App/Router.php:287 #, php-format msgid "Method not allowed for this module. Allowed method(s): %s" msgstr "" -#: src/App/Router.php:288 src/Module/HTTPException/PageNotFound.php:35 +#: src/App/Router.php:289 src/Module/HTTPException/PageNotFound.php:35 #: src/Module/Stats.php:56 msgid "Page not found." msgstr "" -#: src/App/Router.php:300 +#: src/App/Router.php:301 msgid "You must be logged in to use addons. " msgstr "" @@ -779,19 +779,19 @@ msgid "All contacts" msgstr "" #: src/BaseModule.php:440 src/Content/Conversation/Factory/Channel.php:32 -#: src/Content/Widget.php:254 src/Core/ACL.php:182 src/Module/Contact.php:394 +#: src/Content/Widget.php:257 src/Core/ACL.php:182 src/Module/Contact.php:394 #: src/Module/Privacy/PermissionTooltip.php:150 #: src/Module/Privacy/PermissionTooltip.php:172 #: src/Module/Settings/Channels.php:146 msgid "Followers" msgstr "" -#: src/BaseModule.php:445 src/Content/Widget.php:255 src/Module/Contact.php:397 +#: src/BaseModule.php:445 src/Content/Widget.php:258 src/Module/Contact.php:397 #: src/Module/Settings/Channels.php:145 msgid "Following" msgstr "" -#: src/BaseModule.php:450 src/Content/Widget.php:256 src/Module/Contact.php:400 +#: src/BaseModule.php:450 src/Content/Widget.php:259 src/Module/Contact.php:400 msgid "Mutual friends" msgstr "" @@ -799,15 +799,15 @@ msgstr "" msgid "Common" msgstr "" -#: src/Console/Addon.php:163 src/Console/Addon.php:187 +#: src/Console/Addon.php:164 src/Console/Addon.php:188 msgid "Addon not found" msgstr "" -#: src/Console/Addon.php:167 +#: src/Console/Addon.php:168 msgid "Addon already enabled" msgstr "" -#: src/Console/Addon.php:191 +#: src/Console/Addon.php:192 msgid "Addon already disabled" msgstr "" @@ -1701,7 +1701,7 @@ msgstr "" msgid "Network Widgets" msgstr "" -#: src/Content/Feature.php:126 src/Content/Widget.php:230 +#: src/Content/Feature.php:126 src/Content/Widget.php:233 #: src/Model/Circle.php:587 src/Module/Contact.php:380 #: src/Module/Welcome.php:62 msgid "Circles" @@ -1713,7 +1713,7 @@ msgstr "" #: src/Content/Feature.php:127 src/Content/GroupManager.php:128 #: src/Content/Nav.php:274 src/Content/Text/HTML.php:868 -#: src/Content/Widget.php:555 src/Model/User.php:1393 +#: src/Content/Widget.php:558 src/Model/User.php:1393 msgid "Groups" msgstr "" @@ -1721,7 +1721,7 @@ msgstr "" msgid "Display posts that have been distributed by the selected group." msgstr "" -#: src/Content/Feature.php:128 src/Content/Widget.php:524 +#: src/Content/Feature.php:128 src/Content/Widget.php:527 msgid "Archives" msgstr "" @@ -1729,7 +1729,7 @@ msgstr "" msgid "Display an archive where posts can be selected by month and year." msgstr "" -#: src/Content/Feature.php:129 src/Content/Widget.php:303 +#: src/Content/Feature.php:129 src/Content/Widget.php:306 msgid "Protocols" msgstr "" @@ -1737,7 +1737,7 @@ msgstr "" msgid "Display posts with the selected protocols." msgstr "" -#: src/Content/Feature.php:130 src/Content/Widget.php:561 +#: src/Content/Feature.php:130 src/Content/Widget.php:564 #: src/Module/Settings/Account.php:391 msgid "Account Types" msgstr "" @@ -1746,7 +1746,7 @@ msgstr "" msgid "Display posts done by accounts with the selected account type." msgstr "" -#: src/Content/Feature.php:131 src/Content/Widget.php:610 +#: src/Content/Feature.php:131 src/Content/Widget.php:613 #: src/Module/Admin/Site.php:463 src/Module/BaseSettings.php:113 #: src/Module/Settings/Channels.php:211 src/Module/Settings/Display.php:313 msgid "Channels" @@ -1764,7 +1764,7 @@ msgstr "" msgid "Display posts that contain subscribed hashtags." msgstr "" -#: src/Content/Feature.php:133 src/Content/Widget.php:333 +#: src/Content/Feature.php:133 src/Content/Widget.php:336 msgid "Saved Folders" msgstr "" @@ -1824,12 +1824,12 @@ msgstr "" msgid "External link to group" msgstr "" -#: src/Content/GroupManager.php:134 src/Content/Widget.php:530 +#: src/Content/GroupManager.php:134 src/Content/Widget.php:533 msgid "show less" msgstr "" -#: src/Content/GroupManager.php:135 src/Content/Widget.php:425 -#: src/Content/Widget.php:531 +#: src/Content/GroupManager.php:135 src/Content/Widget.php:428 +#: src/Content/Widget.php:534 msgid "show more" msgstr "" @@ -1837,7 +1837,7 @@ msgstr "" msgid "Create new group" msgstr "" -#: src/Content/Item.php:321 src/Model/Item.php:3296 +#: src/Content/Item.php:321 src/Model/Item.php:2980 msgid "event" msgstr "" @@ -1845,7 +1845,7 @@ msgstr "" msgid "status" msgstr "" -#: src/Content/Item.php:330 src/Model/Item.php:3298 +#: src/Content/Item.php:330 src/Model/Item.php:2982 #: src/Module/Post/Tag/Add.php:109 msgid "photo" msgstr "" @@ -1922,7 +1922,7 @@ msgstr "" msgid "Search Text" msgstr "" -#: src/Content/Item.php:440 src/Content/Widget.php:66 +#: src/Content/Item.php:440 src/Content/Widget.php:65 #: src/Model/Contact.php:1247 src/Model/Contact.php:1259 #: src/Module/Contact/Follow.php:152 view/theme/vier/theme.php:183 msgid "Connect/Follow" @@ -2102,7 +2102,7 @@ msgstr "" msgid "People directory" msgstr "" -#: src/Content/Nav.php:294 src/Module/BaseAdmin.php:71 +#: src/Content/Nav.php:294 src/Module/BaseAdmin.php:70 #: src/Module/BaseModeration.php:97 msgid "Information" msgstr "" @@ -2112,7 +2112,7 @@ msgid "Information about this friendica instance" msgstr "" #: src/Content/Nav.php:297 src/Module/Admin/Tos.php:64 -#: src/Module/BaseAdmin.php:81 src/Module/Register.php:169 +#: src/Module/BaseAdmin.php:80 src/Module/Register.php:169 #: src/Module/Tos.php:87 msgid "Terms of Service" msgstr "" @@ -2178,7 +2178,7 @@ msgstr "" msgid "Manage other pages" msgstr "" -#: src/Content/Nav.php:323 src/Module/Admin/Addons/Details.php:98 +#: src/Content/Nav.php:323 src/Module/Admin/Addons/Details.php:101 #: src/Module/Admin/Themes/Details.php:85 src/Module/BaseSettings.php:177 #: src/Module/Welcome.php:38 view/theme/frio/theme.php:231 msgid "Settings" @@ -2192,7 +2192,7 @@ msgstr "" msgid "Manage/edit friends and contacts" msgstr "" -#: src/Content/Nav.php:330 src/Module/BaseAdmin.php:105 +#: src/Content/Nav.php:330 src/Module/BaseAdmin.php:114 msgid "Admin" msgstr "" @@ -2247,8 +2247,8 @@ msgstr "" msgid "%2$s %3$s" msgstr "" -#: src/Content/Text/BBCode.php:926 src/Model/Item.php:4103 -#: src/Model/Item.php:4109 src/Model/Item.php:4110 +#: src/Content/Text/BBCode.php:926 src/Model/Item.php:3787 +#: src/Model/Item.php:3793 src/Model/Item.php:3794 msgid "Link to source" msgstr "" @@ -2285,129 +2285,129 @@ msgstr "" msgid "Follow" msgstr "" -#: src/Content/Widget.php:37 +#: src/Content/Widget.php:36 msgid "Add New Contact" msgstr "" -#: src/Content/Widget.php:38 +#: src/Content/Widget.php:37 msgid "Enter address or web location" msgstr "" -#: src/Content/Widget.php:39 +#: src/Content/Widget.php:38 msgid "Example: bob@example.com, http://example.com/barbara" msgstr "" -#: src/Content/Widget.php:41 +#: src/Content/Widget.php:40 msgid "Connect" msgstr "" -#: src/Content/Widget.php:58 +#: src/Content/Widget.php:57 #, php-format msgid "%d invitation available" msgid_plural "%d invitations available" msgstr[0] "" msgstr[1] "" -#: src/Content/Widget.php:64 view/theme/vier/theme.php:181 +#: src/Content/Widget.php:63 view/theme/vier/theme.php:181 msgid "Find People" msgstr "" -#: src/Content/Widget.php:65 view/theme/vier/theme.php:182 +#: src/Content/Widget.php:64 view/theme/vier/theme.php:182 msgid "Enter name or interest" msgstr "" -#: src/Content/Widget.php:67 view/theme/vier/theme.php:184 +#: src/Content/Widget.php:66 view/theme/vier/theme.php:184 msgid "Examples: Robert Morgenstein, Fishing" msgstr "" -#: src/Content/Widget.php:68 src/Module/Contact.php:440 +#: src/Content/Widget.php:67 src/Module/Contact.php:440 #: src/Module/Directory.php:82 view/theme/vier/theme.php:185 msgid "Find" msgstr "" -#: src/Content/Widget.php:69 src/Module/Contact/Suggestions.php:59 +#: src/Content/Widget.php:68 src/Module/Contact/Suggestions.php:59 #: view/theme/vier/theme.php:186 msgid "Friend Suggestions" msgstr "" -#: src/Content/Widget.php:70 view/theme/vier/theme.php:187 +#: src/Content/Widget.php:69 view/theme/vier/theme.php:187 msgid "Similar Interests" msgstr "" -#: src/Content/Widget.php:71 view/theme/vier/theme.php:188 +#: src/Content/Widget.php:70 view/theme/vier/theme.php:188 msgid "Random Profile" msgstr "" -#: src/Content/Widget.php:72 view/theme/vier/theme.php:189 +#: src/Content/Widget.php:71 view/theme/vier/theme.php:189 msgid "Invite Friends" msgstr "" -#: src/Content/Widget.php:73 src/Module/Directory.php:74 +#: src/Content/Widget.php:72 src/Module/Directory.php:74 #: view/theme/vier/theme.php:190 msgid "Global Directory" msgstr "" -#: src/Content/Widget.php:75 view/theme/vier/theme.php:192 +#: src/Content/Widget.php:74 view/theme/vier/theme.php:192 msgid "Local Directory" msgstr "" -#: src/Content/Widget.php:232 +#: src/Content/Widget.php:235 msgid "Everyone" msgstr "" -#: src/Content/Widget.php:257 src/Module/Contact.php:403 +#: src/Content/Widget.php:260 src/Module/Contact.php:403 msgid "No relationship" msgstr "" -#: src/Content/Widget.php:262 +#: src/Content/Widget.php:265 msgid "Relationships" msgstr "" -#: src/Content/Widget.php:264 src/Module/Circle.php:281 +#: src/Content/Widget.php:267 src/Module/Circle.php:281 #: src/Module/Contact.php:324 msgid "All Contacts" msgstr "" -#: src/Content/Widget.php:305 +#: src/Content/Widget.php:308 msgid "All Protocols" msgstr "" -#: src/Content/Widget.php:335 src/Content/Widget.php:366 +#: src/Content/Widget.php:338 src/Content/Widget.php:369 msgid "Everything" msgstr "" -#: src/Content/Widget.php:364 +#: src/Content/Widget.php:367 msgid "Categories" msgstr "" -#: src/Content/Widget.php:421 +#: src/Content/Widget.php:424 #, php-format msgid "%d contact in common" msgid_plural "%d contacts in common" msgstr[0] "" msgstr[1] "" -#: src/Content/Widget.php:532 +#: src/Content/Widget.php:535 msgid "On this date" msgstr "" -#: src/Content/Widget.php:552 +#: src/Content/Widget.php:555 msgid "Persons" msgstr "" -#: src/Content/Widget.php:553 +#: src/Content/Widget.php:556 msgid "Organisations" msgstr "" -#: src/Content/Widget.php:554 src/Model/Contact.php:1757 +#: src/Content/Widget.php:557 src/Model/Contact.php:1757 msgid "News" msgstr "" -#: src/Content/Widget.php:556 +#: src/Content/Widget.php:559 msgid "Relays" msgstr "" -#: src/Content/Widget.php:563 src/Module/Moderation/BaseUsers.php:58 +#: src/Content/Widget.php:566 src/Module/Moderation/BaseUsers.php:58 msgid "All" msgstr "" @@ -2836,7 +2836,7 @@ msgstr "" msgid "Could not connect to database." msgstr "" -#: src/Core/L10n.php:426 src/Model/Item.php:2339 +#: src/Core/L10n.php:426 src/Model/Item.php:2023 msgid "Undetermined" msgstr "" @@ -3224,7 +3224,7 @@ msgstr "" msgid "Disallowed profile URL." msgstr "" -#: src/Model/Contact.php:3096 src/Module/Friendica.php:88 +#: src/Model/Contact.php:3096 src/Module/Friendica.php:90 msgid "Blocked domain" msgstr "" @@ -3378,92 +3378,92 @@ msgstr "" msgid "Happy Birthday %s" msgstr "" -#: src/Model/Item.php:2346 +#: src/Model/Item.php:2030 #, php-format msgid "%s (%s - %s): %s" msgstr "" -#: src/Model/Item.php:2348 +#: src/Model/Item.php:2032 #, php-format msgid "%s (%s): %s" msgstr "" -#: src/Model/Item.php:2351 +#: src/Model/Item.php:2035 #, php-format msgid "" "Detected languages in this post:\n" "%s" msgstr "" -#: src/Model/Item.php:3300 +#: src/Model/Item.php:2984 msgid "activity" msgstr "" -#: src/Model/Item.php:3302 +#: src/Model/Item.php:2986 msgid "comment" msgstr "" -#: src/Model/Item.php:3305 src/Module/Post/Tag/Add.php:109 +#: src/Model/Item.php:2989 src/Module/Post/Tag/Add.php:109 msgid "post" msgstr "" -#: src/Model/Item.php:3478 +#: src/Model/Item.php:3162 #, php-format msgid "%s is blocked" msgstr "" -#: src/Model/Item.php:3480 +#: src/Model/Item.php:3164 #, php-format msgid "%s is ignored" msgstr "" -#: src/Model/Item.php:3482 +#: src/Model/Item.php:3166 #, php-format msgid "Content from %s is collapsed" msgstr "" -#: src/Model/Item.php:3486 +#: src/Model/Item.php:3170 msgid "Sensitive content" msgstr "" -#: src/Model/Item.php:4003 +#: src/Model/Item.php:3687 msgid "bytes" msgstr "" -#: src/Model/Item.php:4034 +#: src/Model/Item.php:3718 #, php-format msgid "%2$s (%3$d%%, %1$d vote)" msgid_plural "%2$s (%3$d%%, %1$d votes)" msgstr[0] "" msgstr[1] "" -#: src/Model/Item.php:4036 +#: src/Model/Item.php:3720 #, php-format msgid "%2$s (%1$d vote)" msgid_plural "%2$s (%1$d votes)" msgstr[0] "" msgstr[1] "" -#: src/Model/Item.php:4041 +#: src/Model/Item.php:3725 #, php-format msgid "%d voter. Poll end: %s" msgid_plural "%d voters. Poll end: %s" msgstr[0] "" msgstr[1] "" -#: src/Model/Item.php:4043 +#: src/Model/Item.php:3727 #, php-format msgid "%d voter." msgid_plural "%d voters." msgstr[0] "" msgstr[1] "" -#: src/Model/Item.php:4045 +#: src/Model/Item.php:3729 #, php-format msgid "Poll end: %s" msgstr "" -#: src/Model/Item.php:4086 src/Model/Item.php:4087 +#: src/Model/Item.php:3770 src/Model/Item.php:3771 msgid "View on separate page" msgstr "" @@ -3574,7 +3574,7 @@ msgstr "" msgid "Title/Description:" msgstr "" -#: src/Model/Profile.php:793 src/Module/Admin/Summary.php:174 +#: src/Model/Profile.php:793 src/Module/Admin/Summary.php:184 #: src/Module/Moderation/Report/Create.php:266 #: src/Module/Moderation/Summary.php:65 msgid "Summary" @@ -3860,71 +3860,71 @@ msgstr "" msgid "User with delegates can't be removed, please remove delegate users first" msgstr "" -#: src/Module/Admin/Addons/Details.php:49 +#: src/Module/Admin/Addons/Details.php:48 msgid "Addon not found." msgstr "" -#: src/Module/Admin/Addons/Details.php:60 src/Module/Admin/Addons/Index.php:41 +#: src/Module/Admin/Addons/Details.php:59 src/Module/Admin/Addons/Index.php:43 #, php-format msgid "Addon %s disabled." msgstr "" -#: src/Module/Admin/Addons/Details.php:63 src/Module/Admin/Addons/Index.php:43 +#: src/Module/Admin/Addons/Details.php:62 src/Module/Admin/Addons/Index.php:45 #, php-format msgid "Addon %s enabled." msgstr "" -#: src/Module/Admin/Addons/Details.php:72 +#: src/Module/Admin/Addons/Details.php:71 #: src/Module/Admin/Themes/Details.php:38 msgid "Disable" msgstr "" -#: src/Module/Admin/Addons/Details.php:75 +#: src/Module/Admin/Addons/Details.php:74 #: src/Module/Admin/Themes/Details.php:41 src/Module/Settings/Display.php:341 msgid "Enable" msgstr "" -#: src/Module/Admin/Addons/Details.php:95 src/Module/Admin/Addons/Index.php:59 +#: src/Module/Admin/Addons/Details.php:98 src/Module/Admin/Addons/Index.php:77 #: src/Module/Admin/Federation.php:213 src/Module/Admin/Logs/Settings.php:74 #: src/Module/Admin/Logs/View.php:71 src/Module/Admin/Queue.php:59 #: src/Module/Admin/Site.php:446 src/Module/Admin/Storage.php:124 -#: src/Module/Admin/Summary.php:173 src/Module/Admin/Themes/Details.php:82 +#: src/Module/Admin/Summary.php:183 src/Module/Admin/Themes/Details.php:82 #: src/Module/Admin/Themes/Index.php:103 src/Module/Admin/Tos.php:63 #: src/Module/Moderation/Users/Create.php:47 #: src/Module/Moderation/Users/Pending.php:82 msgid "Administration" msgstr "" -#: src/Module/Admin/Addons/Details.php:96 src/Module/Admin/Addons/Index.php:60 -#: src/Module/BaseAdmin.php:78 src/Module/BaseSettings.php:127 +#: src/Module/Admin/Addons/Details.php:99 src/Module/Admin/Addons/Index.php:78 +#: src/Module/BaseAdmin.php:77 src/Module/BaseSettings.php:127 msgid "Addons" msgstr "" -#: src/Module/Admin/Addons/Details.php:97 +#: src/Module/Admin/Addons/Details.php:100 #: src/Module/Admin/Themes/Details.php:84 msgid "Toggle" msgstr "" -#: src/Module/Admin/Addons/Details.php:104 +#: src/Module/Admin/Addons/Details.php:113 #: src/Module/Admin/Themes/Details.php:92 msgid "Author: " msgstr "" -#: src/Module/Admin/Addons/Details.php:105 +#: src/Module/Admin/Addons/Details.php:114 #: src/Module/Admin/Themes/Details.php:93 msgid "Maintainer: " msgstr "" -#: src/Module/Admin/Addons/Index.php:34 +#: src/Module/Admin/Addons/Index.php:35 msgid "Addons reloaded" msgstr "" -#: src/Module/Admin/Addons/Index.php:45 +#: src/Module/Admin/Addons/Index.php:47 #, php-format msgid "Addon %s failed to install." msgstr "" -#: src/Module/Admin/Addons/Index.php:61 src/Module/Admin/Features.php:69 +#: src/Module/Admin/Addons/Index.php:79 src/Module/Admin/Features.php:69 #: src/Module/Admin/Logs/Settings.php:76 src/Module/Admin/Site.php:449 #: src/Module/Admin/Themes/Index.php:105 src/Module/Admin/Tos.php:72 #: src/Module/Settings/Account.php:507 src/Module/Settings/Addons.php:64 @@ -3936,11 +3936,11 @@ msgstr "" msgid "Save Settings" msgstr "" -#: src/Module/Admin/Addons/Index.php:62 +#: src/Module/Admin/Addons/Index.php:80 msgid "Reload active addons" msgstr "" -#: src/Module/Admin/Addons/Index.php:66 +#: src/Module/Admin/Addons/Index.php:84 #, php-format msgid "There are currently no addons available on your node. You can find the official addon repository at %1$s." msgstr "" @@ -4088,7 +4088,7 @@ msgstr[1] "" msgid "This page offers you some numbers to the known part of the federated social network your Friendica node is part of. These numbers are not complete but only reflect the part of the network your node is aware of." msgstr "" -#: src/Module/Admin/Federation.php:214 src/Module/BaseAdmin.php:73 +#: src/Module/Admin/Federation.php:214 src/Module/BaseAdmin.php:72 msgid "Federation Statistics" msgstr "" @@ -4112,8 +4112,8 @@ msgstr "" msgid "PHP log currently disabled." msgstr "" -#: src/Module/Admin/Logs/Settings.php:75 src/Module/BaseAdmin.php:88 -#: src/Module/BaseAdmin.php:89 +#: src/Module/Admin/Logs/Settings.php:75 src/Module/BaseAdmin.php:87 +#: src/Module/BaseAdmin.php:88 msgid "Logs" msgstr "" @@ -4161,7 +4161,7 @@ msgstr "" msgid "Couldn't open %1$s log file.
Check to see if file %1$s is readable." msgstr "" -#: src/Module/Admin/Logs/View.php:72 src/Module/BaseAdmin.php:90 +#: src/Module/Admin/Logs/View.php:72 src/Module/BaseAdmin.php:89 msgid "View Logs" msgstr "" @@ -4351,7 +4351,7 @@ msgstr "" msgid "Interactors" msgstr "" -#: src/Module/Admin/Site.php:447 src/Module/BaseAdmin.php:76 +#: src/Module/Admin/Site.php:447 src/Module/BaseAdmin.php:75 msgid "Site" msgstr "" @@ -5324,7 +5324,7 @@ msgstr "" msgid "Storage Configuration" msgstr "" -#: src/Module/Admin/Storage.php:127 src/Module/BaseAdmin.php:77 +#: src/Module/Admin/Storage.php:127 src/Module/BaseAdmin.php:76 msgid "Storage" msgstr "" @@ -5408,39 +5408,39 @@ msgstr "" msgid "Friendica's configuration now is stored in config/local.config.php, please copy config/local-sample.config.php and move your config from config/local.ini.php. See the Config help page for help with the transition." msgstr "" -#: src/Module/Admin/Summary.php:104 +#: src/Module/Admin/Summary.php:105 #, php-format msgid "%s is not reachable on your system. This is a severe configuration issue that prevents server to server communication. See the installation page for help." msgstr "" -#: src/Module/Admin/Summary.php:128 +#: src/Module/Admin/Summary.php:133 #, php-format msgid "Friendica's system.basepath was updated from '%s' to '%s'. Please remove the system.basepath from your db to avoid differences." msgstr "" -#: src/Module/Admin/Summary.php:136 +#: src/Module/Admin/Summary.php:143 #, php-format msgid "Friendica's current system.basepath '%s' is wrong and the config file '%s' isn't used." msgstr "" -#: src/Module/Admin/Summary.php:144 +#: src/Module/Admin/Summary.php:153 #, php-format msgid "Friendica's current system.basepath '%s' is not equal to the config file '%s'. Please fix your configuration." msgstr "" -#: src/Module/Admin/Summary.php:155 +#: src/Module/Admin/Summary.php:165 msgid "Message queues" msgstr "" -#: src/Module/Admin/Summary.php:158 +#: src/Module/Admin/Summary.php:168 msgid "Server Settings" msgstr "" -#: src/Module/Admin/Summary.php:176 +#: src/Module/Admin/Summary.php:186 msgid "Version" msgstr "" -#: src/Module/Admin/Summary.php:180 +#: src/Module/Admin/Summary.php:190 msgid "Active addons" msgstr "" @@ -5464,7 +5464,7 @@ msgid "Screenshot" msgstr "" #: src/Module/Admin/Themes/Details.php:83 src/Module/Admin/Themes/Index.php:104 -#: src/Module/BaseAdmin.php:79 +#: src/Module/BaseAdmin.php:78 msgid "Themes" msgstr "" @@ -5581,76 +5581,76 @@ msgstr "" msgid "Item was not found." msgstr "" -#: src/Module/BaseAdmin.php:40 src/Module/BaseAdmin.php:44 +#: src/Module/BaseAdmin.php:39 src/Module/BaseAdmin.php:43 #: src/Module/BaseModeration.php:66 src/Module/BaseModeration.php:70 msgid "Please login to continue." msgstr "" -#: src/Module/BaseAdmin.php:49 +#: src/Module/BaseAdmin.php:48 msgid "You don't have access to administration pages." msgstr "" -#: src/Module/BaseAdmin.php:53 +#: src/Module/BaseAdmin.php:52 msgid "Submanaged account can't access the administration pages. Please log back in as the main account." msgstr "" -#: src/Module/BaseAdmin.php:72 src/Module/BaseModeration.php:98 +#: src/Module/BaseAdmin.php:71 src/Module/BaseModeration.php:98 msgid "Overview" msgstr "" -#: src/Module/BaseAdmin.php:75 src/Module/BaseModeration.php:101 +#: src/Module/BaseAdmin.php:74 src/Module/BaseModeration.php:101 msgid "Configuration" msgstr "" -#: src/Module/BaseAdmin.php:80 src/Module/BaseSettings.php:98 +#: src/Module/BaseAdmin.php:79 src/Module/BaseSettings.php:98 msgid "Additional features" msgstr "" -#: src/Module/BaseAdmin.php:83 +#: src/Module/BaseAdmin.php:82 msgid "Database" msgstr "" -#: src/Module/BaseAdmin.php:84 +#: src/Module/BaseAdmin.php:83 msgid "DB updates" msgstr "" -#: src/Module/BaseAdmin.php:85 +#: src/Module/BaseAdmin.php:84 msgid "Inspect Deferred Workers" msgstr "" -#: src/Module/BaseAdmin.php:86 +#: src/Module/BaseAdmin.php:85 msgid "Inspect worker Queue" msgstr "" -#: src/Module/BaseAdmin.php:92 src/Module/BaseModeration.php:109 +#: src/Module/BaseAdmin.php:91 src/Module/BaseModeration.php:109 msgid "Diagnostics" msgstr "" -#: src/Module/BaseAdmin.php:93 +#: src/Module/BaseAdmin.php:92 msgid "PHP Info" msgstr "" -#: src/Module/BaseAdmin.php:94 +#: src/Module/BaseAdmin.php:93 msgid "probe address" msgstr "" -#: src/Module/BaseAdmin.php:95 +#: src/Module/BaseAdmin.php:94 msgid "check webfinger" msgstr "" -#: src/Module/BaseAdmin.php:96 +#: src/Module/BaseAdmin.php:95 msgid "Babel" msgstr "" -#: src/Module/BaseAdmin.php:97 src/Module/Debug/ActivityPubConversion.php:125 +#: src/Module/BaseAdmin.php:96 src/Module/Debug/ActivityPubConversion.php:125 msgid "ActivityPub Conversion" msgstr "" -#: src/Module/BaseAdmin.php:106 +#: src/Module/BaseAdmin.php:115 msgid "Addon Features" msgstr "" -#: src/Module/BaseAdmin.php:107 src/Module/BaseModeration.php:118 +#: src/Module/BaseAdmin.php:116 src/Module/BaseModeration.php:118 msgid "User registrations waiting for confirmation" msgstr "" @@ -7049,52 +7049,52 @@ msgstr "" msgid "Suggest a friend for %s" msgstr "" -#: src/Module/Friendica.php:69 +#: src/Module/Friendica.php:71 msgid "Installed addons/apps:" msgstr "" -#: src/Module/Friendica.php:74 +#: src/Module/Friendica.php:76 msgid "No installed addons/apps" msgstr "" -#: src/Module/Friendica.php:79 +#: src/Module/Friendica.php:81 #, php-format msgid "Read about the Terms of Service of this node." msgstr "" -#: src/Module/Friendica.php:86 +#: src/Module/Friendica.php:88 msgid "On this server the following remote servers are blocked." msgstr "" -#: src/Module/Friendica.php:89 +#: src/Module/Friendica.php:91 #: src/Module/Moderation/Blocklist/Server/Index.php:76 #: src/Module/Moderation/Blocklist/Server/Index.php:100 #: src/Module/Settings/Channels.php:218 msgid "Reason for the block" msgstr "" -#: src/Module/Friendica.php:91 +#: src/Module/Friendica.php:93 msgid "Download this list in CSV format" msgstr "" -#: src/Module/Friendica.php:105 +#: src/Module/Friendica.php:108 #, php-format msgid "This is Friendica, version %s that is running at the web location %s. The database version is %s, the post update version is %s." msgstr "" -#: src/Module/Friendica.php:110 +#: src/Module/Friendica.php:114 msgid "Please visit Friendi.ca to learn more about the Friendica project." msgstr "" -#: src/Module/Friendica.php:111 +#: src/Module/Friendica.php:115 msgid "Bug reports and issues: please visit" msgstr "" -#: src/Module/Friendica.php:111 +#: src/Module/Friendica.php:115 msgid "the bugtracker at github" msgstr "" -#: src/Module/Friendica.php:112 +#: src/Module/Friendica.php:116 msgid "Suggestions, praise, etc. - please email \"info\" at \"friendi - dot - ca" msgstr "" @@ -8571,19 +8571,19 @@ msgid "No contacts." msgstr "" #: src/Module/Profile/Conversations.php:96 src/Module/Profile/Profile.php:342 -#: src/Protocol/Feed.php:1096 +#: src/Protocol/Feed.php:1114 #, php-format msgid "%s's posts" msgstr "" #: src/Module/Profile/Conversations.php:97 src/Module/Profile/Profile.php:343 -#: src/Protocol/Feed.php:1099 +#: src/Protocol/Feed.php:1117 #, php-format msgid "%s's comments" msgstr "" #: src/Module/Profile/Conversations.php:98 src/Module/Profile/Profile.php:344 -#: src/Protocol/Feed.php:1092 +#: src/Protocol/Feed.php:1110 #, php-format msgid "%s's timeline" msgstr ""