diff --git a/.codecov.yml b/.codecov.yml index 7b6eecb66e..be8ab11829 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + codecov: branch: develop ci: diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 26cba10525..28dd56a88b 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + name: my-friendica type: php docroot: "" diff --git a/.devcontainer/.env b/.devcontainer/.env new file mode 100644 index 0000000000..396ac9ebfe --- /dev/null +++ b/.devcontainer/.env @@ -0,0 +1,21 @@ +#Database setup +MYSQL_HOST=127.0.0.1 +MYSQL_DATABASE=friendica +MYSQL_USER=friendica +MYSQL_PASSWORD=friendica + +#Redis +REDIS_HOST=127.0.0.1 + +#Webserver setup +ServerName=localhost +ServerPort=8080 +ServerAlias=friendica.local +DocumentRoot=/var/www/html +APACHE_LOG_DIR=/var/log/apache2 + +#Test users +ADMIN_NICK=admin +ADMIN_PASSW=admin +USER_NICK=user +USER_PASSW=user diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..4867640812 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,96 @@ +ARG VARIANT="8.2-apache-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/php:${VARIANT} + +ARG DEBIAN_FRONTEND=noninteractive +ARG apcu_version=5.1.23 +ARG memcached_version=3.2.0 +ARG redis_version=6.0.2 +ARG imagick_version=3.7.0 + +RUN apt-get update -y; + +# Install MariaDB client +RUN apt-get install -y mariadb-client + +# Base packages +RUN apt install -y vim software-properties-common sudo nano gnupg2 + +# entrypoint.sh and cron.sh dependencies +RUN apt-get install -y --no-install-recommends \ + rsync \ + bzip2 \ + msmtp \ + tini + +RUN apt-get install -y --no-install-recommends \ + bash \ + libpng-dev \ + libjpeg62-turbo-dev \ + libtool \ + libmagick++-dev \ + libmemcached-dev \ + zlib1g-dev \ + libssl-dev \ + libgraphicsmagick1-dev \ + libfreetype6-dev \ + libwebp-dev \ + librsvg2-2 \ + libzip-dev \ + libldap2-dev \ + libgmp-dev \ + libmagickcore-6.q16-6-extra \ + ; \ + \ + docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + ; \ + docker-php-ext-install -j "$(nproc)" \ + pdo_mysql \ + gd \ + exif \ + zip \ + opcache \ + ctype \ + pcntl \ + ldap \ + gmp \ + intl + +# pecl will claim success even if one install fails, so we need to perform each install separately +RUN pecl install apcu-${apcu_version}; \ + pecl install memcached-${memcached_version}; \ + pecl install redis-${redis_version}; \ + pecl install imagick-${imagick_version}; \ + docker-php-ext-enable \ + apcu \ + memcached \ + redis \ + imagick + +RUN apt-get clean -y && rm -rf /var/lib/apt/lists/* + +ENV PHP_MEMORY_LIMIT 512M +ENV PHP_UPLOAD_LIMIT 512M + +RUN { \ + echo 'opcache.enable=1' ; \ + echo 'opcache.interned_strings_buffer=8'; \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.revalidte_freq=1'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini; \ + \ + echo 'apc.enable_cli=1' >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini; \ + \ + { \ + echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ + echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ + echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ + } > /usr/local/etc/php/conf.d/friendica.ini; \ + ln -s /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini; \ + \ + mkdir /var/www/data; \ + chmod -R g=u /var/www diff --git a/.devcontainer/README.MD b/.devcontainer/README.MD new file mode 100644 index 0000000000..56cf8d4ef6 --- /dev/null +++ b/.devcontainer/README.MD @@ -0,0 +1,54 @@ + +This folder holds a devcontainer definition for Friendica. + +The main features are: + +- The development container is based on the PHP dev container image in a variant that includes an Apache2 + webserver. The variant defines the PHP version and the OS it is based on. The currently used variant + is defined in the Dockerfile. + +- Creating a dev container from the Git repository should give you a running development environment + with no or optionally only a very little things to do after creation. + +- A MariaDB container is used for the database. It can be accessed in the dev container's terminal with simple + calling mysql. The needed parameters for the client are created and copied during setup. The runtime configuration + needs to use 127.0.0.1 instead of localhost as the latter causes PHP to try to use a socket connection which is not + available in this setup. + + +The development setup is: + +- After creation of the dev container the Apache2 web server shall be availaible through port forwarding on + port 8080 from your local development machine (http://localhost:8080/). This is also the url as configured + in local.config.php. You should be able to log in with user 'admin@friendica.local' and password 'admin'. + +- Important values are defined in the .env file within the .devcontainer folder and applied during creation wherever possible. The + environment is also available during run/debug time to the application. + +- XDebug can be started by the launch configuration 'Listen for Xdebug'. The launch configuration is in .vscode/launch.json + (this file is added to git). + +- The Apache server in the dev container is reachable with http on the ports 80 and 8080 and with https on port 443. The + url used for Friendica is defined in local.config.php (currently localhost:8080) and any subsequent request will be redirected + to this url. To change the url to one you like you need to modify the url in local.config.php which can be done by + setting the values in the .env file accordingly and rebuilding the container. + +- The hostname friendica.local is used for the ceritificate and added to the hosts file in the container. .local is a reserved TLD + for mDNS and if you can use this depends on your network configuration. For developing and debugging using forwarded ports + (localhost:8080) works fine. + + +Open points: + +- Cron jobs / worker are not available. For a dev environment those are disabled by default (but can be optionally + enabled). + +- Passing values from the local development machine (with $localEnv) does not seem to work. This would be handy to apply + a few settings differently based on user choice. + +- The dev container does not have an email MTA. + +- There are still a bit too much warnings logged at startup but that doesn't seem to be a problem. + +- Only the first launch configuration ('Listen for Xdebug') is working. + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..50d0fb384d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,42 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/php-mariadb +{ + "name": "Friendica", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + "remoteEnv": { + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "developmentUser": "vscode" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "bash -c '.devcontainer/postCreate.sh && .devcontainer/postCreateApacheSetup.sh && .devcontainer/postCreateFriendicaSetup.sh'", + "postStartCommand": "service apache2 start", + + "forwardPorts": [ + 8080 + ], + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "bmewburn.vscode-intelephense-client", + "ms-azuretools.vscode-docker", + "xdebug.php-debug", + "donjayamanne.githistory" + ], + "settings": { + "php.suggest.basic": false + } + } + } + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + //"remoteUser": "root" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..4463ebad86 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + + volumes: + - ../..:/workspaces:cached + env_file: ".env" + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + ports: + - 80:80 + - 443:443 + - 8080:8080 + - 3306:3306 + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + extra_hosts: + - "${ServerAlias}:127.0.0.1" + + db: + image: mariadb:10.4 + restart: unless-stopped + volumes: + - mariadb-data:/var/lib/mysql + env_file: ".env" + environment: + MYSQL_ROOT_PASSWORD: root + command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'] + + # Runs app on the same network as the app container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:app + + redis: + image: redis:latest + restart: unless-stopped + env_file: ".env" + network_mode: service:app + +volumes: + mariadb-data: + +networks: + default: + \ No newline at end of file diff --git a/.devcontainer/include/001-friendica.conf b/.devcontainer/include/001-friendica.conf new file mode 100644 index 0000000000..61eb40c7cc --- /dev/null +++ b/.devcontainer/include/001-friendica.conf @@ -0,0 +1,71 @@ + + ServerAdmin webmaster@localhost + ServerName ${ServerName} + ServerAlias ${ServerAlias} + + DocumentRoot ${DocumentRoot} + + + SetHandler server-status + Order deny,allow + Allow from all + + + + Options Indexes FollowSymLinks MultiViews + AllowOverride All + Order allow,deny + allow from all + + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + ErrorLog ${APACHE_LOG_DIR}/${ServerName}-error.log + CustomLog ${APACHE_LOG_DIR}/${ServerName}-access.log combined + + + + + ServerAdmin webmaster@localhost + ServerName ${ServerName} + ServerAlias ${ServerAlias} + + DocumentRoot ${DocumentRoot} + + + SetHandler server-status + Order deny,allow + Allow from all + + + + Options Indexes FollowSymLinks MultiViews + AllowOverride All + Order allow,deny + allow from all + + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + ErrorLog ${APACHE_LOG_DIR}/${ServerName}-error.log + CustomLog ${APACHE_LOG_DIR}/${ServerName}-access.log combined + + SSLEngine on + + SSLCertificateFile /etc/ssl/certs/friendica.crt + SSLCertificateKeyFile /etc/ssl/private/friendica.key + + + SSLOptions +StdEnvVars + + + BrowserMatch "MSIE [2-6]" \\ + nokeepalive ssl-unclean-shutdown \\ + downgrade-1.0 force-response-1.0 + # MSIE 7 and newer should be able to use keepalive + BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown + \ No newline at end of file diff --git a/.devcontainer/include/00apcu.config.php b/.devcontainer/include/00apcu.config.php new file mode 100644 index 0000000000..2e5ebcf53e --- /dev/null +++ b/.devcontainer/include/00apcu.config.php @@ -0,0 +1,11 @@ + [ + 'cache_driver' => 'apcu', + ], +]; diff --git a/.devcontainer/include/01redis.config.php b/.devcontainer/include/01redis.config.php new file mode 100644 index 0000000000..2ea29bd749 --- /dev/null +++ b/.devcontainer/include/01redis.config.php @@ -0,0 +1,17 @@ + [ + 'session_handler' => 'cache', + 'distributed_cache_driver' => 'redis', + 'lock_driver' => 'redis', + 'redis_host' => getenv('REDIS_HOST'), + 'redis_port' => (getenv('REDIS_PORT') ?: ''), + 'redis_password' => (getenv('REDIS_PW') ?: ''), + 'redis_db' => (getenv('REDIS_DB') ?: 0), + ], + ]; +} else { + return []; +} diff --git a/.devcontainer/include/autoinstall.config.php b/.devcontainer/include/autoinstall.config.php new file mode 100644 index 0000000000..c9e2026354 --- /dev/null +++ b/.devcontainer/include/autoinstall.config.php @@ -0,0 +1,36 @@ + [ + 'hostname' => '${MYSQL_HOST}', + 'username' => '${MYSQL_USER}', + 'password' => '${MYSQL_PASSWORD}', + 'database' => '${MYSQL_DATABASE}', + 'charset' => 'utf8mb4', + ], + + // **************************************************************** + // The configuration below will be overruled by the admin panel. + // Changes made below will only have an effect if the database does + // not contain any configuration for the friendica system. + // **************************************************************** + + 'config' => [ + 'admin_email' => 'admin@${ServerAlias}', + 'sitename' => 'Friendica Social Network', + 'register_policy' => \Friendica\Module\Register::OPEN, + 'register_text' => '', + 'php' => '${FRIENDICA_PHP_PATH}', + ], + 'system' => [ + 'default_timezone' => 'UTC', + 'language' => 'en', + 'basepath' => '${workspaceFolder}', + 'url' => 'http://${ServerName}:${ServerPort}' + ], +]; diff --git a/.devcontainer/include/my.cnf b/.devcontainer/include/my.cnf new file mode 100644 index 0000000000..c6d320846e --- /dev/null +++ b/.devcontainer/include/my.cnf @@ -0,0 +1,4 @@ +[client] +protocol = tcp +user = ${MYSQL_USER} +password = ${MYSQL_PASSWORD} diff --git a/.devcontainer/include/zz-docker.config.php b/.devcontainer/include/zz-docker.config.php new file mode 100644 index 0000000000..a74a08bbb9 --- /dev/null +++ b/.devcontainer/include/zz-docker.config.php @@ -0,0 +1,34 @@ + [ + // Necessary because otherwise the daemon isn't working + 'pidfile' => '/tmp/friendica.pid', + + 'logfile' => '/var/www/html/friendica.log', + 'loglevel' => 'notice', + ], + 'storage' => [ + 'filesystem_path' => '/var/www/html/storage', + ], +]; + +if (!empty(getenv('FRIENDICA_NO_VALIDATION'))) { + $config['system']['disable_url_validation'] = true; + $config['system']['disable_email_validation'] = true; +} + +if (!empty(getenv('SMTP_DOMAIN'))) { + $smtp_from = !empty(getenv('SMTP_FROM')) ? getenv('SMTP_FROM') : 'no-reply'; + + $config['config']['sender_email'] = $smtp_from . "@" . getenv('SMTP_DOMAIN'); +} + +return $config; diff --git a/.devcontainer/launch.json b/.devcontainer/launch.json new file mode 100644 index 0000000000..7a99e991cc --- /dev/null +++ b/.devcontainer/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9000, + "runtimeArgs": [ + "-dzend_extension=xdebug.so", + "-dxdebug.mode=debug", + "-dxdebug.start_with_request=yes", + "-dxdebug.client_host=127.0.0.1", + "-dxdebug.client_port=9000", + "-dxdebug.log=/tmp/xdebug.log" + ] + } + ] +} diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100755 index 0000000000..0ee0c93764 --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# + +# Prepare the workspace files with the values from the devcontainer.env file +set -a +source $workspaceFolder/.devcontainer/.env + +echo ">>> Development Setup" +sudo apt-get update + +# VSCode debugger profile +mkdir -p .vscode && cp .devcontainer/launch.json .vscode/launch.json + +envsubst < $workspaceFolder/.devcontainer/include/my.cnf > /home/vscode/.my.cnf + +# Make the workspace directory the docroot +echo ">>> Symlink $DocumentRoot to $workspaceFolder" +sudo rm -rf $DocumentRoot +sudo ln -fs $workspaceFolder $DocumentRoot + +# Set proper permissions +sudo chown -R $developmentUser:www-data $workspaceFolder +sudo chmod -R g=u $workspaceFolder + +echo 'error_reporting=0' | sudo tee /usr/local/etc/php/conf.d/no-warn.ini + +exit 0 diff --git a/.devcontainer/postCreateApacheSetup.sh b/.devcontainer/postCreateApacheSetup.sh new file mode 100755 index 0000000000..d393a76a58 --- /dev/null +++ b/.devcontainer/postCreateApacheSetup.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# +source $workspaceFolder/.devcontainer/.env + +echo ">>> Apache2 Configuration" +envsubst < $workspaceFolder/.devcontainer/include/001-friendica.conf > /tmp/001-friendica.conf + +# Create a self-signed SSL certificate +sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/ssl/private/friendica.key \ + -out /etc/ssl/certs/friendica.crt \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=$ServerAlias" \ + -addext "subjectAltName = DNS:$ServerAlias, DNS:$ServerName" + +sudo chmod +rx /etc/ssl/private +sudo chmod 644 /etc/ssl/private/friendica.key +sudo chmod 644 /etc/ssl/certs/friendica.crt + +sudo cp /tmp/001-friendica.conf /etc/apache2/sites-available/001-friendica.conf +sudo a2enmod rewrite actions ssl remoteip +{ + echo RemoteIPHeader X-Real-IP ; + echo RemoteIPTrustedProxy 10.0.0.0/8 ; + echo RemoteIPTrustedProxy 172.16.0.0/12 ; + echo RemoteIPTrustedProxy 192.168.0.0/16 ; +} | sudo tee /etc/apache2/conf-available/remoteip.conf > /dev/null +sudo a2enconf remoteip + +sudo a2ensite 001-friendica +sudo a2dissite 000-default + +echo 'ServerName 127.0.0.1' | sudo tee -a /etc/apache2/apache2.conf + +exit 0 diff --git a/.devcontainer/postCreateFriendicaSetup.sh b/.devcontainer/postCreateFriendicaSetup.sh new file mode 100755 index 0000000000..1880ac49cc --- /dev/null +++ b/.devcontainer/postCreateFriendicaSetup.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +source $workspaceFolder/.devcontainer/.env + +# Setup Friendica +echo ">>> Friendica Setup" + +FRIENDICA_PHP_PATH=$(which php) +export FRIENDICA_PHP_PATH + +envsubst < $workspaceFolder/.devcontainer/include/autoinstall.config.php > /tmp/autoinstall.config.php +cp $workspaceFolder/.devcontainer/include/00apcu.config.php $workspaceFolder/config/00apcu.config.php +cp $workspaceFolder/.devcontainer/include/01redis.config.php $workspaceFolder/config/01redis.config.php +cp $workspaceFolder/.devcontainer/include/zz-docker.config.php $workspaceFolder/config/zz-docker.config.php + + +cd $DocumentRoot + +# copy the .htaccess-dist file to .htaccess so that rewrite rules work +cp $DocumentRoot/.htaccess-dist $DocumentRoot/.htaccess + +bin/composer.phar install + +# install friendica +bin/console autoinstall -f /tmp/autoinstall.config.php + +# add users +# (disable a bunch of validation because this is a dev install, deh, it needs invalid emails and stupid passwords) +bin/console config system disable_email_validation 1 +bin/console config system disable_password_exposed 1 +bin/console user add "$ADMIN_NICK" "$ADMIN_NICK" "$ADMIN_NICK@$ServerAlias" en http://friendica.local/profile/$ADMIN_NICK +bin/console user password "$ADMIN_NICK" "$ADMIN_PASSW" +bin/console user add "$USER_NICK" "$USER_NICK" "$USER_NICK@$ServerAlias" en http://friendica.local/profile/$USER_NICK +bin/console user password "$USER_NICK" "$USER_PASSW" + +# create log file +#mkdir -p $workspaceFolder/log +#touch $workspaceFolder/log/friendica.log +#chmod 666 $workspaceFolder/log/friendica.log +touch $workspaceFolder/friendica.log +chmod 666 $workspaceFolder/friendica.log + +exit 0 diff --git a/.editorconfig b/.editorconfig index 610409143b..57445d869c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + # editorconfig tool configuration # see http://editorconfig.org for docs diff --git a/.gitattributes b/.gitattributes index 18ba9e0758..915c608e7a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + # Disable LF normalization for all files * -text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7bdd4f066c..70ae203464 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + blank_issues_enabled: true contact_links: - name: Friendica Community Support diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index e65d397099..81b6876f3e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -19,4 +19,4 @@ assignees: '' -### Additional context \ No newline at end of file +### Additional context diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 86df437339..a0a7837170 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -7,4 +7,4 @@ assignees: '' --- -# For general question about Friendica, please try to find a solution at https://wiki.friendi.ca first. \ No newline at end of file +# For general question about Friendica, please try to find a solution at https://wiki.friendi.ca first. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..9719a47b02 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000000..bcca97c908 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +name: Code Quality + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + + code-style: + name: PHP-CS-Fixer (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + operating-system: ['ubuntu-latest'] + php: ['8.3'] + + 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: xdebug + tools: none + + - name: Clone addon repository + run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon + + - name: Install PHP-CS-Fixer + run: composer install --working-dir=bin/dev/php-cs-fixer + + - name: Run PHP-CS-Fixer + continue-on-error: true + run: bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff --dry-run + + phpstan: + name: PHPStan (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: xdebug + 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 PHPStan + run: composer run phpstan + + phpstan-addons: + name: PHPStan in addons (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: xdebug + 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 PHPStan in addons + run: composer run phpstan-addons + + 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/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..d0aa623842 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +name: CI tests + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + unit-tests: + name: Unit-Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + operating-system: ['ubuntu-latest'] + php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + + 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: xdebug + 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 Unit tests + run: composer run test:unit + + database-tests: + name: Database-Tests (PHP ${{ matrix.php-versions }}) + runs-on: ubuntu-latest + + services: + mariadb: + image: mariadb:latest + env: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: test + MYSQL_PASSWORD: test + MYSQL_USER: test + ports: + - 3306/tcp + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 + + redis: + image: redis + ports: + - 6379/tcp + options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + memcached: + image: memcached + ports: + - 11211/tcp + + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: pecl, composer + extensions: pdo_mysql, gd, zip, opcache, ctype, pcntl, ldap, apcu, memcached, redis, imagick, memcache + coverage: xdebug + ini-values: apc.enabled=1, apc.enable_cli=1 + + - name: Clone addon repository + run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon + + # Install composer dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer + - name: "Install Composer dependencies" + uses: "ramsey/composer-install@v2" + + - name: Start mysql service + run: sudo /etc/init.d/mysql start + + - name: Copy default Friendica config + run: cp config/local-sample.config.php config/local.config.php + + - name: Verify MariaDB connection + env: + PORT: ${{ job.services.mariadb.ports[3306] }} + run: | + while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do + sleep 1 + done + + - name: Setup MYSQL database + env: + PORT: ${{ job.services.mariadb.ports[3306] }} + run: | + mysql -h"127.0.0.1" -P"$PORT" -utest -ptest test < database.sql + + - name: Test with phpunit + run: vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-clover clover.xml + env: + MYSQL_HOST: 127.0.0.1 + MYSQL_PORT: ${{ job.services.mariadb.ports[3306] }} + MYSQL_DATABASE: test + MYSQL_PASSWORD: test + MYSQL_USER: test + REDIS_PORT: ${{ job.services.redis.ports[6379] }} + REDIS_HOST: 127.0.0.1 + MEMCACHED_PORT: ${{ job.services.memcached.ports[11211] }} + MEMCACHE_PORT: ${{ job.services.memcached.ports[11211] }} diff --git a/.gitignore b/.gitignore index 2889d12b5f..b60e96e8df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + favicon.* /.htconfig.php /.htpreconfig.php @@ -68,7 +72,6 @@ venv/ /.idea #ignore addons directory -/addons /addon #ignore base .htaccess @@ -83,8 +86,10 @@ venv/ #Ignore temporary installed phpunit /bin/phpunit -#Ignore cache file +#Ignore cache files .php_cs.cache +.php-cs-fixer.cache +.phpmd.result-cache.php #ignore avatar picture cache path /avatar diff --git a/.htaccess-dist b/.htaccess-dist index c5c1b5b716..bb53517fc3 100644 --- a/.htaccess-dist +++ b/.htaccess-dist @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + # This file is meant to be copied to ".htaccess" on Apache-powered web servers. # The created .htaccess file can be edited manually and will not be overwritten by Friendica updates. diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 81% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 897c6f1104..d376a525f2 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,4 +1,9 @@ in(__DIR__) + ->notPath('addon') ->notPath('bin/dev') ->notPath('config') ->notPath('doc') @@ -37,10 +43,10 @@ return $config ], ], 'blank_line_after_namespace' => true, - 'braces' => [ - 'position_after_anonymous_constructs' => 'same', - 'position_after_control_structures' => 'same', - 'position_after_functions_and_oop_constructs' => 'next', + 'braces_position' => [ + 'anonymous_classes_opening_brace' => 'same_line', + 'control_structures_opening_brace' => 'same_line', + 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', ], 'elseif' => true, 'encoding' => true, @@ -54,10 +60,9 @@ return $config 'syntax' => 'long', ], 'lowercase_keywords' => true, - 'method_argument_space' => [], 'no_closing_tag' => true, 'no_spaces_after_function_name' => true, - 'no_spaces_inside_parenthesis' => true, + 'spaces_inside_parentheses' => false, 'no_trailing_whitespace' => true, 'no_trailing_whitespace_in_comment' => true, 'no_unused_imports' => true, @@ -70,7 +75,7 @@ return $config 'visibility_required' => [ 'elements' => ['property', 'method'] ], - 'new_with_braces' => true, + 'new_with_parentheses' => true, ]) ->setFinder($finder) ->setIndent("\t"); 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/.phpstan-addons.neon b/.phpstan-addons.neon new file mode 100644 index 0000000000..c4864d468d --- /dev/null +++ b/.phpstan-addons.neon @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +parameters: + level: 3 + + paths: + - addon/ + + excludePaths: + analyse: + - addon/*/lang/* + - addon/*/vendor/* + - addon/convert/UnitConvertor.php + - addon/pumpio/oauth/* + + scanDirectories: + - mod + - src + - static + - vendor + - view + + dynamicConstantNames: + - DB_UPDATE_VERSION + + ignoreErrors: + + - + # Ignore missing SMTP class in PHPMailer 5.2.21 + # see https://github.com/PHPMailer/PHPMailer/blob/v5.2.21/class.smtp.php + message: '(^.+ an unknown class SMTP\.$)' + path: addon/mailstream/phpmailer + + - + # Ignore missing SMTP class in PHPMailer 5.2.21 + # see https://github.com/PHPMailer/PHPMailer/blob/v5.2.21/class.smtp.php + message: '(^Property .+ has unknown class SMTP as its type\.$)' + path: addon/mailstream/phpmailer + + - + # Ignore missing SMTP class in PHPMailer 5.2.21 + # see https://github.com/PHPMailer/PHPMailer/blob/v5.2.21/class.smtp.php + message: '(^Method .+ has invalid return type SMTP\.$)' + path: addon/mailstream/phpmailer + + - + # Ignore missing SMTP class in PHPMailer 5.2.21 + # see https://github.com/PHPMailer/PHPMailer/blob/v5.2.21/class.smtp.php + message: '(^Instantiated class SMTP not found\.$)' + path: addon/mailstream/phpmailer diff --git a/.phpstan.neon b/.phpstan.neon new file mode 100644 index 0000000000..fb731728b8 --- /dev/null +++ b/.phpstan.neon @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +parameters: + level: 3 + + paths: + - bin/auth_ejabberd.php + - bin/console.php + - bin/daemon.php + - bin/jetstream.php + - bin/worker.php + - index.php + - src/ + + scanDirectories: + - mod + - static + - vendor + - view + + dynamicConstantNames: + - DB_UPDATE_VERSION + + ignoreErrors: + - + # Ignore missing GdImage class in PHP <= 7.4 + message: '(^Property .+ has unknown class GdImage as its type\.$)' + path: src + + - + # Ignore missing IMAP\Connection class in PHP <= 8.0 + message: '(^Method .+ has invalid return type IMAP\\Connection\.$)' + path: src + + - + # Ignore missing IMAP\Connection class in PHP <= 8.0 + message: '(^Parameter .+ has invalid type IMAP\\Connection\.$)' + path: src diff --git a/.tx/config b/.tx/config index 32bcaf482d..ced61f382d 100644 --- a/.tx/config +++ b/.tx/config @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + [main] host = https://api.transifex.com diff --git a/.woodpecker/.code_standards_check.yml b/.woodpecker/.code_standards_check.yml index 0c951d7018..d147e8fc69 100644 --- a/.woodpecker/.code_standards_check.yml +++ b/.woodpecker/.code_standards_check.yml @@ -1,3 +1,13 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +# The code standard 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 @@ -27,12 +37,16 @@ steps: volumes: - /tmp/drone-cache:/tmp/cache check: - image: friendicaci/php-cs + image: php:8.3 commands: + - echo "**** Use bin/dev/fix-codestyle.sh in case of errors ****" + - apt-get update -q + - DEBIAN_FRONTEND=noninteractive apt-get install -q -y git - if [ ! -z "$${CI_COMMIT_PULL_REQUEST}" ]; then - git fetch --no-tags origin ${CI_COMMIT_TARGET_BRANCH}; - export CHANGED_FILES="$(git diff --name-status $(git merge-base FETCH_HEAD origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA} | grep ^A | cut -f2)"; + git fetch --no-tags --unshallow origin ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/origin/${CI_COMMIT_TARGET_BRANCH}; + CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base ${CI_COMMIT_SHA} origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})"; else - export CHANGED_FILES="$(git diff --name-status ${CI_COMMIT_SHA} | grep ^A | cut -f2)"; + CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB ${CI_COMMIT_SHA})"; fi - - /check-php-cs.sh + - EXTRA_ARGS="--path-mode=intersection -- $${CHANGED_FILES}"; + - ./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --diff --using-cache=no $${EXTRA_ARGS} diff --git a/.woodpecker/.continuous-deployment.yml b/.woodpecker/.continuous-deployment.yml index 4ff956b3b6..ff1dd5cf80 100644 --- a/.woodpecker/.continuous-deployment.yml +++ b/.woodpecker/.continuous-deployment.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + depends_on: - phpunit - code_standards_check @@ -9,6 +13,12 @@ labels: location: friendica type: releaser +# CD is triggered after pushing new code to the develop or *-rc branch, excluding the stable branch +when: + repo: friendica/friendica + branch: [ develop, '*-rc' ] + event: push + skip_clone: true steps: @@ -19,10 +29,6 @@ steps: - git checkout $CI_COMMIT_BRANCH - git fetch origin $CI_COMMIT_REF - git merge $CI_COMMIT_SHA - when: - repo: friendica/friendica - branch: [ develop, '*-rc' ] - event: push restore_cache: image: meltwater/drone-cache:dev settings: @@ -34,22 +40,15 @@ steps: - '.composer' volumes: - /tmp/drone-cache:/tmp/cache - when: - repo: friendica/friendica - branch: [ develop, '*-rc' ] - event: push composer_install: - image: friendicaci/php7.4:php7.4.33 + image: friendicaci/php8.2:php8.2.28 commands: + - mkdir addon # create empty addon folder to appease composer - export COMPOSER_HOME=.composer - composer validate - composer install --no-dev --optimize-autoloader volumes: - /etc/hosts:/etc/hosts - when: - repo: friendica/friendica - branch: [ develop, '*-rc' ] - event: push create_artifacts: image: debian commands: @@ -70,10 +69,6 @@ steps: - ls -lh - cat "$ARTIFACT.sum256" - sha256sum "$ARTIFACT" - when: - repo: friendica/friendica - branch: [ develop, '*-rc' ] - event: push sign_artifacts: image: plugins/gpgsign settings: @@ -86,17 +81,9 @@ steps: exclude: - build/*.sum256 detach_sign: true - when: - repo: friendica/friendica - branch: [ develop, '*-rc' ] - event: push publish_artifacts: image: alpine commands: - cp -fr build/* /tmp/friendica_files/ volumes: - files:/tmp/friendica_files - when: - repo: friendica/friendica - branch: [ develop, '*-rc' ] - event: push diff --git a/.woodpecker/.database_checks.yml b/.woodpecker/.database_checks.yml index 7d25536749..bf947dedaa 100644 --- a/.woodpecker/.database_checks.yml +++ b/.woodpecker/.database_checks.yml @@ -1,10 +1,16 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + matrix: include: - - PHP_MAJOR_VERSION: 7.4 - PHP_VERSION: 7.4.33 + - PHP_MAJOR_VERSION: 8.2 + PHP_VERSION: 8.2.28 -branches: - exclude: [ stable ] +when: + branch: + exclude: [ stable ] + event: [ pull_request, push ] # This forces CI executions at the "opensocial" labeled location (because of much more power...) labels: @@ -35,6 +41,7 @@ steps: composer_install: image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} commands: + - mkdir addon # create empty addon folder to appease composer - export COMPOSER_HOME=.composer - ./bin/composer.phar validate - ./bin/composer.phar install --prefer-dist diff --git a/.woodpecker/.license_check.yml b/.woodpecker/.license_check.yml index e7545f5f43..86174e51fd 100644 --- a/.woodpecker/.license_check.yml +++ b/.woodpecker/.license_check.yml @@ -1,11 +1,14 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +when: + branch: + exclude: [ stable ] + event: [ pull_request, push ] + steps: check: - image: friendicaci/php-cs + image: fsfe/reuse:latest commands: - - if [ ! -z "$${CI_COMMIT_PULL_REQUEST}" ]; then - git fetch --no-tags origin ${CI_COMMIT_TARGET_BRANCH}; - export CHANGED_FILES="$(git diff --name-status $(git merge-base FETCH_HEAD origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA} | grep -i '\.php$' | cut -f2)"; - else - export CHANGED_FILES="$(git diff --name-status ${CI_COMMIT_SHA} | grep -i '\.php$' | cut -f2)"; - fi - - /check-license.sh + - reuse lint diff --git a/.woodpecker/.messages.po_check.yml b/.woodpecker/.messages.po_check.yml index 9c25095940..2f9ae94044 100644 --- a/.woodpecker/.messages.po_check.yml +++ b/.woodpecker/.messages.po_check.yml @@ -1,3 +1,12 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +when: + branch: + exclude: [ stable ] + event: [ pull_request, push ] + steps: build_xgettext: image: friendicaci/transifex @@ -7,6 +16,3 @@ steps: image: friendicaci/transifex commands: - /check-messages.sh - -branches: - exclude: [ stable ] diff --git a/.woodpecker/.phpmd_check.yml b/.woodpecker/.phpmd_check.yml new file mode 100644 index 0000000000..7f9352ded3 --- /dev/null +++ b/.woodpecker/.phpmd_check.yml @@ -0,0 +1,50 @@ +# 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 ] + +# This forces PHP Unit executions at the "opensocial" labeled location (because of access issues with git.friendi.ca) +labels: + location: opensocial + +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.17 + 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.17 + commands: + - ./bin/composer.phar run phpmd diff --git a/.woodpecker/.phpunit.yml b/.woodpecker/.phpunit.yml index 97ea09d374..997d50a52e 100644 --- a/.woodpecker/.phpunit.yml +++ b/.woodpecker/.phpunit.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + matrix: include: - PHP_MAJOR_VERSION: 7.4 @@ -5,18 +9,38 @@ matrix: - PHP_MAJOR_VERSION: 8.0 PHP_VERSION: 8.0.30 - PHP_MAJOR_VERSION: 8.1 - PHP_VERSION: 8.1.23 + PHP_VERSION: 8.1.31 - PHP_MAJOR_VERSION: 8.2 - PHP_VERSION: 8.2.11 + PHP_VERSION: 8.2.28 + - PHP_MAJOR_VERSION: 8.3 + PHP_VERSION: 8.3.17 + - PHP_MAJOR_VERSION: 8.4 + PHP_VERSION: 8.4.5 # This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...) labels: location: opensocial +when: + branch: + exclude: [ stable ] + event: [ pull_request, push ] + +skip_clone: true + steps: + clone: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git config --global --add safe.directory $CI_WORKSPACE + - git clone $CI_REPO_CLONE_URL . + - git checkout $CI_COMMIT_BRANCH + - git fetch origin $CI_COMMIT_REF + - git merge $CI_COMMIT_SHA php-lint: image: php:${PHP_MAJOR_VERSION} - group: lint commands: - find . -name \*.php -not -path './vendor/*' -not -path './view/asset/*' -print0 | xargs -0 -n1 php -l restore_cache: @@ -33,6 +57,7 @@ steps: composer_install: image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} commands: + - git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon - export COMPOSER_HOME=.composer - ./bin/composer.phar validate - ./bin/composer.phar install --prefer-dist @@ -49,6 +74,20 @@ steps: - '.composer' volumes: - /tmp/drone-cache:/tmp/cache + phpstan: + image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} + when: + matrix: + PHP_MAJOR_VERSION: 8.3 + commands: + - bin/composer.phar run phpstan; + phpstan-addons: + image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} + when: + matrix: + PHP_MAJOR_VERSION: 8.3 + commands: + - bin/composer.phar run phpstan-addons; test: image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} environment: @@ -64,7 +103,7 @@ steps: - cp config/local-sample.config.php config/local.config.php - if ! bin/wait-for-connection $MYSQL_HOST $MYSQL_PORT 300; then echo "[ERROR] Waited 300 seconds, no response" >&2; exit 1; fi - mysql -h$MYSQL_HOST -P$MYSQL_PORT -p$MYSQL_PASSWORD -u$MYSQL_USER $MYSQL_DATABASE < database.sql - - if [ "${PHP_MAJOR_VERSION}" = "7.4" -a "${CI_REPO}" = "friendica/friendica" ]; then + - if [ "${PHP_MAJOR_VERSION}" = "8.2" -a "${CI_REPO}" = "friendica/friendica" ]; then phpenmod xdebug; export XDEBUG_MODE=coverage; phpunit --configuration tests/phpunit.xml -d memory_limit=-1 --coverage-clover clover.xml; @@ -75,15 +114,15 @@ steps: image: friendicaci/codecov when: matrix: - PHP_MAJOR_VERSION: 7.4 - PHP_VERSION: 7.4.33 + PHP_MAJOR_VERSION: 8.2 + PHP_VERSION: 8.2.28 repo: - friendica/friendica commands: - codecov -R '.' -Z -f 'clover.xml' - secrets: - - source: codecov-token - target: codecov_token + environment: + CODECOV_TOKEN: + from_secret: codecov-token services: mariadb: diff --git a/.woodpecker/.releaser.yml b/.woodpecker/.releaser.yml index 006bcfec37..d7dfd1cfb9 100644 --- a/.woodpecker/.releaser.yml +++ b/.woodpecker/.releaser.yml @@ -1,6 +1,6 @@ -depends_on: - - phpunit - - code_standards_check +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 # This prevents executing this pipeline at other servers than ci.friendi.ca labels: @@ -9,6 +9,11 @@ labels: skip_clone: true +when: + repo: friendica/friendica + branch: stable + event: tag + steps: clone: image: alpine/git @@ -17,10 +22,7 @@ steps: - git checkout $CI_COMMIT_BRANCH - git fetch origin $CI_COMMIT_REF - git merge $CI_COMMIT_SHA - when: - repo: friendica/friendica - branch: stable - event: tag + restore_cache: image: meltwater/drone-cache:dev settings: @@ -32,20 +34,13 @@ steps: - '.composer' volumes: - /tmp/drone-cache:/tmp/cache - when: - repo: friendica/friendica - branch: stable - event: tag composer_install: - image: friendicaci/php7.4:php7.4.33 + image: friendicaci/php8.2:php8.2.28 commands: + - mkdir addon # create empty addon folder to appease composer - export COMPOSER_HOME=.composer - composer validate - composer install --no-dev --optimize-autoloader - when: - repo: friendica/friendica - branch: stable - event: tag volumes: - /etc/hosts:/etc/hosts create_artifacts: @@ -68,10 +63,6 @@ steps: - ls -lh - cat "$ARTIFACT.sum256" - sha256sum "$ARTIFACT" - when: - repo: friendica/friendica - branch: stable - event: tag sign_artifacts: image: plugins/gpgsign settings: @@ -94,7 +85,3 @@ steps: - cp -fr build/* /tmp/friendica_files/ volumes: - files:/tmp/friendica_files - when: - repo: friendica/friendica - branch: stable - event: tag diff --git a/CHANGELOG b/CHANGELOG index d5d76f6e7d..1389bdb155 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,224 @@ -Version 2023.12 (unreleased) +Version 2025.02 (unreleased) + Friendica Core + Deprecated bin/daemon in favor of bin/console daemon (PR 14642) [nupplaphil] + Deprecated bin/jetstream in favor of bin/console jetstream (PR 14655) [nupplaphil] + + Friendica Addons + + Closed Issues + +Version 2024.12 (2024-12-31) + Friendica Core + Updates to the translations AR, BG, CA, CS, DE, EO, ES, ET, FR, GD, HU, IS, IT, JA, NL, PL, RU, SV + Updates to the documentation [annando, bmillwood, tobiasd] + Updates to the themes (frio) [haheute] + Friendica Core is now REUSE compliant [tobiasd] + General code cleanup [annando, nupplaphil, mexon] + Improved federation with Bluesky, Hubzilla, Peertube, threads, Wordpress [annando] + Improved the API [annando] + Improved display of contact connection state [annando] + Improved handling of bad webfinger requests [annando, mexon, zotanmew] + Improved the order of actions on the 2FA settings page [tobiasd] + Improved server type detection [annando] + Improved content negotiation [annando] + Improved expiration [annando] + Improved contact archiving [annando] + Improved delivery of content [annando] + Improved displayed project icons [annando] + Improved splitting of long postings via connectors [annando] + Improved contact import [annando] + Improved URL detection in searches [annando] + Improved handling of blocked users [annando] + Fixed a bug in creating app specific passwords [nupplaphil] + Fixed a bug in importing some notes from Mastodon [annando] + Fixed a bug with postings from buffer including images [annando] + Fixed a apache2 problem with unsafe URLs [annando] + Fixed a bug in the contact settings [annando] + Fixed a bug with latin1 encoded databases [annando] + Fixed a bug while uploading server blocklists [ne20002] + Fixed a bug while parsing events [annando] + Fixed a bug in the initial registry settings [annando] + Fixed a bug in 0Auth with buffer [annando] + Fixed a problem with rich HTML content [annando] + Fixed a bug with private comments [annando] + Fixed a bug in gettext [tobiasd] + Fixed a bug in the installation process [tobiasd] + Fixed schema.org issue [annando] + Added admin info to stats module [nupplaphil] + Added an option to exclude postings with images without ALT text [annando] + Added an option to hide custom emojis [annando] + Added support for HLS [annando] + Added devcontainer for Friendica [ne20002] + Added jetstream support for AT protocol [annando] + Added native probe support for AT protocol [annando] + Removed custom emojis from contact names [annando] + Removed OStatus support [annando] + + Friendica Addons + bluesky + Added block functionality [annando] + Added option to complete threads [annando] + Fixed issue with blocking contacts [annando] + Improved handling of startersets [annando] + Improved fetching of postings [annando] + invidious [loma-one] + unicode_smileys [loma-one] + fancybox + Deprecated the addon [tobiasd] + + Closed Issues + 13270, 13943, 14121, 14126, 14145, 14174, 14212, 14244, 14281, + 14292, 14294, 14303, 14307, 14344, 14368, 14370, 14373, 14377, + 14381, 14413, 14421, 14525, 14450, 14451, 14464, 14487, 14488, + 14491, 14495, 14512, 14587, 14609, 14630 + +Version 2024.08 (2024-08-17) + Friendica Core + Updates to the translations AR, CS, DE, ES, FR, GD, HU, IS, IT, JA, NL, PL, RU, SV + Updates to the documentation [foss-, loma-one, mexon] + Updates to the themes (frio) [haheute] + General code cleanup [annando, haheute, mexon, MrPetovan] + Improved the redirection for contact actions [annando] + Improved the performance while fetching of replies [annando] + Improved the performance when visiting remote profiles [annando] + Improved OWA [annando] + Improved the procession of worker tasks [annando] + Improved performance in the probing process [annando] + Improved INBOX performance [annando] + Improved perfomance when expireing postings [annando] + Improved mirroring settings for RSS contacts [annando] + Improved supported image formats [annando] + Improved handling of CC for comments [annando] + Improved handling of "sensitive" flags for postings [annando] + Improved display of log levels [annando, tobiasd] + Improved handling of permissions for attachments [annando] + Improved addon handling [MrPetovan] + Improved API for channels and circles [annando] + Improved performance while displaying local postings [annando] + Improved federation with pixelfed, threads [annando] + Improved integration with Bluesky [annando] + Improved automatic cleanup of the database [annando] + Fixed access to restricted timeline via API [annando] + Fixed problem fetching from INBOXes [annando] + Fixed display of contacts from unavailable networks [annando] + Fixed profile display [annando] + Fixed a problem with local un-/follows [annando] + Fixed the uimport POST endpoint [annando] + Fixed problem with 0Auth logins [annando] + Fixed problem with @mentions in comments [annando] + Fixed XSS in profile fields [annando, apexrabbit, Devilx86, MrPetovan, ponlayookm] + Fixed bug in deleting unused cached avatar pictures [annando] + Fixed paging bug on the media tab of remote profiles [annando] + Fixed display of attached links [annando] + Fixed a bug in circle only contacts [annando] + Fixed display of moderation reports [MrPetovan, TheTomcat14] + Fixed delivery problems to group postings [annando] + Added monitoring service endpoint [annando] + Added admin option display_link_length to set the length of displayed links [annando] + Added the possibility to upload media files via API [annando] + Added console command to clear avatar cache [annando] + Added platform data to the API [annando] + Added parsing support for Nodeinfo 2.1 and 2.2 [annando] + Added node description to Nodeinfo [annando] + Added owner information of relay accounts [annando] + Added option for users about how to transmit postings with titles [annando] + Added for non HTML content of feeds [annando] + Added reshares for postings from Bluesky and tumbl [annando] + Added public forums with manual request approval [annando] + Added "next try" information for deferred worker jobs listing [annando] + Added support of FEP-e232 [annando] + Added automatic closure of registration if admin becomes inactive [annando] + Added channel only option for contacts [annando] + + Friendica Addons + Updates to the translations AR, CS, DE, FR, IT, PL, SV + Blockbot + Added Relatica to good client list [hankg] + Improved agent identifier list [annando] + Bluesky + Added monitoring statistics [annando] + Added support of sensitive postings [annando] + Improved API handling [annando] + Improved fetching of user DID [annando] + Fixed conversion BS/Friendica handles [annando] + jsuploader + Improved detection of supported file types [annando] + mailstream + Improved image handling [mexon] + tumblr + Added monitoring statistics [annando] + Improved quoted postings [annando] + + Closed Issues + 11963, 13714, 13787, 13812, 13821, 13910, 14012, 14030, 14059, + 14077, 14079, 14045, 14052, 14055, 14081, 14084, 14102, 14110, + 14118, 14121, 14125, 14132, 14134, 14153, 14160, 14170, 14175, + 14186, 14197, 14220, 14228, 14231, 14240, 14249, 14250, 14285, + 14295, 14303, 14312, 14324, 14329, 14349, 14364 + +Version 2024.03 (2024-03-21) + Friendica Core + Updates to the translations AR, BG, CS, DE, EO, ES, FR, GD, HU, IS, IT, JA, PL, RO, RU, SV + Updates to the themes (frio, vier) [annando, foss-, haheute, MrPetovan, Raroun, toddy15] + Improved the channel feature [annando] + Improved the search performance [annando] + Improved spam detection [annando] + Improved the account overview on the moderation page [annando] + Improved account creation via CLI console [mexon] + Improved the Mastodon compatible API [annando, MrPetovan] + Improved logging of the system load value [annando] + Improved image handling [annando] + Improved detection of user activity [annando] + Improved display of embedded videos [annando] + Fixed an issue that could lead to empty URLs in the server block list [annando] + Fixed XSS attacks [leoOliver, MrPetovan, snajafov] + Fixed an issue when importing emails [annando] + Fixed an issue that blocked users could still use the API [annando] + Fixed an issue when fetching remote content [annando, arcanicanis] + Fixed an issue with unescaped HTML characters for RSS feeds [MrPetovan, r1pu5u] + Fixed an issue when showing the post preview [annando] + General code cleanup [annando, MrPetovan] + Updated the PasswordExposed usage [mexon] + Removed fpostit (Friendica post bookmarklet) [MrPetovan] + Removed the possibility for users to follow relays directly [annando] + Removed unused OEmbed functionality [annando] + Removed legacy schemes from frio theme [MrPetovan] + Added blur effect to sensitive images and user setting against it [annando] + Added account type Channel Relay [annando] + Added OCR generated image descriptions via tesseract addon [annando] + Added WebP and BMP support [annando] + Added blocked email addresses for registration [annando] + + Friendica Addons + advancedcontentfilter + Updated dependency for PHP8.2 compatibility [MrPetovan] + blockbot + Fixed an issue preventing the creation of previews on remote systems [annando] + Updated block lists [annando] + bluesky + Overhaul of the Bluesky connector [annando] + Fixed problem with empty quoted shares [annando] + openstreetmap + Fix a config problem [haheute] + pnut: + Connector addon was added [spacenerdmo] + tesseract + Added the addon to generate image descriptions from images via OCR [annando] + tumblr + Improved handling of quoted shares [annando] + url_replace + Added addon to replace URLs from Twitter, Youtube and some others using 12ft.io [toddy15] + Fixed an issue with empty config vars [MrPetovan] + + Closed Issues + 903, 7732, 8768, 11142, 13220, 13293, 13765, 13768, 13809, + 13814, 13814, 13818, 13819, 13822, 13823, 13828, 13837, 13839, + 13844, 13845, 13859, 13863, 13873, 13877, 13886, 13887, 13897, + 13899, 13905, 13909, 13922, 13925, 13930, 13939, 13940, 13946, + 13947, 13949, 13950, 13953, 13955, 13959, 13968, 13969, 13972, + 13984, 13985, 13986 + +Version 2023.12 (2023-12-24) Friendica Core Raised minimal PHP version to 7.4 Updates to the translations AR, BG, CA, CS, DE, EN GB, EN US, EO, ES, ET, FI, FR, GD, HU, IS, IT, JA, NL, PL, RO, RU, SV @@ -223,7 +443,7 @@ Version 2023.04 (2023-04-23) twitter Improve remote-self handling [annando] impressum - Avoide obfuscation on un-set email addresses [MrPestovan] + Avoide obfuscation on un-set email addresses [MrPetovan] notifyall Fixed a bug selecting the email addresses [nupplaphil] tumblr @@ -498,7 +718,7 @@ Version 2022.02 (2022-02-06) Added a media tab on profile pages [annando] Removed video tab on profile pages [annando] Bumped the minimal version of PHP to 7.3 - + Friendica Addons Updates to the translations AR, DE, FR, HU, IT, PL, SV [translation teams] Deprecated addons: blogger, buffer, jappixmini, notimeline, xmpp @@ -523,7 +743,7 @@ Version 2022.02 (2022-02-06) Fixed API calls [MrPetovan] Fixed a problem leading to duplicated links [annando] Updated twitteroauth dependency [nupplaphil] - + Closed Issues 9720, 10301, 10365, 10454, 10634, 10691, 10725, 10726, 10729, 10734, 10737, 10739, 10745, 10754, 10767, 10791, 10829, 10832, 10839, 10841, @@ -627,7 +847,7 @@ Version 2021.07 (2021-07-04) The "authenticate" hook was moved deeper into the process [very-ape] Added support for RTL languages [MrPetovan] Added download link for the calendar entries [annando] - + Friendica Addons Updates to the translations DE, HU, IT, JA [translation teams] nitter @@ -652,7 +872,7 @@ Version 2021.07 (2021-07-04) adaptation of new addon functionalities and code improvements [mexon] phpmailer updated dependencies [nupplaphil] - + Closed Issues 7967, 8262, 9715, 9064, 9993, 10055, 10147, 10184, 10198, 10205, 10210, 10219, 10232, 10254, 10287, 10293, 10306, 10312, 10314, 10342, 10364, @@ -1046,7 +1266,7 @@ Version 2020.03 "Red Hot Poker" (2020-03-30) Added fetching of contact relations [annando] unicode emoticons: Extended the list of supported emoticons [loma-one] - + Closed Issues: 4599, 5562, 6205, 6418, 6757, 7558, 7560, 7771, 7808, 7817, 7892, 7964, 7968, 7978, 7984, 7991, 7992, 7994, 8002, 8008, 8014, 8058, @@ -1283,7 +1503,7 @@ Version 2019.06 (2019-06-23) Version 2019.04 (2019-04-28) Friendica Core: Fixed a privacy problem with postings accessed by feed [MrPetovan] - + Version 2019.03 (2019-03-22) Friendica Core: Update to the translation (CS, DE, EN-GB, EN-US, ES, FR, IT, PL, SV, ZH-CN) [translation teams] @@ -1458,7 +1678,7 @@ Version 2019.01 (2019-01-21) 6268, 6282, 6283, 6208, 6289, 6294, 6308, 6309, 6313, 6316, 6323, 6329, 6334, 6344, 6347, 6343, 6349, 6350, 6355, 6358, 6360, 6361, 6363, 6368, 6370, 6391, 6394, 6424, 6425, 6439, 6459 - + Version 2018.09 (2018-09-23) Friendica Core: Update to the translation (CS, DE, EN-US, FI, IT, NL, PL, ZH-CN) [translation teams] @@ -1493,13 +1713,13 @@ Version 2018.09 (2018-09-23) Fixed a bug in the daemon mode of the background worker [annando] Fixed a bug in the frio theme that contact filtering [rabuzarus] Fixed a bug that mangled the display of some additional smileys [abanink] - Fixed a bug in generating registration mails [MrPetovan] + Fixed a bug in generating registration mails [MrPetovan] Fixed a bug that caused blank re-share bodies [MrPetovon] Fixed a bug in the API handling of private mails [fabrixxm] Fixed a bug when calling the mail() function [miqrogroove] Fixed a bug that caused deleted accounts being displayed in the local directory [miqrogroove] Fixed a bug when checking the domain of an email address [VVelox] - Fixed a bug that prevented re-shares from Twitter to be shown as this [annando] + Fixed a bug that prevented re-shares from Twitter to be shown as this [annando] Fixed a bug that caused broken profile links [miqrogroove] Fixed a bug that caused content from unknown accounts appearing in the timeline [annando] Fixed a bug with the ignoring and blocking of contacts [annando] @@ -1672,7 +1892,7 @@ Version 2018.05 (2018-06-01) Friendica Addons: Updates to the translations (DE, EN_GB, EN_US, ES, FI, FR, IS, IT, NL, PL, RU, ZH_CN) [translation teams] - advancedcontentfilter: new addon with advanced filter capabilities [MrPetova] + advancedcontentfilter: new addon with advanced filter capabilities [MrPetovan] catavatar: new addon for profile pictures based on David Revoy's cat-avatar generator [annando, fabrixxm, tobiasd] languagefilter: better help text [andyhee] mathjax: fixed the config form and adopted new CDN URL [tobiasd] diff --git a/CHANGELOG.license b/CHANGELOG.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/CHANGELOG.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/CONTRIBUTING.md.license b/CONTRIBUTING.md.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/CONTRIBUTING.md.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/CREDITS.txt b/CREDITS.txt index 27f9673e26..db97b238f9 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -23,6 +23,7 @@ Andi Stadler Andreas H. Andreas Neustifter Andrej Stieben +Andrey Esin André Alves André Lohan Andy @@ -32,6 +33,7 @@ Anthronaut Anton Antron Samurai Anubis2814 +arcanicanis Arian - Cazare Muncitori Asher Pen atjn @@ -45,6 +47,7 @@ beardyunixer Beatriz Vital Beluga Ben +Ben Millwood Ben Roberts ben-utzer Beringer Zsolt @@ -71,6 +74,7 @@ Christian Wiwie Cohan Robinson Colby Sollars Copiis +cracrayol CrystalStiletto csolisr Cyboulette @@ -90,9 +94,11 @@ Dean Townsley Denis Chenu dependabot[bot] Devlon Duthie +dew-git Diego Souza Domovoy Doru DEACONU +Dr. Tobias Quathamer Dylan Thiedeke Développeur égaré eddy2508 @@ -136,6 +142,7 @@ gudzpoz GunChleoc guzzisti Haakon Meland Eriksen +haheute Hank Grabowski Hannes Heute Hans Meine @@ -147,6 +154,7 @@ hlad hoergen Hubert Kościański Hypolite Petovan +ike Ilmari ImgBotApp irhen @@ -185,8 +193,10 @@ Kris Kristoffer Grundström ktlinux KulikAlex +Laura Hausmann Lea1995polish Leberwurscht +Leonard Leonard Lausen Lionel Triay loma-one @@ -222,10 +232,13 @@ Michal Šupler Michalina Mike Macgirvin miqrogroove +Morgan McMillian mpanhans +MrPetovan mytbk nathilia-peirce Nicola Spanti +ne20002 Nicolas Derive nnsrymni nobody @@ -255,8 +268,10 @@ Pierre Bernardeau Pierre Rudloff Piotr Blonkowski Piotr Strębski +pixelroot pokerazor R C +r1pu5u Rabuzarus Radek Rafael Garau @@ -294,13 +309,14 @@ Senex Petrovic Seth SickShark X Silke Meyer +Simon Simon L'nu Simon Rupf Simó Albert i Beltran +snajafov softmetz soko1 Spencer Dub -SpencerDub St John Karp Stanislav N. Steffen K9 @@ -332,6 +348,7 @@ Tom Tom Aurlund Tom Hu tomamplius +tommy tomson tomtom84 Tony Baldwin Torbjörn Andersson @@ -362,6 +379,7 @@ Wanting Chen Wil Tur Wladimir Palant Wouter Broers +www-data Xiaofei Xu XMPPはいいぞ xundeenergie @@ -374,6 +392,7 @@ Zered zotlabs zottel Zvi ben Yaakov (a.k.a rdc) +Éibhear Ó hAnluain Михаил Олексій Замковий 朱陈锬 diff --git a/CREDITS.txt.license b/CREDITS.txt.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/CREDITS.txt.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/Doxyfile.license b/Doxyfile.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/Doxyfile.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/FEDERATION.md b/FEDERATION.md new file mode 100644 index 0000000000..eaa4f81024 --- /dev/null +++ b/FEDERATION.md @@ -0,0 +1,32 @@ +# Federation + +## Supported federation protocols and standards + +- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server, Server-to-Client) +- [WebFinger](https://webfinger.net/) +- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) +- [NodeInfo](https://nodeinfo.diaspora.software/) +- [Diaspora* Protocol](https://diaspora.github.io/diaspora_federation/) +- [DFRN](https://git.friendi.ca/friendica/friendica/src/branch/develop/spec) + +## Supported FEPs + +- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) +- [FEP-1b12: Group federation](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md) - basics for federation with lemmy, kbin +- [FEP-2677: Identifying the Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md) +- [FEP-e232: Object Links](https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md) +- [FEP-61cf: The OpenWebAuth Protocol](https://codeberg.org/fediverse/fep/src/branch/main/fep/61cf/fep-61cf.md) - basics to log in to Hubzilla +- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) + +## ActivityPub + +- We send a follow activity for the id of a received root post. This is meant as a request to be included in the collection of receivers for this specific post. + +## Diaspora protocol + +Friendica supports most entities of the Diaspora protocol except polls. + +## Additional documentation + +- Documentation is available at every Friendica node at `/help` and in the project repository [friendica/doc](https://git.friendi.ca/friendica/friendica/src/branch/develop/doc) (links work on the nodes documentation). + diff --git a/FEDERATION.md.license b/FEDERATION.md.license new file mode 100644 index 0000000000..985c307f25 --- /dev/null +++ b/FEDERATION.md.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/LICENSES/AGPL-3.0-only.txt b/LICENSES/AGPL-3.0-only.txt new file mode 100644 index 0000000000..be3f7b28e5 --- /dev/null +++ b/LICENSES/AGPL-3.0-only.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 0000000000..0c97efd25b --- /dev/null +++ b/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000000..137069b823 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..ea890afbc7 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) . + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/CC-BY-3.0.txt b/LICENSES/CC-BY-3.0.txt new file mode 100644 index 0000000000..1a16e05564 --- /dev/null +++ b/LICENSES/CC-BY-3.0.txt @@ -0,0 +1,319 @@ +Creative Commons Legal Code + +Attribution 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of this + License. + c. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + d. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + e. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + f. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + g. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + h. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + i. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(b), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(b), as requested. + b. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Section 3(b), in the case of an Adaptation, + a credit identifying the use of the Work in the Adaptation (e.g., + "French translation of the Work by Original Author," or "Screenplay + based on original Work by Original Author"). The credit required by + this Section 4 (b) may be implemented in any reasonable manner; + provided, however, that in the case of a Adaptation or Collection, at + a minimum such credit will appear, if a credit for all contributing + authors of the Adaptation or Collection appears, then as part of these + credits and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may only + use the credit required by this Section for the purpose of attribution + in the manner set out above and, by exercising Your rights under this + License, You may not implicitly or explicitly assert or imply any + connection with, sponsorship or endorsement by the Original Author, + Licensor and/or Attribution Parties, as appropriate, of You or Your + use of the Work, without the separate, express prior written + permission of the Original Author, Licensor and/or Attribution + Parties. + c. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of this License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000000..13ca539f37 --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,156 @@ +Creative Commons Attribution 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC-BY-SA-3.0.txt b/LICENSES/CC-BY-SA-3.0.txt new file mode 100644 index 0000000000..604209a804 --- /dev/null +++ b/LICENSES/CC-BY-SA-3.0.txt @@ -0,0 +1,359 @@ +Creative Commons Legal Code + +Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined below) for the purposes of this + License. + c. "Creative Commons Compatible License" means a license that is listed + at https://creativecommons.org/compatiblelicenses that has been + approved by Creative Commons as being essentially equivalent to this + License, including, at a minimum, because that license: (i) contains + terms that have the same purpose, meaning and effect as the License + Elements of this License; and, (ii) explicitly permits the relicensing + of adaptations of works made available under that license under this + License or a Creative Commons jurisdiction license with the same + License Elements as this License. + d. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + e. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, ShareAlike. + f. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + g. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + h. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + i. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + j. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + k. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(c), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(c), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under the + terms of: (i) this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible + License. If you license the Adaptation under one of the licenses + mentioned in (iv), you must comply with the terms of that license. If + you license the Adaptation under the terms of any of the licenses + mentioned in (i), (ii) or (iii) (the "Applicable License"), you must + comply with the terms of the Applicable License generally and the + following provisions: (I) You must include a copy of, or the URI for, + the Applicable License with every copy of each Adaptation You + Distribute or Publicly Perform; (II) You may not offer or impose any + terms on the Adaptation that restrict the terms of the Applicable + License or the ability of the recipient of the Adaptation to exercise + the rights granted to that recipient under the terms of the Applicable + License; (III) You must keep intact all notices that refer to the + Applicable License and to the disclaimer of warranties with every copy + of the Work as included in the Adaptation You Distribute or Publicly + Perform; (IV) when You Distribute or Publicly Perform the Adaptation, + You may not impose any effective technological measures on the + Adaptation that restrict the ability of a recipient of the Adaptation + from You to exercise the rights granted to that recipient under the + terms of the Applicable License. This Section 4(b) applies to the + Adaptation as incorporated in a Collection, but this does not require + the Collection apart from the Adaptation itself to be made subject to + the terms of the Applicable License. + c. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Ssection 3(b), in the case of an + Adaptation, a credit identifying the use of the Work in the Adaptation + (e.g., "French translation of the Work by Original Author," or + "Screenplay based on original Work by Original Author"). The credit + required by this Section 4(c) may be implemented in any reasonable + manner; provided, however, that in the case of a Adaptation or + Collection, at a minimum such credit will appear, if a credit for all + contributing authors of the Adaptation or Collection appears, then as + part of these credits and in a manner at least as prominent as the + credits for the other contributing authors. For the avoidance of + doubt, You may only use the credit required by this Section for the + purpose of attribution in the manner set out above and, by exercising + Your rights under this License, You may not implicitly or explicitly + assert or imply any connection with, sponsorship or endorsement by the + Original Author, Licensor and/or Attribution Parties, as appropriate, + of You or Your use of the Work, without the separate, express prior + written permission of the Original Author, Licensor and/or Attribution + Parties. + d. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of the License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/LICENSES/CC-BY-SA-4.0.txt b/LICENSES/CC-BY-SA-4.0.txt new file mode 100644 index 0000000000..835a6836b3 --- /dev/null +++ b/LICENSES/CC-BY-SA-4.0.txt @@ -0,0 +1,170 @@ +Creative Commons Attribution-ShareAlike 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. + +Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution-ShareAlike 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. + + C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. + + 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC-PDDC.txt b/LICENSES/CC-PDDC.txt new file mode 100644 index 0000000000..846c9c00b8 --- /dev/null +++ b/LICENSES/CC-PDDC.txt @@ -0,0 +1,9 @@ +Creative Commons Public Domain Dedication and Certification + +The person or persons who have associated work with this document (the "Dedicator" or "Certifier") hereby either (a) certifies that, to the best of his knowledge, the work of authorship identified is in the public domain of the country from which the work is published, or (b) hereby dedicates whatever copyright the dedicators holds in the work of authorship identified below (the "Work") to the public domain. A certifier, moreover, dedicates any copyright interest he may have in the associated work, and for these purposes, is described as a "dedicator" below. + +A certifier has taken reasonable steps to verify the copyright status of this work. Certifier recognizes that his good faith efforts may not shield him from liability if in fact the work certified is not in the public domain. + +Dedicator makes this dedication for the benefit of the public at large and to the detriment of the Dedicator's heirs and successors. Dedicator intends this dedication to be an overt act of relinquishment in perpetuity of all present and future rights under copyright law, whether vested or contingent, in the Work. Dedicator understands that such relinquishment of all rights includes the relinquishment of all rights to enforce (by lawsuit or otherwise) those copyrights in the Work. + +Dedicator recognizes that, once placed in the public domain, the Work may be freely reproduced, distributed, transmitted, used, modified, built upon, or otherwise exploited by anyone for any purpose, commercial or non-commercial, and in any way, including by methods that have not yet been invented or conceived. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000000..17cb286430 --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,117 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000000..17cb286430 --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,117 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-3.0-only.txt b/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000000..f6cdd22a6c --- /dev/null +++ b/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000000..2071b23b0e --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/OFL-1.1.txt b/LICENSES/OFL-1.1.txt new file mode 100644 index 0000000000..6fe84ee21e --- /dev/null +++ b/LICENSES/OFL-1.1.txt @@ -0,0 +1,43 @@ +SIL OPEN FONT LICENSE + +Version 1.1 - 26 February 2007 + +PREAMBLE + +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS + +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION + +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/LICENSES/WTFPL.txt b/LICENSES/WTFPL.txt new file mode 100644 index 0000000000..7a3094a826 --- /dev/null +++ b/LICENSES/WTFPL.txt @@ -0,0 +1,11 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md index 4dedf006b1..d4a49330e4 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,28 @@ Friendica - your open and free social network Welcome to the free social web. Friendica is a platform for decentralised social communication linking to other independent social and corporate services. -Friendica connects you to a federated communications network of thousands of servers called the Fediverse. Through various protocols you can interact with anyone on [Friendica]( https://friendi.ca), [Mastodon](https://joinmastodon.org), [Lemmy](https://join-lemmy.org/), [Diaspora](https://diasporafoundation.org), [Misskey](https://join.misskey.page), [Peertube](https://joinpeertube.org/), [Pixelfed](https://pixelfed.org/), [Pleroma](https://pleroma.social) and many more. Receiving content from Tumblr, Wordpress and RSS is also possible. Friendica allows to import and mirror your content via add-ons such as ITTT and Buffer. You can customize and control how publicly or privately you want to share your content. +Friendica connects you to a federated communications network of thousands of servers called the Fediverse. +Through various protocols you can interact with anyone on [Friendica]( https://friendi.ca), [Mastodon](https://joinmastodon.org), [Lemmy](https://join-lemmy.org/), [Diaspora](https://diasporafoundation.org), [Misskey](https://join.misskey.page), [Peertube](https://joinpeertube.org/), [Pixelfed](https://pixelfed.org/), [Pleroma](https://pleroma.social) and many more. +Receiving content from Tumblr, WordPress and RSS is also possible. +Friendica allows to import and mirror your content via add-ons such as ITTT and Buffer. +You can control the privacy scope of your content. -Being part of the Fediverse allows you to be free from data-harvesting corporations. Enjoy open social communication, independent of any specific provider. +Being part of the Fediverse allows you to be free from data-harvesting corporations. +Enjoy open social communication, independent of any specific provider. -[Join Friendica](https://dir.friendica.social/servers) today or setup [your own Friendica instance](doc/Install.md). +[Join Friendica](https://dir.friendica.social/servers) today or set up [your own Friendica instance](doc/Install.md). ### Friendica on desktop -![Frio theme in desktop browser](images/screenshots/friendica-2023-10-frio-desktop.png?raw=true "Frio theme in desktop browser") +![Frio theme in desktop browser](images/screenshots/friendica-2023-12-frio-desktop.png?raw=true "Frio theme in desktop browser") ### Friendica on mobile

-frio on mobile, dark color scheme -frio on mobile, light color scheme +frio on mobile, dark color scheme +frio on mobile, light color scheme

-### Alternative Theme "Vier" - -![Vier theme in desktop browser](images/screenshots/friendica-vier-community.png?raw=true "Vier theme in desktop browser") - ## Endorsements -- [![Awesome Humane Tech](images/humane-tech-badge.svg)](https://codeberg.org/teaserbot-labs/delightful-humane-design) In August 2020 Friendica was added to the curated delightful humane design resources in the [Fediverse category](https://codeberg.org/teaserbot-labs/delightful-humane-design#fediverse). +- Friendica is listed on [![Awesome Humane Tech](images/humane-tech-badge.svg)](https://codeberg.org/teaserbot-labs/delightful-humane-design) in the [Fediverse category](https://codeberg.org/teaserbot-labs/delightful-humane-design#fediverse). diff --git a/README.md.license b/README.md.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/README.md.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000000..b7e959535b --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,306 @@ +version = 1 +SPDX-PackageName = "Friendica" +SPDX-PackageSupplier = "Friendica Project " +SPDX-PackageDownloadLocation = "https://friendi.ca" + +[[annotations]] +path = [ + "database.sql", + "composer.*", + "bin/dev/php-cs-fixer/composer.*", + "doc/**", + "spec/*", + "tests/**", + ".devcontainer/**", + ".vscode/*", + ".phpactor.json", + "**/.gitignore", + "src/**/README.md", + "mods/**", + "static/*.jsonld", + ".github/ISSUE_TEMPLATE/*", + "view/theme/frio/README.md", + "view/theme/vier/README.md", + "view/theme/quattro/TODO", + "view/theme/quattro/**Makefile", + "view/theme/**/screenshot.jpg", + "view/theme/**/screenshot.png", + "view/theme/duepuntozero/deriv/**", + "images/icons/**", + "images/friendica*", + "images/ff*", + "images/person-*.jpg", + "view/install/*.png", + "view/theme/quattro/icons/*", + "view/theme/smoothly/images/*", + "images/screenshots/*", + "view/theme/frio/img/**", + "config/local-sample.config.php", + "images/article.gif", + "images/audio.gif", + "images/b_block.gif", + "images/b_drop.gif", + "images/b_drop.png", + "images/b_drophide.gif", + "images/b_dropshow.gif", + "images/b_edit.gif", + "images/b_edit.png", + "images/beer_mug.gif", + "images/blank.png", + "images/bug-x.gif", + "images/calendar.png", + "images/camera-icon.gif", + "images/coffee.gif", + "images/connect-bg.png", + "images/content-types.png", + "images/default-profile-mm.jpg", + "images/default-profile-sm.jpg", + "images/default-profile.jpg", + "images/dislike.gif", + "images/document.gif", + "images/globe.gif", + "images/hide_off.png", + "images/hide_on.png", + "images/icons.png", + "images/larrow.gif", + "images/larrw.gif", + "images/like.gif", + "images/link-icon.gif", + "images/lock_icon.gif", + "images/lrarrow.gif", + "images/mail.png", + "images/mapicon.gif", + "images/no.gif", + "images/noglobe.gif", + "images/nosign.jpg", + "images/onoff.jpg", + "images/pause.gif", + "images/pen.png", + "images/pencil.gif", + "images/penhover.png", + "images/people.gif", + "images/play.gif", + "images/plugin.png", + "images/rarrow.gif", + "images/rarrw.gif", + "images/recycle.gif", + "images/remote-link.gif", + "images/rotator.gif", + "images/search_18.png", + "images/selected.png", + "images/share.gif", + "images/show_all_off.png", + "images/show_all_on.png", + "images/show_off.png", + "images/show_on.png", + "images/smiley-Oo.gif", + "images/smiley-bangheaddesk.gif", + "images/smiley-brokenheart.gif", + "images/smiley-cool.gif", + "images/smiley-cry.gif", + "images/smiley-embarrassed.gif", + "images/smiley-facepalm.gif", + "images/smiley-foot-in-mouth.gif", + "images/smiley-frown.gif", + "images/smiley-heart.gif", + "images/smiley-innocent.gif", + "images/smiley-kiss.gif", + "images/smiley-laughing.gif", + "images/smiley-money-mouth.gif", + "images/smiley-sealed.gif", + "images/smiley-shaka.gif", + "images/smiley-smile.gif", + "images/smiley-surprised.gif", + "images/smiley-thumbsup.gif", + "images/smiley-tongue-out.gif", + "images/smiley-undecided.gif", + "images/smiley-wink.gif", + "images/smiley-yell.gif", + "images/spencil.gif", + "images/star.png", + "images/star_dummy.png", + "images/tools.png", + "images/twopeople.png", + "images/unlock_icon.gif", + "images/video.gif", + "images/logo.png", +] +SPDX-FileCopyrightText = "2010-2024 The Friendica Project" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = [ + "view/lang/**/strings.php", + "view/lang/**/messages.po" + ] +SPDX-FileCopyrightText = "2010-2024 The Friendica Project" +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "view/fonts/*" +SPDX-FileCopyrightText = "2014, Andi Stadler for the Friendica project" +SPDX-License-Identifier = "CC-BY-SA-4.0" + +[[annotations]] +path = [ + "view/theme/vier/css/font-awesome*.css", + "view/theme/vier/css/font2.css" +] +SPDX-FileCopyrightText = "Dave Gandy" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/vier/font/*" +SPDX-FileCopyrightText = "Dave Gandy" +SPDX-License-Identifier = "OFL-1.1" + +[[annotations]] +path = "view/theme/frio/font/open_sans/**" +SPDX-FileCopyrightText = "2010 Steve Matteson, Google LLC" +SPDX-License-Identifier = "OFL-1.1" + +[[annotations]] +path = "view/theme/frio/frameworks/bootstrap/**" +SPDX-FileCopyrightText = "2011-2019 Twitter, Inc." +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/bootstrap-colorpicker/**" +SPDX-FileCopyrightText = "2012 Stefan Petre" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "view/theme/frio/frameworks/bootstrap-select/**" +SPDX-FileCopyrightText = "2013-2026 bootstrap-select" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/bootstrap-toggle/**" +SPDX-FileCopyrightText = "2014 Min Hur, The New York Times Company" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/jasny/**" +SPDX-FileCopyrightText = "2012-2014 Arnold Daniels" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "view/theme/frio/frameworks/justifiedGallery/**" +SPDX-FileCopyrightText = "2023 Miro Mannino, Hypolite Petovan" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/js/fancybox/**" +SPDX-FileCopyrightText = "2019 fancyApps" +SPDX-License-Identifier = "GPL-3.0-only" + +[[annotations]] +path = [ + "view/theme/smoothly/js/modernizr.custom.2.5.3.*js", + "view/js/modernizr.js" +] +SPDX-FileCopyrightText = "modernizrJS" +SPDX-License-Identifier = "MIT OR BSD-3-Clause" + +[[annotations]] +path = "view/theme/frio/frameworks/flexMenu/*" +SPDX-FileCopyrightText = "2012-2014 352 Inc. & Contributors" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = [ + "view/theme/quattro/jquery.tools.min.js", + "view/theme/frio/js/jquery.tools.min.js" +] +SPDX-FileCopyrightText = "NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE. http://flowplayer.org/tools/" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "view/theme/frio/php/PHPColors/*" +SPDX-FileCopyrightText = "Arlo Carreon" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/autosize/*" +SPDX-FileCopyrightText = "Jack Moore" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/sticky-kit/jquery.sticky-kit.min.js" +SPDX-FileCopyrightText = "2015 Leaf Corcoran" +SPDX-License-Identifier = "WTFPL" + +[[annotations]] +path = "view/theme/frio/frameworks/jquery-scrollspy/*" +SPDX-FileCopyrightText = "2011 Samuel Alexander, 2015 SoftwareSpot" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/ekko-lightbox/*" +SPDX-FileCopyrightText = "2013 Ashley White" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/theme/frio/frameworks/awesome-bootstrap-checkbox/*" +SPDX-FileCopyrightText = "2014 Philip Daineka" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/js/vanillaEmojiPicker/*" +SPDX-FileCopyrightText = "2020, woody180 https://github.com/woody180/vanilla-javascript-emoji-picker" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "view/js/country.js" +SPDX-FileCopyrightText = "Jim Carlock" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "view/js/hls/**" +SPDX-FileCopyrightText = "2017 Dailymotion (http://www.dailymotion.com)" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = [ "images/rm-16.png", "images/rhash-16.png"] +SPDX-FileCopyrightText = "Red Matrix Project" +SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "images/pumpio.png" +SPDX-FileCopyrightText = "2012 E14N" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "images/buffer.png" +SPDX-FileCopyrightText = "buffer.com" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "images/default/corgidon.png" +SPDX-FileCopyrightText = "Coridon https://github.com/corgidon/" +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "images/default/koyuspace.png" +SPDX-FileCopyrightText = "Koyuspace https://github.com/koyuspace/mastodon" +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = "images/default/pleroma.png" +SPDX-FileCopyrightText = "017-2022 Pleroma Authors " +SPDX-License-Identifier = "CC-BY-SA-4.0" + +[[annotations]] +path = "images/dreamwidth.png" +SPDX-FileCopyrightText = "Dreamwidth Studios, LLC. https://www.dreamwidth.org/site/opensource" +SPDX-License-Identifier = "CC-BY-SA-3.0" + +[[annotations]] +path = "images/insanejournal.gif" +SPDX-FileCopyrightText = "Insane Journal https://www.insanejournal.com" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "images/humane-tech-badge.svg" +SPDX-FileCopyrightText = "delightful humane design" +SPDX-License-Identifier = "CC0-1.0" diff --git a/REUSE.toml.license b/REUSE.toml.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/REUSE.toml.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/VERSION b/VERSION index c0d71d8545..2d4bb52626 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2023.12 +2025.02-dev diff --git a/VERSION.license b/VERSION.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/VERSION.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/Vagrantfile b/Vagrantfile index 1687351231..60117e9cf6 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -7,7 +7,7 @@ public_folder = "/vagrant" Vagrant.configure(2) do |config| # Set server to Debian 11 / Bullseye 64bit - config.vm.box = "debian/bullseye64" + config.vm.box = "debian/bookworm64" # Disable automatic box update checking. If you disable this, then # boxes will only be checked for updates when the user runs diff --git a/Vagrantfile.license b/Vagrantfile.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/Vagrantfile.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/bin/.htaccess.license b/bin/.htaccess.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/bin/.htaccess.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/bin/auth_ejabberd.php b/bin/auth_ejabberd.php index 3a95532464..595ccf204a 100755 --- a/bin/auth_ejabberd.php +++ b/bin/auth_ejabberd.php @@ -1,22 +1,10 @@ #!/usr/bin/env php . + * SPDX-License-Identifier: AGPL-3.0-or-later * * ejabberd extauth script for the integration with friendica * @@ -56,44 +44,12 @@ if (php_sapi_name() !== 'cli') { exit(); } -use Dice\Dice; -use Friendica\App\Mode; -use Friendica\Core\Logger\Capability\LogChannel; -use Friendica\Security\ExAuth; -use Psr\Log\LoggerInterface; - -if (sizeof($_SERVER["argv"]) == 0) { - die(); -} - -$directory = dirname($_SERVER["argv"][0]); - -if (substr($directory, 0, 1) != DIRECTORY_SEPARATOR) { - $directory = $_SERVER["PWD"] . DIRECTORY_SEPARATOR . $directory; -} - -$directory = realpath($directory . DIRECTORY_SEPARATOR . ".."); - -chdir($directory); +chdir(dirname(__DIR__)); require dirname(__DIR__) . '/vendor/autoload.php'; -$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); -/** @var \Friendica\Core\Addon\Capability\ICanLoadAddons $addonLoader */ -$addonLoader = $dice->create(\Friendica\Core\Addon\Capability\ICanLoadAddons::class); -$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies')); -$dice = $dice->addRule(LoggerInterface::class,['constructParams' => [LogChannel::AUTH_JABBERED]]); +$container = \Friendica\Core\DiceContainer::fromBasePath(dirname(__DIR__)); -\Friendica\DI::init($dice); -\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); +$app = \Friendica\App::fromContainer($container); -// Check the database structure and possibly fixes it -\Friendica\Core\Update::check(\Friendica\DI::basePath(), true); - -$appMode = $dice->create(Mode::class); - -if ($appMode->isNormal()) { - /** @var ExAuth $oAuth */ - $oAuth = $dice->create(ExAuth::class); - $oAuth->readStdin(); -} +$app->processEjabberd($_SERVER); diff --git a/bin/composer.phar b/bin/composer.phar index 52f548bd23..b58b584bb9 100755 Binary files a/bin/composer.phar and b/bin/composer.phar differ diff --git a/bin/composer.phar.license b/bin/composer.phar.license new file mode 100644 index 0000000000..57e47667a3 --- /dev/null +++ b/bin/composer.phar.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2012 Nils Adermann, Jordi Boggiano + +SPDX-License-Identifier: MIT diff --git a/bin/console b/bin/console index 79b6c8d3b9..82a0510c66 100755 --- a/bin/console +++ b/bin/console @@ -1,10 +1,14 @@ -#!/bin/bash +#!/bin/sh -dir=$(cd "${0%[/\\]*}" > /dev/null; pwd) +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 -if [[ -d /proc/cygdrive && $(which php) == $(readlink -n /proc/cygdrive)/* ]]; then +dir=$(cd "$(dirname "$0")" > /dev/null 2>&1; pwd) + +if [ -d /proc/cygdrive ] && [ "$(which php)" = "$(readlink -n /proc/cygdrive)/*" ]; then # We are in Cygwin using Windows php, so the path must be translated - dir=$(cygpath -m "$dir"); + dir=$(cygpath -m "$dir") fi php "${dir}/console.php" "$@" diff --git a/bin/console.bat b/bin/console.bat index 06c41a03e6..438c4da0b2 100755 --- a/bin/console.bat +++ b/bin/console.bat @@ -1,3 +1,7 @@ +REM SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +REM +REM SPDX-License-Identifier: CC0-1.0 + @echo OFF :: in case DelayedExpansion is on and a path contains ! setlocal DISABLEDELAYEDEXPANSION diff --git a/bin/console.php b/bin/console.php index b797e6ba99..88382e1665 100755 --- a/bin/console.php +++ b/bin/console.php @@ -1,22 +1,10 @@ #!/usr/bin/env php . + * SPDX-License-Identifier: AGPL-3.0-or-later * */ @@ -25,21 +13,13 @@ if (php_sapi_name() !== 'cli') { exit(); } -use Dice\Dice; -use Friendica\Core\Logger\Capability\LogChannel; -use Friendica\DI; -use Psr\Log\LoggerInterface; +// Ensure that te console is executed from the base path of the installation +chdir(dirname(__DIR__)); require dirname(__DIR__) . '/vendor/autoload.php'; -$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); -/** @var \Friendica\Core\Addon\Capability\ICanLoadAddons $addonLoader */ -$addonLoader = $dice->create(\Friendica\Core\Addon\Capability\ICanLoadAddons::class); -$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies')); -$dice = $dice->addRule(LoggerInterface::class, ['constructParams' => [LogChannel::CONSOLE]]); +$container = \Friendica\Core\DiceContainer::fromBasePath(dirname(__DIR__)); -/// @fixme Necessary until Hooks inside the Logger can get loaded without the DI-class -DI::init($dice); -\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); +$app = \Friendica\App::fromContainer($container); -(new Friendica\Core\Console($dice, $argv))->execute(); +$app->processConsole($_SERVER); diff --git a/bin/daemon.php b/bin/daemon.php index 53a564b8bc..3fa1b60a68 100755 --- a/bin/daemon.php +++ b/bin/daemon.php @@ -1,23 +1,12 @@ #!/usr/bin/env php . + * SPDX-License-Identifier: AGPL-3.0-or-later * + * @deprecated 2025.02 use `bin/console.php daemon` instead */ /** @@ -30,231 +19,20 @@ if (php_sapi_name() !== 'cli') { exit(); } -use Dice\Dice; -use Friendica\App\Mode; -use Friendica\Core\Logger; -use Friendica\Core\Update; -use Friendica\Core\Worker; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Util\DateTimeFormat; -use Psr\Log\LoggerInterface; - -// Get options -$shortopts = 'f'; -$longopts = ['foreground']; -$options = getopt($shortopts, $longopts); - // Ensure that daemon.php is executed from the base path of the installation -if (!file_exists('index.php') && (sizeof($_SERVER['argv']) != 0)) { - $directory = dirname($_SERVER['argv'][0]); - - if (substr($directory, 0, 1) != '/') { - $directory = $_SERVER['PWD'] . '/' . $directory; - } - $directory = realpath($directory . '/..'); - - chdir($directory); -} +chdir(dirname(__DIR__)); require dirname(__DIR__) . '/vendor/autoload.php'; -$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); -/** @var \Friendica\Core\Addon\Capability\ICanLoadAddons $addonLoader */ -$addonLoader = $dice->create(\Friendica\Core\Addon\Capability\ICanLoadAddons::class); -$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies')); -$dice = $dice->addRule(LoggerInterface::class, ['constructParams' => [Logger\Capability\LogChannel::DAEMON]]); +fwrite(STDOUT, '`bin/daemon.php` is deprecated since 2025.02 and will be removed in 5 months, please use `bin/console.php daemon` instead.' . \PHP_EOL); -DI::init($dice); -\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); +// BC: Add console command as second argument +$argv = $_SERVER['argv'] ?? []; +array_splice($argv, 1, 0, "daemon"); +$_SERVER['argv'] = $argv; -if (DI::mode()->isInstall()) { - die("Friendica isn't properly installed yet.\n"); -} +$container = \Friendica\Core\DiceContainer::fromBasePath(dirname(__DIR__)); -DI::mode()->setExecutor(Mode::DAEMON); +$app = \Friendica\App::fromContainer($container); -DI::config()->reload(); - -if (empty(DI::config()->get('system', 'pidfile'))) { - die(<< [ - 'pidfile' => '/path/to/daemon.pid', - ], -TXT - ); -} - -$pidfile = DI::config()->get('system', 'pidfile'); - -if (in_array('start', $_SERVER['argv'])) { - $mode = 'start'; -} - -if (in_array('stop', $_SERVER['argv'])) { - $mode = 'stop'; -} - -if (in_array('status', $_SERVER['argv'])) { - $mode = 'status'; -} - -$foreground = array_key_exists('f', $options) || array_key_exists('foreground', $options); - -if (!isset($mode)) { - die("Please use either 'start', 'stop' or 'status'.\n"); -} - -if (empty($_SERVER['argv'][0])) { - die("Unexpected script behaviour. This message should never occur.\n"); -} - -$pid = null; - -if (is_readable($pidfile)) { - $pid = intval(file_get_contents($pidfile)); -} - -if (empty($pid) && in_array($mode, ['stop', 'status'])) { - DI::keyValue()->set('worker_daemon_mode', false); - die("Pidfile wasn't found. Is the daemon running?\n"); -} - -if ($mode == 'status') { - if (posix_kill($pid, 0)) { - die("Daemon process $pid is running.\n"); - } - - unlink($pidfile); - - DI::keyValue()->set('worker_daemon_mode', false); - die("Daemon process $pid isn't running.\n"); -} - -if ($mode == 'stop') { - posix_kill($pid, SIGTERM); - - unlink($pidfile); - - Logger::notice('Worker daemon process was killed', ['pid' => $pid]); - - DI::keyValue()->set('worker_daemon_mode', false); - die("Worker daemon process $pid was killed.\n"); -} - -if (!empty($pid) && posix_kill($pid, 0)) { - die("Daemon process $pid is already running.\n"); -} - -Logger::notice('Starting worker daemon.', ['pid' => $pid]); - -if (!$foreground) { - echo "Starting worker daemon.\n"; - - DBA::disconnect(); - - // Fork a daemon process - $pid = pcntl_fork(); - if ($pid == -1) { - echo "Daemon couldn't be forked.\n"; - Logger::warning('Could not fork daemon'); - exit(1); - } elseif ($pid) { - // The parent process continues here - if (!file_put_contents($pidfile, $pid)) { - echo "Pid file wasn't written.\n"; - Logger::warning('Could not store pid file'); - posix_kill($pid, SIGTERM); - exit(1); - } - echo 'Child process started with pid ' . $pid . ".\n"; - Logger::notice('Child process started', ['pid' => $pid]); - exit(0); - } - - // We now are in the child process - register_shutdown_function('shutdown'); - - // Make the child the main process, detach it from the terminal - if (posix_setsid() < 0) { - return; - } - - // Closing all existing connections with the outside - fclose(STDIN); - - // And now connect the database again - DBA::connect(); -} - -DI::keyValue()->set('worker_daemon_mode', true); - -// Just to be sure that this script really runs endlessly -set_time_limit(0); - -$wait_interval = intval(DI::config()->get('system', 'cron_interval', 5)) * 60; - -$do_cron = true; -$last_cron = 0; - -// Now running as a daemon. -while (true) { - // Check the database structure and possibly fixes it - Update::check(DI::basePath(), true); - - if (!$do_cron && ($last_cron + $wait_interval) < time()) { - Logger::info('Forcing cron worker call.', ['pid' => $pid]); - $do_cron = true; - } - - if ($do_cron || (!DI::system()->isMaxLoadReached() && Worker::entriesExists() && Worker::isReady())) { - Worker::spawnWorker($do_cron); - } else { - Logger::info('Cool down for 5 seconds', ['pid' => $pid]); - sleep(5); - } - - if ($do_cron) { - // We force a reconnect of the database connection. - // This is done to ensure that the connection don't get lost over time. - DBA::reconnect(); - - $last_cron = time(); - } - - $start = time(); - Logger::info('Sleeping', ['pid' => $pid, 'until' => gmdate(DateTimeFormat::MYSQL, $start + $wait_interval)]); - - do { - $seconds = (time() - $start); - - // logarithmic wait time calculation. - // Background: After jobs had been started, they often fork many workers. - // To not waste too much time, the sleep period increases. - $arg = (($seconds + 1) / ($wait_interval / 9)) + 1; - $sleep = min(1000000, round(log10($arg) * 1000000, 0)); - usleep($sleep); - - $pid = pcntl_waitpid(-1, $status, WNOHANG); - if ($pid > 0) { - Logger::info('Children quit via pcntl_waitpid', ['pid' => $pid, 'status' => $status]); - } - - $timeout = ($seconds >= $wait_interval); - } while (!$timeout && !Worker\IPC::JobsExists()); - - if ($timeout) { - $do_cron = true; - Logger::info('Woke up after $wait_interval seconds.', ['pid' => $pid, 'sleep' => $wait_interval]); - } else { - $do_cron = false; - Logger::info('Worker jobs are calling to be forked.', ['pid' => $pid]); - } -} - -function shutdown() { - posix_kill(posix_getpid(), SIGTERM); - posix_kill(posix_getpid(), SIGHUP); -} +$app->processConsole($_SERVER); diff --git a/bin/dev/autotest.sh b/bin/dev/autotest.sh index b6f67cf118..a3f01d9656 100755 --- a/bin/dev/autotest.sh +++ b/bin/dev/autotest.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash # +# SPDX-FileCopyrightText: 2010-2024 the Friendica project +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# # This script is used for autotesting the Friendica codebase with different # types of tests and environments. # @@ -138,9 +142,9 @@ function execute_tests() { if [ -n "${USEDOCKER}" ]; then echo "Fire up the mysql docker" DOCKER_CONTAINER_ID=$(docker run \ - -e MYSQL_ROOT_PASSWORD=friendica \ + -e MYSQL_ROOT_PASSWORD="${DATABASE_PASSWORD}" \ -e MYSQL_USER="${DATABASE_USER}" \ - -e MYSQL_PASSWORD=friendica \ + -e MYSQL_PASSWORD="${DATABASE_PASSWORD}" \ -e MYSQL_DATABASE="${DATABASE_NAME}" \ -d mysql) DATABASE_HOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "${DOCKER_CONTAINER_ID}") @@ -152,8 +156,8 @@ function execute_tests() { echo "To use the docker container set the USEDOCKER environment variable" exit 3 fi - mysql -u "${DATABASE_USER}" -pfriendica -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true - mysql -u "${DATABASE_USER}" -pfriendica -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST} + mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true + mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST} else DATABASE_HOST=mysql fi @@ -171,9 +175,9 @@ function execute_tests() { if [ -n "${USEDOCKER}" ]; then echo "Fire up the mariadb docker" DOCKER_CONTAINER_ID=$(docker run \ - -e MYSQL_ROOT_PASSWORD=friendica \ + -e MYSQL_ROOT_PASSWORD="${DATABASE_PASSWORD}" \ -e MYSQL_USER="${DATABASE_USER}" \ - -e MYSQL_PASSWORD=friendica \ + -e MYSQL_PASSWORD="${DATABASE_PASSWORD}" \ -e MYSQL_DATABASE="${DATABASE_NAME}" \ -d mariadb) DATABASE_HOST=$(docker inspect --format="{{.NetworkSettings.IPAddress}}" "${DOCKER_CONTAINER_ID}") @@ -185,8 +189,8 @@ function execute_tests() { echo "To use the docker container set the USEDOCKER environment variable" exit 3 fi - mysql -u "${DATABASE_USER}" -pfriendica -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true - mysql -u "${DATABASE_USER}" -pfriendica -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST} + mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "DROP DATABASE IF EXISTS ${DATABASE_NAME}" -h ${DATABASE_HOST} || true + mysql -u "${DATABASE_USER}" -p"${DATABASE_PASSWORD}" -e "CREATE DATABASE ${DATABASE_NAME} DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" -h ${DATABASE_HOST} else DATABASE_HOST=mariadb fi @@ -203,14 +207,14 @@ function execute_tests() { if [ -n "${USEDOCKER}" ]; then echo "Initialize database..." - docker exec ${DOCKER_CONTAINER_ID} mysql -u root -pfriendica -e "CREATE DATABASE IF NOT EXISTS ${DATABASE_NAME};" + docker exec ${DOCKER_CONTAINER_ID} mysql -u root -p"${DATABASE_PASSWORD}" -e "CREATE DATABASE IF NOT EXISTS ${DATABASE_NAME};" fi export MYSQL_HOST="${DATABASE_HOST}" #call installer echo "Installing Friendica..." - "${PHP}" ./bin/console.php autoinstall --dbuser="${DATABASE_USER}" --dbpass=friendica --dbdata="${DATABASE_NAME}" --dbhost="${DATABASE_HOST}" --url=https://friendica.local --admin=admin@friendica.local + "${PHP}" ./bin/console.php autoinstall --dbuser="${DATABASE_USER}" --dbpass="${DATABASE_PASSWORD}" --dbdata="${DATABASE_NAME}" --dbhost="${DATABASE_HOST}" --url=https://friendica.local --admin=admin@friendica.local fi #test execution diff --git a/bin/dev/fix-codestyle.sh b/bin/dev/fix-codestyle.sh new file mode 100755 index 0000000000..3a66b2ec8c --- /dev/null +++ b/bin/dev/fix-codestyle.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2010-2025 the Friendica project +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# this script checks or fixes php-files, based on the php-cs rules +# +# You can use the following variables: +# COMMAND ... the php-cs command to execute (default is "check --diff") +# TARGET_BRANCH ... set the target branch for the current branch to create a diff between them +# +## + +COMMAND=${COMMAND:-"check --diff"} + +if [ -n "${TARGET_BRANCH}" ]; then + CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB "$(git ls-remote -q | grep refs/heads/"${TARGET_BRANCH}"$ | awk '{print $1}' | xargs git rev-parse )".."$(git rev-parse HEAD)")"; +else + CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB "$(git rev-parse HEAD)")"; +fi + +EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}"); + +./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer ${COMMAND} --config=.php-cs-fixer.dist.php -v --using-cache=no ${EXTRA_ARGS} diff --git a/bin/dev/make_credits.py b/bin/dev/make_credits.py index be7a52e322..8492f96eba 100755 --- a/bin/dev/make_credits.py +++ b/bin/dev/make_credits.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2010-2024 the Friendica project +# +# SPDX-License-Identifier: AGPL-3.0-or-later + """ This script will collect the contributors to friendica and its translations from * the git log of the friendica core and addons repositories diff --git a/bin/dev/minifyjs.sh b/bin/dev/minifyjs.sh index 2c38cf8801..22c02f80cc 100755 --- a/bin/dev/minifyjs.sh +++ b/bin/dev/minifyjs.sh @@ -1,5 +1,9 @@ #!/bin/bash +# SPDX-FileCopyrightText: 2010-2024 the Friendica project +# +# SPDX-License-Identifier: AGPL-3.0-or-later + command -v uglifyjs >/dev/null 2>&1 || { echo >&2 "I require UglifyJS but it's not installed. Aborting."; exit 1; } MINIFY_CMD=uglifyjs diff --git a/bin/dev/php-cs-fixer/composer.json b/bin/dev/php-cs-fixer/composer.json index 9d571fda34..b0ac7ffc73 100644 --- a/bin/dev/php-cs-fixer/composer.json +++ b/bin/dev/php-cs-fixer/composer.json @@ -1,5 +1,5 @@ { "require": { - "friendsofphp/php-cs-fixer": "^2.18" + "friendsofphp/php-cs-fixer": "^3.46" } } diff --git a/bin/dev/php-cs-fixer/composer.lock b/bin/dev/php-cs-fixer/composer.lock index bf0642abb2..71fca5fdcf 100644 --- a/bin/dev/php-cs-fixer/composer.lock +++ b/bin/dev/php-cs-fixer/composer.lock @@ -4,27 +4,162 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f2e42b9a6e381e9aa8be6280c22d5968", + "content-hash": "df2cc2d63945b734c8edd007837e47ad", "packages": [ { - "name": "composer/semver", - "version": "3.2.4", + "name": "clue/ndjson-react", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81", + "reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "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" + ], + "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-08-19T19:43:53+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.54", + "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", @@ -80,29 +215,31 @@ "type": "tidelift" } ], - "time": "2020-11-13T08:59:24+00:00" + "time": "2024-07-12T11:35:52+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.6", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -139,37 +276,32 @@ "type": "tidelift" } ], - "time": "2021-03-25T17:01:18+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { - "name": "doctrine/annotations", - "version": "1.12.1", + "name": "evenement/evenement", + "version": "v3.0.2", "source": { "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", - "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0" + "php": ">=7.0" }, "require-dev": { - "doctrine/cache": "1.*", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^9.1.5" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -178,66 +310,49 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" } ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "description": "Événement is a very simple event dispatching library for PHP", "keywords": [ - "annotations", - "docblock", - "parser" + "event-dispatcher", + "event-emitter" ], - "time": "2021-02-21T21:00:45+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { - "name": "doctrine/lexer", - "version": "1.2.1", + "name": "fidry/cpu-core-counter", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -246,97 +361,80 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" } ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" + "CPU", + "core" ], "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" + "url": "https://github.com/theofidry", + "type": "github" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2024-02-07T09:43:46+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.18.4", + "version": "v3.62.0", "source": { "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "06f764e3cb6d60822d8f5135205f9d32b5508a31" + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "627692f794d35c43483f34b01d94740df2a73507" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/06f764e3cb6d60822d8f5135205f9d32b5508a31", - "reference": "06f764e3cb6d60822d8f5135205f9d32b5508a31", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/627692f794d35c43483f34b01d94740df2a73507", + "reference": "627692f794d35c43483f34b01d94740df2a73507", "shasum": "" }, "require": { - "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^1.2", - "doctrine/annotations": "^1.2", + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.6 || ^7.0 || ^8.0", - "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", - "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", - "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", - "symfony/finder": "^3.0 || ^4.0 || ^5.0", - "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0", - "symfony/polyfill-php70": "^1.0", - "symfony/polyfill-php72": "^1.4", - "symfony/process": "^3.0 || ^4.0 || ^5.0", - "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" + "fidry/cpu-core-counter": "^1.0", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.4", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.4.2", - "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5", - "phpunitgoodpractices/polyfill": "^1.5", - "phpunitgoodpractices/traits": "^1.9.1", - "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", - "symfony/phpunit-bridge": "^5.2.1", - "symfony/yaml": "^3.0 || ^4.0 || ^5.0" + "facile-it/paraunit": "^1.3 || ^2.3", + "infection/infection": "^0.29.5", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "suggest": { "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters.", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", - "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." + "ext-mbstring": "For handling non-UTF8 characters." }, "bin": [ "php-cs-fixer" @@ -346,18 +444,8 @@ "psr-4": { "PhpCsFixer\\": "src/" }, - "classmap": [ - "tests/Test/AbstractFixerTestCase.php", - "tests/Test/AbstractIntegrationCaseFactory.php", - "tests/Test/AbstractIntegrationTestCase.php", - "tests/Test/Assert/AssertTokensTrait.php", - "tests/Test/IntegrationCase.php", - "tests/Test/IntegrationCaseFactory.php", - "tests/Test/IntegrationCaseFactoryInterface.php", - "tests/Test/InternalIntegrationCaseFactory.php", - "tests/Test/IsIdenticalConstraint.php", - "tests/Test/TokensWithObservedTransformers.php", - "tests/TestCase.php" + "exclude-from-classmap": [ + "src/Fixer/Internal/*" ] }, "notification-url": "https://packagist.org/downloads/", @@ -375,83 +463,43 @@ } ], "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], "funding": [ { "url": "https://github.com/keradus", "type": "github" } ], - "time": "2021-03-20T14:52:33+00:00" - }, - { - "name": "php-cs-fixer/diff", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759", - "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "SpacePossum" - } - ], - "description": "sebastian/diff v2 backport support for PHP5.6", - "homepage": "https://github.com/PHP-CS-Fixer", - "keywords": [ - "diff" - ], - "time": "2020-10-14T08:39:05+00:00" + "time": "2024-08-07T17:03:09+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -476,7 +524,7 @@ "container-interop", "psr" ], - "time": "2021-03-05T17:36:06+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/event-dispatcher", @@ -526,30 +574,30 @@ }, { "name": "psr/log", - "version": "1.1.3", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "79dff0b268932c640297f5208d6298f71855c03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -559,7 +607,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -569,54 +617,615 @@ "psr", "psr-3" ], - "time": "2020-03-23T09:12:05+00:00" + "time": "2024-08-21T13:31:24+00:00" }, { - "name": "symfony/console", - "version": "v5.2.6", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2", - "symfony/string": "^5.1" - }, - "conflict": { - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-09-16T13:41:56+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", + "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -645,7 +1254,7 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], @@ -663,29 +1272,29 @@ "type": "tidelift" } ], - "time": "2021-03-28T09:42:18+00:00" + "time": "2024-07-26T12:30:32+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -727,48 +1336,43 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.2.4", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d08d6ec121a425897951900ab692b612a61d6240" + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", - "reference": "d08d6ec121a425897951900ab692b612a61d6240", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/event-dispatcher-contracts": "^2", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/error-handler": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/service-contracts": "^1.1|^2", - "symfony/stopwatch": "^4.4|^5.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -809,33 +1413,30 @@ "type": "tidelift" } ], - "time": "2021-02-18T17:12:37+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.2.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ba7d54483095a198fa51781bc608d17e84dffa2", - "reference": "0ba7d54483095a198fa51781bc608d17e84dffa2", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -885,25 +1486,29 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/filesystem", - "version": "v5.2.6", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "8c86a82f51658188119e62cff0a050a12d09836f" + "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c86a82f51658188119e62cff0a050a12d09836f", - "reference": "8c86a82f51658188119e62cff0a050a12d09836f", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", + "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8" + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" }, "type": "library", "autoload": { @@ -944,24 +1549,27 @@ "type": "tidelift" } ], - "time": "2021-03-28T14:30:26+00:00" + "time": "2024-06-28T09:49:33+00:00" }, { "name": "symfony/finder", - "version": "v5.2.4", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "reference": "af29198d87112bebdd397bd7735fbd115997824c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", + "reference": "af29198d87112bebdd397bd7735fbd115997824c", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", "autoload": { @@ -1002,27 +1610,25 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2024-07-24T07:06:38+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.2.4", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce" + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", - "reference": "5d0f633f9bbfcf7ec642a2b5037268e61b0a62ce", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -1068,45 +1674,45 @@ "type": "tidelift" } ], - "time": "2021-01-27T12:56:27+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-ctype": "*" + }, "suggest": { "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1144,20 +1750,20 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.22.1", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", - "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -1168,21 +1774,18 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1222,20 +1825,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.22.1", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -1246,21 +1849,18 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -1303,45 +1903,45 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.22.1", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1380,234 +1980,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.20.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", - "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "metapackage", - "extra": { - "branch-alias": { - "dev-main": "1.20-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "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 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "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": "2020-10-23T14:02:19+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.22.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", - "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "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": "2021-01-07T16:49:33+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.22.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "files": [ - "bootstrap.php" - ], - "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 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "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": "2021-01-07T16:49:33+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -1615,21 +2001,18 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -1674,25 +2057,97 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { - "name": "symfony/process", - "version": "v5.2.4", + "name": "symfony/polyfill-php81", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", + "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "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-06-19T12:30:46+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "shasum": "" + }, + "require": { + "php": ">=8.1" }, "type": "library", "autoload": { @@ -1733,33 +2188,34 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.0" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "suggest": { - "symfony/service-implementation": "" + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1769,7 +2225,10 @@ "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - } + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1809,25 +2268,25 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.2.4", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c" + "reference": "63e069eb616049632cde9674c46957819454b8aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b12274acfab9d9850c52583d136a24398cdf1a0c", - "reference": "b12274acfab9d9850c52583d136a24398cdf1a0c", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/63e069eb616049632cde9674c46957819454b8aa", + "reference": "63e069eb616049632cde9674c46957819454b8aa", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1.0|^2" + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { @@ -1868,44 +2327,47 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/string", - "version": "v5.2.6", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + "reference": "ccf9b30251719567bfd46494138327522b9a9446" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", + "reference": "ccf9b30251719567bfd46494138327522b9a9446", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -1948,7 +2410,7 @@ "type": "tidelift" } ], - "time": "2021-03-17T17:12:15+00:00" + "time": "2024-07-22T10:21:14+00:00" } ], "packages-dev": [], diff --git a/bin/dev/vagrant_provision.sh b/bin/dev/vagrant_provision.sh index 40d08cedb2..7078b84d64 100755 --- a/bin/dev/vagrant_provision.sh +++ b/bin/dev/vagrant_provision.sh @@ -1,4 +1,8 @@ #!/bin/bash +# SPDX-FileCopyrightText: 2010-2024 the Friendica project +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# # Script to setup the vagrant instance for running friendica # # DO NOT RUN on your physical machine as this won't be of any use @@ -38,6 +42,18 @@ openssl genrsa -out "$SSL_DIR/xip.io.key" 4096 openssl req -new -subj "$(echo -n "$SUBJ" | tr "\n" "/")" -key "$SSL_DIR/xip.io.key" -out "$SSL_DIR/xip.io.csr" -passin pass:$PASSPHRASE openssl x509 -req -days 365 -in "$SSL_DIR/xip.io.csr" -signkey "$SSL_DIR/xip.io.key" -out "$SSL_DIR/xip.io.crt" +#Install php +echo ">>> Add PHP repository" +apt-get install -qq -y lsb-release ca-certificates apt-transport-https software-properties-common gnupg +echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/sury-php.list +wget -qO - https://packages.sury.org/php/apt.gpg | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/php.gpg +apt update + +echo ">>> Installing PHP8" +apt-get install -qq php libapache2-mod-php php8.3-cli php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring php8.3-xml imagemagick php8.3-imagick php8.3-zip php8.3-gmp php8.3-intl + +echo ">>> Installing PHP7" +apt-get install -qq php7.4 php7.4-cli php7.4-mysql php7.4-curl php7.4-gd php7.4-mbstring php7.4-xml php7.4-imagick php7.4-zip php7.4-gmp php7.4-intl #Install apache2 echo ">>> Installing Apache2 webserver" @@ -49,19 +65,6 @@ vhost -s 192.168.56.10.xip.io -d /var/www -p /etc/ssl/xip.io -c xip.io -a friend a2dissite 000-default service apache2 restart -#Install php -echo ">>> Installing PHP7" -apt-get install -qq php libapache2-mod-php php-cli php-mysql php-curl php-gd php-mbstring php-xml imagemagick php-imagick php-zip php-gmp -systemctl restart apache2 - -echo ">>> Installing PHP8" -apt-get install -qq -y lsb-release ca-certificates apt-transport-https software-properties-common gnupg -echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/sury-php.list -wget -qO - https://packages.sury.org/php/apt.gpg | sudo apt-key add - -apt update -apt-get install -qq php8.0 php8.0-cli php8.0-mysql php8.0-curl php8.0-gd php8.0-mbstring php8.0-xml php8.0-imagick php8.0-zip php8.0-gmp -systemctl restart apache2 - #Install mysql echo ">>> Installing Mysql" debconf-set-selections <<< "mariadb-server mariadb-server/root_password password root" @@ -126,7 +129,7 @@ bin/console user password "$USER_NICK" "$USER_PASSW" # create cronjob - activate if you have enough memory in you dev VM # cronjob runs as www-data user echo ">>> Installing cronjob" -echo "*/10 * * * * www-data cd /vagrant; /usr/bin/php bin/worker.php" >> /etc/cron.d/friendica +echo "*/10 * * * * www-data cd /vagrant; /usr/bin/php bin/console.php worker" >> /etc/cron.d/friendica # friendica needs write access to /tmp chmod 777 /tmp diff --git a/bin/dev/vagrant_vhost.sh b/bin/dev/vagrant_vhost.sh index f26d8e14d7..bef5c0ed23 100755 --- a/bin/dev/vagrant_vhost.sh +++ b/bin/dev/vagrant_vhost.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +# SPDX-FileCopyrightText: 2010-2024 the Friendica project +# +# SPDX-License-Identifier: AGPL-3.0-or-later + # Run this as sudo! # I move this file to /usr/local/bin/vhost and run command 'vhost' from anywhere, using sudo. @@ -174,4 +178,4 @@ else # Enable Site cd /etc/apache2/sites-available/ && a2ensite ${ServerName}.conf service apache2 reload -fi \ No newline at end of file +fi diff --git a/bin/jetstream.php b/bin/jetstream.php new file mode 100755 index 0000000000..b5ed177a59 --- /dev/null +++ b/bin/jetstream.php @@ -0,0 +1,33 @@ +#!/usr/bin/env php +processConsole($_SERVER); diff --git a/bin/run_xgettext.sh b/bin/run_xgettext.sh index 0768ed4f14..ec5cb955d0 100755 --- a/bin/run_xgettext.sh +++ b/bin/run_xgettext.sh @@ -1,8 +1,20 @@ -#!/bin/bash -set -eo pipefail +#!/bin/sh -function resolve { - if [ "$(uname)" == "Darwin" ] +# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + +set -e + +# Custom function to handle pipefail behavior +pipefail() { + local cmd="$1" + shift + { eval "$cmd"; } || exit 1 +} + +resolve() { + if [ "$(uname)" = "Darwin" ] then realpath "$1" else @@ -12,26 +24,26 @@ function resolve { FULLPATH=$(dirname "$(resolve "$0")") -if [ "$1" == "--help" ] || [ "$1" == "-h" ] +if [ "$1" = "--help" ] || [ "$1" = "-h" ] then echo "$(basename "$(resolve "$0")") [options]" echo echo "-a | --addon extract strings from addon 'name'" - echo "-s | --single single addon mode: extract string from current folder" + echo "-s | --single single addon mode: extract string from current folder" exit fi MODE='default' ADDONNAME= -if [ "$1" == "--addon" ] || [ "$1" == "-a" ] +if [ "$1" = "--addon" ] || [ "$1" = "-a" ] then MODE='addon' - if [ -z "$2" ]; then echo -e "ERROR: missing addon name\n\nrun_xgettext.sh -a "; exit 1; fi + if [ -z "$2" ]; then echo "ERROR: missing addon name\n\nrun_xgettext.sh -a "; exit 1; fi ADDONNAME=$2 if [ ! -d "$FULLPATH/../addon/$ADDONNAME" ]; then echo "ERROR: addon '$ADDONNAME' not found"; exit 2; fi fi -if [ "$1" == "--single" ] || [ "$1" == "-s" ] +if [ "$1" = "--single" ] || [ "$1" = "-s" ] then MODE='single' fi @@ -65,33 +77,31 @@ case "$MODE" in ;; esac - KEYWORDS="-k -kt -ktt:1,2" echo "Extract strings to $OUTFILE.." [ -f "$OUTFILE" ] && rm "$OUTFILE"; touch "$OUTFILE" # shellcheck disable=SC2086 # $FINDOPTS is meant to be split -find_result=$(find "$FINDSTARTDIR" $FINDOPTS -name "*.php" -type f | LC_ALL=C sort --stable) +find_result=$(find "$FINDSTARTDIR" $FINDOPTS -name "*.php" -type f | LC_ALL=C sort -s) -total_files=$(wc -l <<< "${find_result}") +total_files=$(echo "${find_result}" | wc -l) count=1 for file in $find_result do - echo -ne " \r" - echo -ne "Reading file $count/$total_files..." + printf " \r" + printf "Reading file %d/%d..." "$count" "$total_files" # On Windows, find still outputs the name of pruned folders if [ ! -d "$file" ] then # shellcheck disable=SC2086 # $KEYWORDS is meant to be split - xgettext $KEYWORDS -j -o "$OUTFILE" --from-code=UTF-8 "$file" || exit 1 + xgettext $KEYWORDS --no-wrap -j -o "$OUTFILE" --from-code=UTF-8 "$file" || exit 1 sed -i.bkp "s/CHARSET/UTF-8/g" "$OUTFILE" fi - (( count++ )) + count=$((count + 1)) done -echo -ne "\n" echo "Interpolate metadata.." @@ -114,7 +124,7 @@ case "$MODE" in ;; esac -if [ "" != "$1" ] && [ "$MODE" == "default" ] +if [ -n "$1" ] && [ "$MODE" = "default" ] then UPDATEFILE="$(resolve "${FULLPATH}/$1")" echo "Merging new strings to $UPDATEFILE.." diff --git a/bin/testargs.php b/bin/testargs.php index 0d736bcc26..22db41c3ad 100644 --- a/bin/testargs.php +++ b/bin/testargs.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * During installation we need to check if register_argc_argv is * enabled for the command line PHP processor, because otherwise diff --git a/bin/wait-for-connection b/bin/wait-for-connection index 6b3dbc674b..66796249f1 100755 --- a/bin/wait-for-connection +++ b/bin/wait-for-connection @@ -1,25 +1,10 @@ #!/usr/bin/php . - * - * This script tries to connect to a database for a given interval - * Useful in case of installation e.g. to wait for the database to not generate unnecessary errors + * SPDX-License-Identifier: AGPL-3.0-or-later * * Usage: php bin/wait-for-connection {HOST} {PORT} [{TIMEOUT}] */ diff --git a/bin/worker.php b/bin/worker.php index c855f9bc55..cd3d035599 100755 --- a/bin/worker.php +++ b/bin/worker.php @@ -1,24 +1,14 @@ #!/usr/bin/env php . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Starts the background processing + * + * @deprecated 2025.02 use `bin/console.php worker` instead */ if (php_sapi_name() !== 'cli') { @@ -26,66 +16,20 @@ if (php_sapi_name() !== 'cli') { exit(); } -use Dice\Dice; -use Friendica\App; -use Friendica\App\Mode; -use Friendica\Core\Logger\Capability\LogChannel; -use Friendica\Core\Update; -use Friendica\Core\Worker; -use Friendica\DI; -use Psr\Log\LoggerInterface; - -// Get options -$shortopts = 'sn'; -$longopts = ['spawn', 'no_cron']; -$options = getopt($shortopts, $longopts); - // Ensure that worker.php is executed from the base path of the installation -if (!file_exists("index.php") && (sizeof($_SERVER["argv"]) != 0)) { - $directory = dirname($_SERVER["argv"][0]); - - if (substr($directory, 0, 1) != '/') { - $directory = $_SERVER["PWD"] . '/' . $directory; - } - $directory = realpath($directory . '/..'); - - chdir($directory); -} +chdir(dirname(__DIR__)); require dirname(__DIR__) . '/vendor/autoload.php'; -$dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); -/** @var \Friendica\Core\Addon\Capability\ICanLoadAddons $addonLoader */ -$addonLoader = $dice->create(\Friendica\Core\Addon\Capability\ICanLoadAddons::class); -$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies')); -$dice = $dice->addRule(LoggerInterface::class, ['constructParams' => [LogChannel::WORKER]]); +fwrite(STDOUT, '`bin/worker.php` is deprecated since 2025.02 and will be removed in 5 months, please use `bin/console.php worker` instead.' . \PHP_EOL); -DI::init($dice); -\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); +// BC: Add console command as second argument +$argv = $_SERVER['argv'] ?? []; +array_splice($argv, 1, 0, "worker"); +$_SERVER['argv'] = $argv; -DI::mode()->setExecutor(Mode::WORKER); +$container = \Friendica\Core\DiceContainer::fromBasePath(dirname(__DIR__)); -// Check the database structure and possibly fixes it -Update::check(DI::basePath(), true); +$app = \Friendica\App::fromContainer($container); -// Quit when in maintenance -if (!DI::mode()->has(App\Mode::MAINTENANCEDISABLED)) { - return; -} - -$spawn = array_key_exists('s', $options) || array_key_exists('spawn', $options); - -if ($spawn) { - Worker::spawnWorker(); - exit(); -} - -$run_cron = !array_key_exists('n', $options) && !array_key_exists('no_cron', $options); - -$process = DI::process()->create(getmypid(), basename(__FILE__)); - -Worker::processQueue($run_cron, $process); - -Worker::unclaimProcess($process); - -DI::process()->delete($process); +$app->processConsole($_SERVER); diff --git a/composer.json b/composer.json index 21603c7b27..9973336590 100644 --- a/composer.json +++ b/composer.json @@ -29,13 +29,14 @@ "ext-xml": "*", "asika/simple-console": "^1.0", "bacon/bacon-qr-code": "^2.0.0", - "divineomega/password_exposed": "^2.8", + "bower-asset/base64": "^1.0", + "divineomega/password_exposed": "^3", "enyo/dropzone": "^5.9", "ezyang/htmlpurifier": "^4.7", "friendica/json-ld": "^1.0", "geekwright/po": "^2.0", - "guzzlehttp/guzzle": "^6.5", - "guzzlehttp/oauth-subscriber": "^0.6", + "guzzlehttp/guzzle": "^7", + "guzzlehttp/oauth-subscriber": "^0.8", "kornrunner/blurhash": "^1.2", "league/html-to-markdown": "^4.8", "level-2/dice": "^4", @@ -46,36 +47,38 @@ "minishlink/web-push": "^6.0", "mobiledetect/mobiledetectlib": "^3.74", "nikic/fast-route": "^1.3", + "npm-asset/chart.js": "^2.8", + "npm-asset/cropperjs": "1.2.2", + "npm-asset/dompurify": "^1.0", + "npm-asset/es-jquery-sortable": "^0.9.13", + "npm-asset/fork-awesome": "^1.1", + "npm-asset/fullcalendar": "^3.10", + "npm-asset/imagesloaded": "4.1.4", + "npm-asset/jgrowl": "^1.4", + "npm-asset/jquery": "^2.0", + "npm-asset/jquery-colorbox": "^1.6", + "npm-asset/jquery-datetimepicker": "^2.5", + "npm-asset/moment": "^2.24", + "npm-asset/perfect-scrollbar": "0.6.16", + "npm-asset/textcomplete": "^0.18.2", + "npm-asset/typeahead.js": "^0.11.1", + "oomphinc/composer-installers-extender": "^2.0", "paragonie/hidden-string": "^1.0", "patrickschur/language-detection": "^5.0.0", "pear/console_table": "^1.3", "phpseclib/phpseclib": "^3.0", + "phrity/websocket": "^1.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", "smarty/smarty": "^4", + "symfony/event-dispatcher": "^5.4", "ua-parser/uap-php": "^3.9", - "xemlock/htmlpurifier-html5": "^0.1.11", - "fxp/composer-asset-plugin": "^1.4", - "bower-asset/base64": "^1.0", - "bower-asset/chart-js": "^2.8", - "bower-asset/dompurify": "^1.0", - "bower-asset/fork-awesome": "^1.1", - "npm-asset/cropperjs": "1.2.2", - "npm-asset/es-jquery-sortable": "^0.9.13", - "npm-asset/fullcalendar": "^3.10", - "npm-asset/imagesloaded": "4.1.4", - "npm-asset/jquery": "^2.0", - "npm-asset/jquery-colorbox": "^1.6", - "npm-asset/jquery-datetimepicker": "^2.5", - "npm-asset/jgrowl": "^1.4", - "npm-asset/moment": "^2.24", - "npm-asset/perfect-scrollbar": "0.6.16", - "npm-asset/textcomplete": "^0.18.2", - "npm-asset/typeahead.js": "^0.11.1" + "xemlock/htmlpurifier-html5": "^0.1.11" }, "suggest": { "ext-imagick": "For faster image processing", @@ -87,13 +90,20 @@ { "type": "vcs", "url": "https://git.friendi.ca/friendica/php-json-ld" + }, + { + "type": "composer", + "url": "https://asset-packagist.org" } ], "autoload": { "psr-4": { - "Friendica\\": "src/", - "Friendica\\Addon\\": "addon/" - } + "Friendica\\": "src/" + }, + "classmap": ["addon/"], + "exclude-from-classmap": [ + "addon/*/vendor/" + ] }, "autoload-dev": { "psr-4": { @@ -104,14 +114,22 @@ "platform": { "php": "7.4" }, + "sort-packages": true, "autoloader-suffix": "Friendica", - "optimize-autoloader": true, "preferred-install": "dist", - "fxp-asset": { - "installer-paths": { - "npm-asset-library": "view/asset", - "bower-asset-library": "view/asset" - } + "allow-plugins": { + "composer/installers": true, + "oomphinc/composer-installers-extender": true, + "php-http/discovery": false + } + }, + "extra": { + "installer-types": ["bower-asset", "npm-asset"], + "installer-paths": { + "view/asset/{$name}/": [ + "type:bower-asset", + "type:npm-asset" + ] } }, "archive": { @@ -132,23 +150,35 @@ ] }, "require-dev": { - "mockery/mockery": "^1.3", + "dms/phpunit-arraysubset-asserts": "^0.3.1", "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^9", - "dms/phpunit-arraysubset-asserts": "^0.3.1" + "mockery/mockery": "^1.3", + "php-mock/php-mock-mockery": "^1.5", + "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", + "phpstan-addons": "phpstan analyze --memory-limit 1024M --configuration .phpstan-addons.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", + "lang:recreate": "bin/run_xgettext.sh", "cs:install": "@composer install --working-dir=bin/dev/php-cs-fixer", "cs:check": [ "@cs:install", - "bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff" + "bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --diff" ], "cs:fix": [ "@cs:install", "bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer fix" - ] + ], + "cs:fix-develop": "TARGET_BRANCH=develop COMMAND=fix bin/dev/fix-codestyle.sh", + "db:update-structure": "bin/console.php dbstructure dumpsql > database.sql", + "install:prod": "@composer install -o --no-dev" } } diff --git a/composer.lock b/composer.lock index a541913811..fb9fadddbd 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": "082b16e2c88895f1a03d5b0ffe678ba7", + "content-hash": "897b878d6db24b9a6437bd9f971478be", "packages": [ { "name": "asika/simple-console", @@ -89,38 +89,6 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "time": "2022-12-07T17:46:57+00:00" }, - { - "name": "bower-asset/Chart-js", - "version": "v2.9.4", - "source": { - "type": "git", - "url": "https://github.com/chartjs/Chart.js.git", - "reference": "9bd4cf82fda9f50a5fb50b72843e06ab88124278" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/chartjs/Chart.js/zipball/9bd4cf82fda9f50a5fb50b72843e06ab88124278", - "reference": "9bd4cf82fda9f50a5fb50b72843e06ab88124278", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "./dist/Chart.js", - "bower-asset-ignore": [ - ".github", - ".codeclimate.yml", - ".gitignore", - ".npmignore", - ".travis.yml", - "scripts" - ] - }, - "license": [ - "MIT" - ], - "description": "Simple HTML5 charts using the canvas element.", - "time": "2020-10-19T12:22:11+00:00" - }, { "name": "bower-asset/base64", "version": "1.3.0", @@ -132,114 +100,12 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/davidchambers/Base64.js/zipball/22192690552ba07bf035f95a5d2d1a0e2f0ced46", - "reference": "22192690552ba07bf035f95a5d2d1a0e2f0ced46", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "./base64.js", - "bower-asset-ignore": [ - "**/.*", - "Makefile", - "coverage/", - "scripts/", - "test/" - ] + "reference": "22192690552ba07bf035f95a5d2d1a0e2f0ced46" }, + "type": "bower-asset", "license": [ "WTFPL" - ], - "description": "Base64 encoding and decoding", - "time": "2023-09-18T21:37:26+00:00" - }, - { - "name": "bower-asset/dompurify", - "version": "1.0.11", - "source": { - "type": "git", - "url": "https://github.com/cure53/DOMPurify.git", - "reference": "3c1c0d7e11cda896b0c69cf82e0ca6e0c0e7dd38" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cure53/DOMPurify/zipball/3c1c0d7e11cda896b0c69cf82e0ca6e0c0e7dd38", - "reference": "3c1c0d7e11cda896b0c69cf82e0ca6e0c0e7dd38", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "src/purify.js", - "bower-asset-ignore": [ - "**/.*", - "demos", - "scripts", - "test", - "website" - ] - }, - "license": [ - "MPL-2.0", - "Apache-2.0" - ], - "description": "A DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG", - "keywords": [ - "cross site scripting", - "dom", - "filter", - "html", - "mathml", - "sanitize", - "sanitizer", - "secure", - "security", - "svg", - "xss" - ], - "time": "2019-06-18T13:33:05+00:00" - }, - { - "name": "bower-asset/fork-awesome", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/ForkAwesome/Fork-Awesome.git", - "reference": "1e3849530d0266ece3a883649e1398414b92241d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ForkAwesome/Fork-Awesome/zipball/1e3849530d0266ece3a883649e1398414b92241d", - "reference": "1e3849530d0266ece3a883649e1398414b92241d", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": [ - "less/fork-awesome.less", - "scss/fork-awesome.scss" - ], - "bower-asset-ignore": [ - "*/.*", - "*.json", - "src", - "*.yml", - "Gemfile", - "Gemfile.lock", - "*.md" - ] - }, - "license": [ - "OFL-1.1", - "MIT", - "CC-BY-3.0" - ], - "description": "Fork Awesome", - "keywords": [ - "awesome", - "font", - "fork", - "icon" - ], - "time": "2021-08-26T18:46:39+00:00" + ] }, { "name": "brick/math", @@ -299,28 +165,28 @@ }, { "name": "composer/ca-bundle", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "b66d11b7479109ab547f9405b97205640b17d385" + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", - "reference": "b66d11b7479109ab547f9405b97205640b17d385", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan": "^1.10", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -366,7 +232,153 @@ "type": "tidelift" } ], - "time": "2023-12-18T12:05:55+00:00" + "time": "2024-03-15T14:00:32+00:00" + }, + { + "name": "composer/installers", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "12fb2dfe5e16183de69e784a7b84046c43d97e8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/12fb2dfe5e16183de69e784a7b84046c43d97e8e", + "reference": "12fb2dfe5e16183de69e784a7b84046c43d97e8e", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "composer/composer": "^1.10.27 || ^2.7", + "composer/semver": "^1.7.2 || ^3.4.0", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-phpunit": "^1", + "symfony/phpunit-bridge": "^7.1.1", + "symfony/process": "^5 || ^6 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-main": "2.x-dev" + }, + "plugin-modifies-install-path": true + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Starbug", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "concreteCMS", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "matomo", + "mediawiki", + "miaoxing", + "modulework", + "modx", + "moodle", + "osclass", + "pantheon", + "phpbb", + "piwik", + "ppi", + "processwire", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "tastyigniter", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v2.3.0" + }, + "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-06-24T20:46:46+00:00" }, { "name": "dasprid/enum", @@ -507,38 +519,101 @@ }, { "name": "divineomega/password_exposed", - "version": "v2.8.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/DivineOmega/password_exposed.git", - "reference": "908ed8e62ef95411bd0f866e29c69cef2bbca880" + "reference": "327f93ee5cab54622077bcae721412b55be16720" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DivineOmega/password_exposed/zipball/908ed8e62ef95411bd0f866e29c69cef2bbca880", - "reference": "908ed8e62ef95411bd0f866e29c69cef2bbca880", + "url": "https://api.github.com/repos/DivineOmega/password_exposed/zipball/327f93ee5cab54622077bcae721412b55be16720", + "reference": "327f93ee5cab54622077bcae721412b55be16720", "shasum": "" }, "require": { "divineomega/do-file-cache-psr-6": "^2.0", - "guzzlehttp/guzzle": "^6.3", - "paragonie/certainty": "^1|^2", - "php": ">=5.6" + "divineomega/psr-18-guzzle-adapter": "^1.0", + "nyholm/psr7": "^1.0", + "paragonie/certainty": "^2.4", + "php": "^7.1||^8.0", + "php-http/discovery": "^1.6", + "psr/cache": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0", + "psr/http-message-implementation": "^1.0" }, "require-dev": { "fzaninotto/faker": "^1.7", + "kriswallsmith/buzz": "^1.0", "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^6.5", - "vimeo/psalm": "^1" + "phpunit/phpunit": "^7.0||^8.0", + "symfony/cache": "^4.2.12", + "vimeo/psalm": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/PasswordExposedFunction.php" + ], + "psr-4": { + "DivineOmega\\PasswordExposed\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Jordan Hall", + "email": "jordan@hall05.co.uk" + }, + { + "name": "Contributors", + "homepage": "https://github.com/DivineOmega/password_exposed/graphs/contributors" + } + ], + "description": "This PHP package provides a `password_exposed` helper function, that uses the haveibeenpwned.com API to check if a password has been exposed in a data breach.", + "homepage": "https://github.com/DivineOmega/password_exposed", + "funding": [ + { + "url": "https://github.com/DivineOmega", + "type": "github" + } + ], + "time": "2021-04-20T09:34:23+00:00" + }, + { + "name": "divineomega/psr-18-guzzle-adapter", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/DivineOmega/psr-18-guzzle-adapter.git", + "reference": "a2bdcddd4d4a17aac460e58d1e064e6bd2de5e57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DivineOmega/psr-18-guzzle-adapter/zipball/a2bdcddd4d4a17aac460e58d1e064e6bd2de5e57", + "reference": "a2bdcddd4d4a17aac460e58d1e064e6bd2de5e57", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.3||^7.0", + "php": "^7.1||^8.0", + "psr/http-client": "^1.0" }, "type": "library", "autoload": { "psr-4": { - "DivineOmega\\PasswordExposed\\": "src/" - }, - "files": [ - "src/PasswordExposedFunction.php" - ] + "DivineOmega\\Psr18GuzzleAdapter\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -550,8 +625,14 @@ "email": "jordan@hall05.co.uk" } ], - "description": "This PHP package provides a `password_exposed` helper function, that uses the haveibeenpwned.com API to check if a password has been exposed in a data breach.", - "time": "2019-01-25T12:00:28+00:00" + "description": "PSR-18 adapter for the Guzzle HTTP client", + "funding": [ + { + "url": "https://github.com/DivineOmega", + "type": "github" + } + ], + "time": "2021-04-20T08:50:57+00:00" }, { "name": "enyo/dropzone", @@ -762,65 +843,6 @@ ], "time": "2023-07-09T14:00:15+00:00" }, - { - "name": "fxp/composer-asset-plugin", - "version": "v1.4.6", - "source": { - "type": "git", - "url": "https://github.com/fxpio/composer-asset-plugin.git", - "reference": "886ece037849d3935c5a34cdcd984e46f2de5fae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fxpio/composer-asset-plugin/zipball/886ece037849d3935c5a34cdcd984e46f2de5fae", - "reference": "886ece037849d3935c5a34cdcd984e46f2de5fae", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0", - "php": ">=5.3.3" - }, - "require-dev": { - "composer/composer": "^1.6.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Fxp\\Composer\\AssetPlugin\\FxpAssetPlugin", - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "Fxp\\Composer\\AssetPlugin\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Pluchino", - "email": "francois.pluchino@gmail.com" - } - ], - "description": "NPM/Bower Dependency Manager for Composer", - "homepage": "https://github.com/fxpio/composer-asset-plugin", - "keywords": [ - "asset", - "bower", - "composer", - "dependency manager", - "nodejs", - "npm", - "package" - ], - "time": "2019-08-08T18:36:07+00:00" - }, { "name": "geekwright/po", "version": "v2.0.2", @@ -871,37 +893,47 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.8", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", - "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.9", - "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.17" + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.1" + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "6.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -954,16 +986,21 @@ } ], "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", "keywords": [ "client", "curl", "framework", "http", "http client", + "psr-18", + "psr-7", "rest", "web service" ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -978,37 +1015,39 @@ "type": "tidelift" } ], - "time": "2022-06-20T22:16:07+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/oauth-subscriber", - "version": "0.6.0", + "version": "0.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/oauth-subscriber.git", - "reference": "8d6cab29f8397e5712d00a383eeead36108a3c1f" + "reference": "92b619b03bd21396e51c62e6bce83467d2ce8f53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/oauth-subscriber/zipball/8d6cab29f8397e5712d00a383eeead36108a3c1f", - "reference": "8d6cab29f8397e5712d00a383eeead36108a3c1f", + "url": "https://api.github.com/repos/guzzle/oauth-subscriber/zipball/92b619b03bd21396e51c62e6bce83467d2ce8f53", + "reference": "92b619b03bd21396e51c62e6bce83467d2ce8f53", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^6.5|^7.2", - "guzzlehttp/psr7": "^1.7|^2.0", - "php": ">=5.5.0" + "guzzlehttp/guzzle": "^7.9", + "guzzlehttp/psr7": "^2.7", + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "~4.0|^9.3.3" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "suggest": { "ext-openssl": "Required to sign using RSA-SHA1" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "0.6-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { @@ -1021,45 +1060,81 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" } ], "description": "Guzzle OAuth 1.0 subscriber", - "homepage": "http://guzzlephp.org/", "keywords": [ "Guzzle", "oauth" ], - "time": "2021-07-13T12:01:32+00:00" + "support": { + "issues": "https://github.com/guzzle/oauth-subscriber/issues", + "source": "https://github.com/guzzle/oauth-subscriber/tree/0.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/oauth-subscriber", + "type": "tidelift" + } + ], + "time": "2025-01-06T19:15:59+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.3", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", - "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -1094,6 +1169,10 @@ "keywords": [ "promise" ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -1108,42 +1187,48 @@ "type": "tidelift" } ], - "time": "2023-05-21T12:31:43+00:00" + "time": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.9.1", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b", - "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { - "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Psr7\\": "src/" } @@ -1182,6 +1267,11 @@ "name": "Tobias Schultze", "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], "description": "PSR-7 message implementation that also provides common utility methods", @@ -1195,6 +1285,10 @@ "uri", "url" ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -1209,7 +1303,7 @@ "type": "tidelift" } ], - "time": "2023-04-17T16:00:37+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "kornrunner/blurhash", @@ -1317,6 +1411,24 @@ "html", "markdown" ], + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + } + ], "time": "2020-07-01T00:34:03+00:00" }, { @@ -1403,16 +1515,16 @@ }, { "name": "matriphe/iso-639", - "version": "1.2", + "version": "1.3", "source": { "type": "git", "url": "https://github.com/matriphe/php-iso-639.git", - "reference": "0245d844daeefdd22a54b47103ffdb0e03c323e1" + "reference": "9a4a5823147890e70e0e0f60f3baea95e8d3b5f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matriphe/php-iso-639/zipball/0245d844daeefdd22a54b47103ffdb0e03c323e1", - "reference": "0245d844daeefdd22a54b47103ffdb0e03c323e1", + "url": "https://api.github.com/repos/matriphe/php-iso-639/zipball/9a4a5823147890e70e0e0f60f3baea95e8d3b5f1", + "reference": "9a4a5823147890e70e0e0f60f3baea95e8d3b5f1", "shasum": "" }, "require-dev": { @@ -1443,7 +1555,7 @@ "language", "laravel" ], - "time": "2017-07-19T15:11:19+00:00" + "time": "2024-03-17T21:30:14+00:00" }, { "name": "mattwright/urlresolver", @@ -1705,980 +1817,466 @@ ], "time": "2018-02-13T20:26:39+00:00" }, + { + "name": "npm-asset/chart.js", + "version": "2.9.4", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz" + }, + "require": { + "npm-asset/chartjs-color": ">=2.1.0,<3.0.0", + "npm-asset/moment": ">=2.10.2,<3.0.0" + }, + "type": "npm-asset", + "license": [ + "MIT" + ] + }, + { + "name": "npm-asset/chartjs-color", + "version": "2.4.1", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz" + }, + "require": { + "npm-asset/chartjs-color-string": ">=0.6.0,<0.7.0", + "npm-asset/color-convert": ">=1.9.3,<2.0.0" + }, + "type": "npm-asset", + "license": [ + "MIT" + ] + }, + { + "name": "npm-asset/chartjs-color-string", + "version": "0.6.0", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz" + }, + "require": { + "npm-asset/color-name": ">=1.0.0,<2.0.0" + }, + "type": "npm-asset", + "license": [ + "MIT" + ] + }, + { + "name": "npm-asset/color-convert", + "version": "1.9.3", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + }, + "require": { + "npm-asset/color-name": "1.1.3" + }, + "type": "npm-asset", + "license": [ + "MIT" + ] + }, + { + "name": "npm-asset/color-name", + "version": "1.1.3", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + }, + "type": "npm-asset", + "license": [ + "MIT" + ] + }, { "name": "npm-asset/cropperjs", "version": "1.2.2", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.2.2.tgz", - "shasum": "30dc7a7ce872155b23a33bd10ad4c76c0d613f55" - }, - "require-dev": { - "npm-asset/babel-core": ">=6.26.0,<7.0.0", - "npm-asset/babel-plugin-external-helpers": ">=6.22.0,<7.0.0", - "npm-asset/babel-preset-env": ">=1.6.1,<2.0.0", - "npm-asset/cpy-cli": ">=1.0.1,<2.0.0", - "npm-asset/cssnano": ">=3.10.0,<4.0.0", - "npm-asset/del-cli": ">=1.1.0,<2.0.0", - "npm-asset/eslint": ">=4.14.0,<5.0.0", - "npm-asset/eslint-config-airbnb-base": ">=12.1.0,<13.0.0", - "npm-asset/eslint-plugin-import": ">=2.8.0,<3.0.0", - "npm-asset/node-qunit-phantomjs": ">=2.0.0,<3.0.0", - "npm-asset/npm-run-all": ">=4.1.2,<5.0.0", - "npm-asset/postcss-cli": ">=4.1.1,<5.0.0", - "npm-asset/postcss-cssnext": ">=3.0.2,<4.0.0", - "npm-asset/postcss-header": ">=1.0.0,<2.0.0", - "npm-asset/postcss-url": ">=7.3.0,<8.0.0", - "npm-asset/rollup": ">=0.53.3,<0.54.0", - "npm-asset/rollup-plugin-babel": ">=3.0.3,<4.0.0", - "npm-asset/rollup-watch": ">=4.3.1,<5.0.0", - "npm-asset/stylefmt": ">=6.0.0,<7.0.0", - "npm-asset/uglify-js": ">=3.3.4,<4.0.0" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/fengyuanchen/cropperjs/issues" - }, - "npm-asset-files": [ - "src", - "dist" - ], - "npm-asset-main": "dist/cropper.common.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/fengyuanchen/cropperjs.git" - }, - "npm-asset-scripts": { - "build": "npm run build:css && npm run build:js", - "build:css": "postcss src/css/cropper.css -o dist/cropper.css --no-map", - "build:js": "rollup -c", - "clear": "del-cli dist", - "compress": "npm run compress:css && npm run compress:js", - "compress:css": "postcss dist/cropper.css -u cssnano -o dist/cropper.min.css --no-map", - "compress:js": "uglifyjs dist/cropper.js -o dist/cropper.min.js -c -m --comments /^!/", - "copy": "cpy dist/cropper.css docs/css", - "lint": "eslint src/js --fix", - "release": "npm run clear && npm run lint && npm run build && npm run compress && npm run copy && npm test", - "start": "npm-run-all --parallel watch:*", - "test": "node-qunit-phantomjs test/index.html --timeout 10", - "watch:css": "postcss src/css/cropper.css -o docs/css/cropper.css -m -w", - "watch:js": "rollup -c -m -w" - } + "url": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.2.2.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Chen Fengyuan", - "url": "http://chenfengyuan.com" - } - ], - "description": "JavaScript image cropper.", - "homepage": "https://fengyuanchen.github.io/cropperjs", - "keywords": [ - "crop", - "cropper", - "cropper.js", - "cropperjs", - "cropping", - "css", - "development", - "front-end", - "html", - "image", - "javascript", - "move", - "rotate", - "scale", - "web", - "zoom" - ], - "time": "2018-01-03T13:39:39+00:00" + ] + }, + { + "name": "npm-asset/dompurify", + "version": "1.0.11", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/dompurify/-/dompurify-1.0.11.tgz" + }, + "type": "npm-asset", + "license": [ + "MPL-2.0 OR Apache-2.0" + ] }, { "name": "npm-asset/es-jquery-sortable", "version": "0.9.13-patch2", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/es-jquery-sortable/-/es-jquery-sortable-0.9.13-patch2.tgz", - "shasum": "a4db16d133fbce1bcd1543c98279902a6b0812a3" + "url": "https://registry.npmjs.org/es-jquery-sortable/-/es-jquery-sortable-0.9.13-patch2.tgz" }, "require": { "npm-asset/jquery": ">=2.1.2,<3.0.0" }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/johnny/jquery-sortable/issues" - }, - "npm-asset-main": "./source/js/jquery-sortable.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/johnny/jquery-sortable.git" - }, - "npm-asset-scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } - }, + "type": "npm-asset", "license": [ "BSD-3" - ], - "authors": [ - "" - ], - "description": "jquery plugin for sortable, nestable lists", - "homepage": "https://github.com/johnny/jquery-sortable", - "keywords": [ - "drag", - "dragging", - "sort", - "sortable", - "sorting" - ], - "time": "2019-11-20T03:55:51+00:00" + ] }, { "name": "npm-asset/ev-emitter", "version": "1.1.1", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz", - "shasum": "8f18b0ce5c76a5d18017f71c0a795c65b9138f2a" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/metafizzy/ev-emitter/issues" - }, - "npm-asset-main": "ev-emitter.js", - "npm-asset-directories": { - "test": "test" - }, - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/metafizzy/ev-emitter.git" - }, - "npm-asset-scripts": { - "test": "mocha test/test" - } + "url": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "David DeSandro" - } - ], - "description": "lil' event emitter", - "homepage": "https://github.com/metafizzy/ev-emitter#readme", - "keywords": [ - "emitter", - "event", - "pubsub" - ], - "time": "2017-07-06T13:46:38+00:00" + ] }, { "name": "npm-asset/eventemitter3", "version": "2.0.3", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", - "shasum": "b5e1079b59fb5e1ba2771c0a993be060a58c99ba" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/primus/eventemitter3/issues" - }, - "npm-asset-main": "index.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git://github.com/primus/eventemitter3.git" - }, - "npm-asset-scripts": { - "build": "mkdir -p umd && browserify index.js -s EventEmitter3 | uglifyjs -m -o umd/eventemitter3.min.js", - "benchmark": "find benchmarks/run -name '*.js' -exec benchmarks/start.sh {} \\;", - "test": "nyc --reporter=html --reporter=text mocha", - "test-browser": "zuul -- test.js", - "prepublish": "npm run build", - "sync": "node versions.js" - } + "url": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Arnout Kazemier" - } - ], - "description": "EventEmitter3 focuses on performance while maintaining a Node.js AND browser compatible interface.", - "homepage": "https://github.com/primus/eventemitter3#readme", - "keywords": [ - "EventEmitter", - "EventEmitter2", - "EventEmitter3", - "Events", - "addEventListener", - "addListener", - "emit", - "emits", - "emitter", - "event", - "once", - "pub/sub", - "publish", - "reactor", - "subscribe" - ], - "time": "2017-03-31T14:51:09+00:00" + ] + }, + { + "name": "npm-asset/fork-awesome", + "version": "1.2.0", + "dist": { + "type": "tar", + "url": "https://registry.npmjs.org/fork-awesome/-/fork-awesome-1.2.0.tgz" + }, + "type": "npm-asset", + "license": [ + "(OFL-1.1 AND MIT)" + ] }, { "name": "npm-asset/fullcalendar", "version": "3.10.5", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-3.10.5.tgz", - "shasum": "57a3a64d7d744181582bb9e1be32d1846e1db53a" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://fullcalendar.io/wiki/Reporting-Bugs/" - }, - "npm-asset-main": "dist/fullcalendar.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/fullcalendar/fullcalendar.git" - }, - "npm-asset-scripts": { - "clean": "gulp clean", - "dist": "npm run check-env && gulp dist", - "lint": "gulp lint-and-example-repos", - "test": "gulp test:single", - "check-env": "check-node-version --node 11" - } + "url": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-3.10.5.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Adam Shaw", - "email": "arshaw@arshaw.com", - "url": "http://arshaw.com/" - } - ], - "description": "Full-sized drag & drop event calendar", - "homepage": "https://fullcalendar.io/", - "keywords": [ - "calendar", - "event", - "full-sized", - "jquery-plugin" - ], - "time": "2021-11-03T00:01:43+00:00" + ] }, { "name": "npm-asset/imagesloaded", "version": "4.1.4", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/imagesloaded/-/imagesloaded-4.1.4.tgz", - "shasum": "1376efcd162bb768c34c3727ac89cc04051f3cc7" + "url": "https://registry.npmjs.org/imagesloaded/-/imagesloaded-4.1.4.tgz" }, "require": { "npm-asset/ev-emitter": ">=1.0.0,<2.0.0" }, - "require-dev": { - "npm-asset/chalk": ">=1.1.1,<2.0.0", - "npm-asset/cheerio": ">=0.19.0,<0.20.0", - "npm-asset/gulp": ">=3.9.0,<4.0.0", - "npm-asset/gulp-jshint": ">=1.11.2,<2.0.0", - "npm-asset/gulp-json-lint": ">=0.1.0,<0.2.0", - "npm-asset/gulp-rename": ">=1.2.2,<2.0.0", - "npm-asset/gulp-replace": ">=0.5.4,<0.6.0", - "npm-asset/gulp-requirejs-optimize": "dev-github:metafizzy/gulp-requirejs-optimize", - "npm-asset/gulp-uglify": ">=1.4.2,<2.0.0", - "npm-asset/gulp-util": ">=3.0.7,<4.0.0", - "npm-asset/highlight.js": ">=8.9.1,<9.0.0", - "npm-asset/marked": ">=0.3.5,<0.4.0", - "npm-asset/minimist": ">=1.2.0,<2.0.0", - "npm-asset/transfob": ">=1.0.0,<2.0.0" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/desandro/imagesloaded/issues" - }, - "npm-asset-main": "imagesloaded.js", - "npm-asset-directories": { - "test": "test" - }, - "npm-asset-repository": { - "type": "git", - "url": "git://github.com/desandro/imagesloaded.git" - }, - "npm-asset-scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } - }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "David DeSandro" - } - ], - "description": "JavaScript is all like _You images done yet or what?_", - "homepage": "https://github.com/desandro/imagesloaded", - "keywords": [ - "dom", - "images", - "jquery-plugin", - "loaded", - "ui" - ], - "time": "2018-01-02T16:56:03+00:00" + ] }, { "name": "npm-asset/jgrowl", "version": "1.4.9", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.9.tgz", - "shasum": "f0259b74904f4cfc05ea1ad1188fe9b7b3384e2e" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/stanlemon/jGrowl/issues" - }, - "npm-asset-main": "jquery.jgrowl.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+ssh://git@github.com/stanlemon/jGrowl.git" - }, - "npm-asset-scripts": { - "build": "grunt" - } + "url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.9.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Stan Lemon", - "email": "stosh1985@gmail.com", - "url": "http://stanlemon.net" - } - ], - "description": "jGrowl is a jQuery plugin that raises unobtrusive messages within the browser, similar to the way that OS X's Growl Framework works. The idea is simple, deliver notifications to the end user in a noticeable way that doesn't obstruct the work flow and yet ", - "homepage": "https://github.com/stanlemon/jGrowl#readme", - "time": "2023-02-22T23:58:06+00:00" + ] }, { "name": "npm-asset/jquery", "version": "2.2.4", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz", - "shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02" - }, - "require-dev": { - "npm-asset/commitplease": "2.0.0", - "npm-asset/core-js": "0.9.17", - "npm-asset/grunt": "0.4.5", - "npm-asset/grunt-babel": "5.0.1", - "npm-asset/grunt-cli": "0.1.13", - "npm-asset/grunt-compare-size": "0.4.0", - "npm-asset/grunt-contrib-jshint": "0.11.2", - "npm-asset/grunt-contrib-uglify": "0.9.2", - "npm-asset/grunt-contrib-watch": "0.6.1", - "npm-asset/grunt-git-authors": "2.0.1", - "npm-asset/grunt-jscs": "2.1.0", - "npm-asset/grunt-jsonlint": "1.0.4", - "npm-asset/grunt-npmcopy": "0.1.0", - "npm-asset/gzip-js": "0.3.2", - "npm-asset/jsdom": "5.6.1", - "npm-asset/load-grunt-tasks": "1.0.0", - "npm-asset/qunit-assert-step": "1.0.3", - "npm-asset/qunitjs": "1.17.1", - "npm-asset/requirejs": "2.1.17", - "npm-asset/sinon": "1.10.3", - "npm-asset/sizzle": "2.2.1", - "npm-asset/strip-json-comments": "1.0.3", - "npm-asset/testswarm": "1.1.0", - "npm-asset/win-spawn": "2.0.0" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/jquery/jquery/issues" - }, - "npm-asset-main": "dist/jquery.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/jquery/jquery.git" - }, - "npm-asset-scripts": { - "build": "npm install && grunt", - "start": "grunt watch", - "test": "grunt && grunt test" - } + "url": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "jQuery Foundation and other contributors", - "url": "https://github.com/jquery/jquery/blob/2.2.4/AUTHORS.txt" - } - ], - "description": "JavaScript library for DOM operations", - "homepage": "http://jquery.com", - "keywords": [ - "browser", - "javascript", - "jquery", - "library" - ], - "time": "2016-05-20T17:26:07+00:00" + ] }, { "name": "npm-asset/jquery-colorbox", "version": "1.6.4", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/jquery-colorbox/-/jquery-colorbox-1.6.4.tgz", - "shasum": "799452523a6c494839224ef702e807deb9c06cc5" + "url": "https://registry.npmjs.org/jquery-colorbox/-/jquery-colorbox-1.6.4.tgz" }, "require": { "npm-asset/jquery": ">=1.3.2" }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/jackmoore/colorbox/issues" - }, - "npm-asset-main": "jquery.colorbox.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+ssh://git@github.com/jackmoore/colorbox.git" - }, - "npm-asset-scripts": [] - }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Jack Moore", - "email": "hello@jacklmoore.com", - "url": "http://www.jacklmoore.com" - } - ], - "description": "jQuery lightbox and modal window plugin.", - "homepage": "http://www.jacklmoore.com/colorbox", - "keywords": [ - "gallery", - "jquery-plugin", - "lightbox", - "modal", - "popup", - "ui" - ], - "time": "2016-05-10T22:22:39+00:00" + ] }, { "name": "npm-asset/jquery-datetimepicker", "version": "2.5.21", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/jquery-datetimepicker/-/jquery-datetimepicker-2.5.21.tgz", - "shasum": "00c388a78df2732fedfdb5c6529b6e84d53e0235" + "url": "https://registry.npmjs.org/jquery-datetimepicker/-/jquery-datetimepicker-2.5.21.tgz" }, "require": { "npm-asset/jquery": ">=1.7.2", "npm-asset/jquery-mousewheel": ">=3.1.13", "npm-asset/php-date-formatter": ">=1.3.4,<2.0.0" }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/xdan/datetimepicker/issues" - }, - "npm-asset-main": "build/jquery.datetimepicker.full.min.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/xdan/datetimepicker.git" - }, - "npm-asset-scripts": { - "test": "karma start --browsers Firefox karma.conf.js --single-run", - "concat": "concat-cli -f node_modules/php-date-formatter/js/php-date-formatter.min.js jquery.datetimepicker.js node_modules/jquery-mousewheel/jquery.mousewheel.js -o build/jquery.datetimepicker.full.js", - "minify": "uglifyjs jquery.datetimepicker.js -c -m -o build/jquery.datetimepicker.min.js && uglifycss jquery.datetimepicker.css > build/jquery.datetimepicker.min.css", - "minifyconcat": "uglifyjs build/jquery.datetimepicker.full.js -c -m -o build/jquery.datetimepicker.full.min.js", - "github": "git add --all && git commit -m \"New version %npm_package_version% \" && git tag %npm_package_version% && git push --tags origin HEAD:master && npm publish", - "build": "npm run minify && npm run concat && npm run minifyconcat", - "public": "npm run test && npm version patch --no-git-tag-version && npm run build && npm run github" - } - }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Chupurnov", - "email": "chupurnov@gmail.com", - "url": "https://xdsoft.net/" - } - ], - "description": "jQuery Plugin DateTimePicker it is DatePicker and TimePicker in one", - "homepage": "https://github.com/xdan/datetimepicker", - "keywords": [ - "calendar", - "date", - "datepicker", - "datetime", - "jquery-plugin", - "time", - "timepicker" - ], - "time": "2019-02-23T11:25:30+00:00" + ] }, { "name": "npm-asset/jquery-mousewheel", "version": "3.1.13", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", - "shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5" + "url": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz" }, - "require-dev": { - "npm-asset/grunt": "~0.4.1", - "npm-asset/grunt-contrib-connect": "~0.5.0", - "npm-asset/grunt-contrib-jshint": "~0.7.1", - "npm-asset/grunt-contrib-uglify": "~0.2.7" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/jquery/jquery-mousewheel/issues" - }, - "npm-asset-files": [ - "ChangeLog.md", - "jquery.mousewheel.js", - "README.md", - "LICENSE.txt" - ], - "npm-asset-main": "./jquery.mousewheel.js", - "npm-asset-directories": { - "test": "test" - }, - "npm-asset-repository": { - "type": "git", - "url": "https://github.com/jquery/jquery-mousewheel.git" - }, - "npm-asset-scripts": [] - }, - "authors": [ - { - "name": "jQuery Foundation and other contributors", - "url": "https://github.com/jquery/jquery-mousewheel/blob/master/AUTHORS.txt" - } - ], - "description": "A jQuery plugin that adds cross-browser mouse wheel support.", - "homepage": "https://github.com/jquery/jquery-mousewheel", - "keywords": [ - "browser", - "event", - "jquery", - "jquery-plugin", - "mouse", - "mousewheel", - "wheel" - ], - "time": "2015-07-15T18:05:23+00:00" + "type": "npm-asset" }, { "name": "npm-asset/moment", - "version": "2.29.4", + "version": "2.30.1", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "shasum": "3dbe052889fe7c1b2ed966fcb3a77328964ef108" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/moment/moment/issues" - }, - "npm-asset-main": "./moment.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/moment/moment.git" - }, - "npm-asset-scripts": { - "ts3.1-typescript-test": "cross-env node_modules/typescript3/bin/tsc --project ts3.1-typing-tests", - "typescript-test": "cross-env node_modules/typescript/bin/tsc --project typing-tests", - "test": "grunt test", - "eslint": "eslint Gruntfile.js tasks src", - "prettier-check": "prettier --check Gruntfile.js tasks src", - "prettier-fmt": "prettier --write Gruntfile.js tasks src", - "coverage": "nyc npm test && nyc report", - "coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls" - }, - "npm-asset-engines": { - "node": "*" - } + "url": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Iskren Ivov Chernev", - "email": "iskren.chernev@gmail.com", - "url": "https://github.com/ichernev" - }, - { - "name": "Tim Wood", - "email": "washwithcare@gmail.com", - "url": "http://timwoodcreates.com/" - }, - { - "name": "Rocky Meza", - "url": "http://rockymeza.com" - }, - { - "name": "Matt Johnson", - "email": "mj1856@hotmail.com", - "url": "http://codeofmatt.com" - }, - { - "name": "Isaac Cambron", - "email": "isaac@isaaccambron.com", - "url": "http://isaaccambron.com" - }, - { - "name": "Andre Polykanine", - "email": "andre@oire.org", - "url": "https://github.com/oire" - } - ], - "description": "Parse, validate, manipulate, and display dates", - "homepage": "https://momentjs.com", - "keywords": [ - "date", - "ender", - "format", - "i18n", - "l10n", - "moment", - "parse", - "time", - "validate" - ], - "time": "2022-07-06T16:01:32+00:00" + ] }, { "name": "npm-asset/perfect-scrollbar", "version": "0.6.16", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-0.6.16.tgz", - "shasum": "b1d61a5245cf3962bb9a8407a3fc669d923212fc" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/noraesae/perfect-scrollbar/issues" - }, - "npm-asset-files": [ - "dist", - "src", - "index.js", - "jquery.js", - "perfect-scrollbar.d.ts" - ], - "npm-asset-main": "./index.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/noraesae/perfect-scrollbar.git" - }, - "npm-asset-scripts": { - "test": "gulp", - "before-deploy": "gulp && gulp compress", - "release": "rm -rf dist && gulp && npm publish" - }, - "npm-asset-engines": { - "node": ">= 0.12.0" - } + "url": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-0.6.16.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Hyunje Jun", - "email": "me@noraesae.net" - }, - { - "name": "Hyunje Jun", - "email": "me@noraesae.net" - } - ], - "description": "Minimalistic but perfect custom scrollbar plugin", - "homepage": "https://github.com/noraesae/perfect-scrollbar#readme", - "keywords": [ - "frontend", - "jquery-plugin", - "scroll", - "scrollbar" - ], - "time": "2017-01-10T01:03:05+00:00" + ] }, { "name": "npm-asset/php-date-formatter", - "version": "v1.3.6", - "source": { - "type": "git", - "url": "https://github.com/kartik-v/php-date-formatter.git", - "reference": "514a53660b0d69439236fd3cbc3f41512adb00a0" - }, + "version": "1.3.6", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kartik-v/php-date-formatter/zipball/514a53660b0d69439236fd3cbc3f41512adb00a0", - "reference": "514a53660b0d69439236fd3cbc3f41512adb00a0", - "shasum": "" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/kartik-v/php-date-formatter/issues" - }, - "npm-asset-main": "js/php-date-formatter.js", - "npm-asset-directories": { - "example": "examples" - }, - "npm-asset-repository": { - "type": "git", - "url": "https://github.com/kartik-v/php-date-formatter.git" - } + "type": "tar", + "url": "https://registry.npmjs.org/php-date-formatter/-/php-date-formatter-1.3.6.tgz" }, + "type": "npm-asset", "license": [ "BSD-3-Clause" - ], - "authors": [ - "Kartik Visweswaran " - ], - "description": "A Javascript datetime formatting and manipulation library using PHP date-time formats.", - "homepage": "https://github.com/kartik-v/php-date-formatter", - "time": "2020-04-14T10:16:32+00:00" + ] }, { "name": "npm-asset/textarea-caret", "version": "3.1.0", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", - "shasum": "5d5a35bb035fd06b2ff0e25d5359e97f2655087f" - }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/component/textarea-caret-position/issues" - }, - "npm-asset-files": [ - "index.js" - ], - "npm-asset-main": "index.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/component/textarea-caret-position.git" - } + "url": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz" }, + "type": "npm-asset", "license": [ "MIT" - ], - "description": "(x, y) coordinates of the caret in a textarea or input type='text'", - "homepage": "https://github.com/component/textarea-caret-position#readme", - "keywords": [ - "caret", - "position", - "textarea" - ], - "time": "2018-02-20T06:11:03+00:00" + ] }, { "name": "npm-asset/textcomplete", "version": "0.18.2", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/textcomplete/-/textcomplete-0.18.2.tgz", - "shasum": "de0d806567102f7e32daffcbcc3db05af1515eb5" + "url": "https://registry.npmjs.org/textcomplete/-/textcomplete-0.18.2.tgz" }, "require": { "npm-asset/eventemitter3": ">=2.0.3,<3.0.0", "npm-asset/textarea-caret": ">=3.0.1,<4.0.0", "npm-asset/undate": ">=0.2.3,<0.3.0" }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/yuku-t/textcomplete/issues" - }, - "npm-asset-main": "lib/index.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+ssh://git@github.com/yuku-t/textcomplete.git" - }, - "npm-asset-scripts": { - "build": "yarn run clean && run-p build:*", - "build:dist": "webpack && webpack --env=min && run-p print-dist-gz-size", - "build:docs": "run-p build:docs:*", - "build:docs:html": "webpack --config webpack.doc.config.js && pug -o docs src/doc/index.pug", - "build:docs:md": "documentation build src/*.js -f md -o doc/api.md", - "build:lib": "babel src -d lib -s && for js in src/*.js; do cp $js lib/${js##*/}.flow; done", - "clean": "rm -fr dist docs lib", - "format": "prettier --no-semi --trailing-comma all --write 'src/*.js' 'test/**/*.js'", - "gh-release": "npm pack textcomplete && gh-release -a textcomplete-$(cat package.json|jq -r .version).tgz", - "opener": "wait-on http://localhost:8082 && opener http://localhost:8082", - "print-dist-gz-size": "printf 'dist/textcomplete.min.js.gz: %d bytes\\n' \"$(gzip -9kc dist/textcomplete.min.js | wc -c)\"", - "start": "run-p watch opener", - "test": "run-p test:*", - "test:bundlesize": "yarn run build:dist && bundlesize", - "test:e2e": "NODE_ENV=test karma start --single-run", - "test:lint": "eslint src/*.js test/**/*.js", - "test:typecheck": "flow check", - "watch": "run-p watch:*", - "watch:webpack": "webpack-dev-server --config webpack.doc.config.js", - "watch:pug": "pug -o docs --watch src/doc/index.pug" - } - }, + "type": "npm-asset", "license": [ "MIT" - ], - "authors": [ - { - "name": "Yuku Takahashi" - } - ], - "description": "Autocomplete for textarea elements", - "homepage": "https://github.com/yuku-t/textcomplete#readme", - "time": "2020-06-10T06:11:00+00:00" + ] }, { "name": "npm-asset/typeahead.js", "version": "0.11.1", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/typeahead.js/-/typeahead.js-0.11.1.tgz", - "shasum": "4e64e671b22310a8606f4aec805924ba84b015b8" + "url": "https://registry.npmjs.org/typeahead.js/-/typeahead.js-0.11.1.tgz" }, "require": { "npm-asset/jquery": ">=1.7" }, - "type": "npm-asset-library", - "extra": { - "npm-asset-bugs": { - "url": "https://github.com/twitter/typeahead.js/issues" - }, - "npm-asset-main": "dist/typeahead.bundle.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "https://github.com/twitter/typeahead.js.git" - }, - "npm-asset-scripts": { - "test": "./node_modules/karma/bin/karma start --single-run --browsers PhantomJS" - } - }, - "authors": [ - { - "name": "Twitter, Inc.", - "url": "https://twitter.com/twitteross" - }, - { - "name": "Jake Harding", - "url": "https://twitter.com/JakeHarding" - }, - { - "name": "Tim Trueman", - "url": "https://twitter.com/timtrueman" - }, - { - "name": "Veljko Skarich", - "url": "https://twitter.com/vskarich" - } - ], - "description": "fast and fully-featured autocomplete library", - "homepage": "http://twitter.github.com/typeahead.js", - "keywords": [ - "autocomplete", - "typeahead" - ], - "time": "2015-04-27T04:03:42+00:00" + "type": "npm-asset" }, { "name": "npm-asset/undate", "version": "0.2.4", "dist": { "type": "tar", - "url": "https://registry.npmjs.org/undate/-/undate-0.2.4.tgz", - "shasum": "ccb2a8cf38edc035d1006fcb2909c4c6024a8400" + "url": "https://registry.npmjs.org/undate/-/undate-0.2.4.tgz" }, - "type": "npm-asset-library", + "type": "npm-asset", + "license": [ + "MIT" + ] + }, + { + "name": "nyholm/psr7", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", "extra": { - "npm-asset-bugs": { - "url": "https://github.com/yuku-t/undate/issues" - }, - "npm-asset-main": "lib/index.js", - "npm-asset-directories": [], - "npm-asset-repository": { - "type": "git", - "url": "git+https://github.com/yuku-t/undate.git" - }, - "npm-asset-scripts": { - "build": "babel src -d lib && for js in src/*.js; do cp $js lib/${js##*/}.flow; done", - "test": "run-p test:*", - "test:eslint": "eslint src/*.js test/*.js", - "test:flow": "flow check", - "test:karma": "karma start --single-run" + "branch-alias": { + "dev-master": "1.8-dev" } }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Yuku Takahashi" + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" } ], - "description": "Undoable update for HTMLTextAreaElement", - "homepage": "https://github.com/yuku-t/undate#readme", + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", "keywords": [ - "textarea" + "psr-17", + "psr-7" ], - "time": "2018-01-24T10:49:39+00:00" + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-13T09:31:12+00:00" + }, + { + "name": "oomphinc/composer-installers-extender", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/oomphinc/composer-installers-extender.git", + "reference": "cbf4b6f9a24153b785d09eee755b995ba87bd5f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oomphinc/composer-installers-extender/zipball/cbf4b6f9a24153b785d09eee755b995ba87bd5f9", + "reference": "cbf4b6f9a24153b785d09eee755b995ba87bd5f9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "composer/installers": "^1.0 || ^2.0", + "php": ">=7.1" + }, + "require-dev": { + "composer/composer": "^2.0", + "phpunit/phpunit": "^7.2", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "composer-plugin", + "extra": { + "class": "OomphInc\\ComposerInstallersExtender\\Plugin" + }, + "autoload": { + "psr-4": { + "OomphInc\\ComposerInstallersExtender\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Beemsterboer", + "email": "stephen@oomphinc.com", + "homepage": "https://github.com/balbuf" + }, + { + "name": "Nathan Dentzau", + "email": "nate@oomphinc.com", + "homepage": "http://oomph.is/ndentzau" + } + ], + "description": "Extend the composer/installers plugin to accept any arbitrary package type.", + "homepage": "http://www.oomphinc.com/", + "support": { + "issues": "https://github.com/oomphinc/composer-installers-extender/issues", + "source": "https://github.com/oomphinc/composer-installers-extender/tree/2.0.1" + }, + "time": "2021-12-15T12:32:42+00:00" }, { "name": "paragonie/certainty", @@ -3083,17 +2681,91 @@ "time": "2018-01-25T20:47:17+00:00" }, { - "name": "phpseclib/phpseclib", - "version": "3.0.34", + "name": "php-http/discovery", + "version": "1.19.2", "source": { "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a" + "url": "https://github.com/php-http/discovery.git", + "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a", + "url": "https://api.github.com/repos/php-http/discovery/zipball/61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", + "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "symfony/phpunit-bridge": "^6.2" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "time": "2023-11-30T16:49:05+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.37", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", "shasum": "" }, "require": { @@ -3186,7 +2858,226 @@ "type": "tidelift" } ], - "time": "2023-11-27T11:13:31+00:00" + "time": "2024-03-03T02:14:58+00:00" + }, + { + "name": "phrity/net-stream", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-stream.git", + "reference": "9105931b65ad90c75f4885a40b268b0f65802e3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/9105931b65ad90c75f4885a40b268b0f65802e3e", + "reference": "9105931b65ad90c75f4885a40b268b0f65802e3e", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0", + "phrity/util-errorhandler": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 | ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0", + "phrity/net-uri": "^1.1", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory", + "homepage": "https://phrity.sirn.se/net-stream", + "keywords": [ + "Socket", + "client", + "psr-17", + "psr-7", + "server", + "stream", + "stream factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-stream/issues", + "source": "https://github.com/sirn-se/phrity-net-stream/tree/1.3.0" + }, + "time": "2023-10-22T10:47:03+00:00" + }, + { + "name": "phrity/net-uri", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-uri.git", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 | ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "PSR-7 Uri and PSR-17 UriFactory implementation", + "homepage": "https://phrity.sirn.se/net-uri", + "keywords": [ + "psr-17", + "psr-7", + "uri", + "uri factory" + ], + "time": "2023-08-21T10:33:06+00:00" + }, + { + "name": "phrity/util-errorhandler", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Util\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Inline error handler; catch and resolve errors for code block.", + "homepage": "https://phrity.sirn.se/util-errorhandler", + "keywords": [ + "error", + "warning" + ], + "time": "2024-09-12T06:49:16+00:00" + }, + { + "name": "phrity/websocket", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/websocket-php.git", + "reference": "8a525da4457b599ab1960f24183f25626c96ce3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/8a525da4457b599ab1960f24183f25626c96ce3c", + "reference": "8a525da4457b599ab1960f24183f25626c96ce3c", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0", + "phrity/net-stream": "^1.2", + "phrity/net-uri": "^1.2", + "phrity/util-errorhandler": "^1.0", + "psr/http-message": "^1.1 | ^2.0", + "psr/log": "^1.0 | ^2.0 | ^3.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0", + "phrity/net-mock": "^1.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "WebSocket client and server", + "homepage": "https://phrity.sirn.se/websocket", + "keywords": [ + "client", + "server", + "websocket" + ], + "support": { + "issues": "https://github.com/sirn-se/websocket-php/issues", + "source": "https://github.com/sirn-se/websocket-php/tree/1.7.3" + }, + "time": "2024-05-31T13:43:32+00:00" }, { "name": "pragmarx/google2fa", @@ -3415,6 +3306,9 @@ "psr", "psr-6" ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, "time": "2016-08-06T20:24:11+00:00" }, { @@ -3463,27 +3357,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/" @@ -3508,7 +3397,61 @@ "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", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" }, { "name": "psr/http-client", @@ -3557,24 +3500,27 @@ "psr", "psr-18" ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -3598,7 +3544,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -3609,7 +3555,10 @@ "request", "response" ], - "time": "2023-04-10T20:10:41+00:00" + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -3659,6 +3608,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, "time": "2023-04-04T09:50:52+00:00" }, { @@ -3706,6 +3658,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -3746,6 +3701,10 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { @@ -3801,16 +3760,16 @@ }, { "name": "smarty/smarty", - "version": "v4.3.4", + "version": "v4.5.3", "source": { "type": "git", "url": "https://github.com/smarty-php/smarty.git", - "reference": "3931d8f54b8f7a4ffab538582d34d4397ba8daa5" + "reference": "9fc96a13dbaf546c3d7bcf95466726578cd4e0fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smarty-php/smarty/zipball/3931d8f54b8f7a4ffab538582d34d4397ba8daa5", - "reference": "3931d8f54b8f7a4ffab538582d34d4397ba8daa5", + "url": "https://api.github.com/repos/smarty-php/smarty/zipball/9fc96a13dbaf546c3d7bcf95466726578cd4e0fa", + "reference": "9fc96a13dbaf546c3d7bcf95466726578cd4e0fa", "shasum": "" }, "require": { @@ -3858,7 +3817,12 @@ "keywords": [ "templating" ], - "time": "2023-09-14T10:59:08+00:00" + "support": { + "forum": "https://github.com/smarty-php/smarty/discussions", + "issues": "https://github.com/smarty-php/smarty/issues", + "source": "https://github.com/smarty-php/smarty/tree/v4.5.3" + }, + "time": "2024-05-28T21:46:01+00:00" }, { "name": "spomky-labs/base64url", @@ -3922,128 +3886,35 @@ "time": "2020-11-03T09:10:25+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.28.0", + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], - "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": "2023-01-26T09:30:37+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", "shasum": "" }, "require": { "php": ">=7.1" }, - "suggest": { - "ext-intl": "For best performance" - }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" } }, "autoload": { "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4060,16 +3931,11 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4084,7 +3950,171 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "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 tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/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-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-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\\EventDispatcher\\": "" + } + }, + "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 dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-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": "symfony/polyfill-php56", @@ -4152,30 +4182,27 @@ "time": "2020-10-23T14:02:19+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4183,14 +4210,21 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4200,7 +4234,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4208,6 +4242,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4222,7 +4259,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "ua-parser/uap-php", @@ -4354,6 +4391,7 @@ "type": "patreon" } ], + "abandoned": "web-token/jwt-library", "time": "2021-03-17T14:55:52+00:00" }, { @@ -4428,6 +4466,7 @@ "type": "patreon" } ], + "abandoned": "web-token/jwt-library", "time": "2021-03-17T14:55:52+00:00" }, { @@ -4501,6 +4540,7 @@ "type": "patreon" } ], + "abandoned": "web-token/jwt-library", "time": "2021-03-01T19:55:28+00:00" }, { @@ -4567,6 +4607,7 @@ "type": "patreon" } ], + "abandoned": "web-token/jwt-library", "time": "2021-01-21T19:18:03+00:00" }, { @@ -4636,6 +4677,7 @@ "type": "patreon" } ], + "abandoned": "web-token/jwt-library", "time": "2021-03-24T13:35:17+00:00" }, { @@ -4691,6 +4733,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", @@ -4893,16 +5080,16 @@ }, { "name": "mockery/mockery", - "version": "1.6.7", + "version": "1.6.10", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06" + "reference": "47065d1be1fa05def58dc14c03cf831d3884ef0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", - "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", + "url": "https://api.github.com/repos/mockery/mockery/zipball/47065d1be1fa05def58dc14c03cf831d3884ef0b", + "reference": "47065d1be1fa05def58dc14c03cf831d3884ef0b", "shasum": "" }, "require": { @@ -4914,8 +5101,8 @@ "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.10", - "symplify/easy-coding-standard": "^12.0.8" + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, "type": "library", "autoload": { @@ -4965,7 +5152,7 @@ "test double", "testing" ], - "time": "2023-12-10T02:24:34+00:00" + "time": "2024-03-19T16:15:45+00:00" }, { "name": "myclabs/deep-copy", @@ -5024,25 +5211,27 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -5050,7 +5239,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -5072,24 +5261,88 @@ "parser", "php" ], - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.3", + "name": "pdepend/pdepend", + "version": "2.16.2", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "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", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -5128,7 +5381,13 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -5178,24 +5437,437 @@ "time": "2022-02-21T01:04:05+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "name": "php-mock/php-mock", + "version": "2.5.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "url": "https://github.com/php-mock/php-mock.git", + "reference": "fff1a621ebe54100fa3bd852e7be57773a0c0127" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/fff1a621ebe54100fa3bd852e7be57773a0c0127", + "reference": "fff1a621ebe54100fa3bd852e7be57773a0c0127", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0", + "phpunit/php-text-template": "^1 || ^2 || ^3 || ^4" + }, + "replace": { + "malkusch/php-mock": "*" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "phpmock\\": [ + "classes/", + "tests/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock/issues", + "source": "https://github.com/php-mock/php-mock/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2024-02-10T21:07:01+00:00" + }, + { + "name": "php-mock/php-mock-integration", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-integration.git", + "reference": "ec6a00a8129d50ed0f07907c91e3274ca4ade877" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/ec6a00a8129d50ed0f07907c91e3274ca4ade877", + "reference": "ec6a00a8129d50ed0f07907c91e3274ca4ade877", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "php-mock/php-mock": "^2.5", + "phpunit/php-text-template": "^1 || ^2 || ^3 || ^4" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\integration\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Integration package for PHP-Mock", + "homepage": "https://github.com/php-mock/php-mock-integration", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "stub", + "test", + "test double" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock-integration/issues", + "source": "https://github.com/php-mock/php-mock-integration/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2024-02-10T21:37:25+00:00" + }, + { + "name": "php-mock/php-mock-mockery", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-mockery.git", + "reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-mockery/zipball/291994acdc26daf1e3c659cfbe58b01eeb180b7f", + "reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f", + "shasum": "" + }, + "require": { + "mockery/mockery": "^1", + "php": ">=5.6", + "php-mock/php-mock-integration": "^2.2.1 || ^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4|^5|^8" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\mockery\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Mock built-in PHP functions (e.g. time()) with Mockery. This package relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock-mockery", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock-mockery/issues", + "source": "https://github.com/php-mock/php-mock-mockery/tree/1.5.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2025-03-08T19:46:20+00:00" + }, + { + "name": "php-mock/php-mock-phpunit", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-phpunit.git", + "reference": "e1f7e795990b00937376e345883ea68ca3bda7e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/e1f7e795990b00937376e345883ea68ca3bda7e0", + "reference": "e1f7e795990b00937376e345883ea68ca3bda7e0", + "shasum": "" + }, + "require": { + "php": ">=7", + "php-mock/php-mock-integration": "^2.3", + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10.0.17 || ^11" + }, + "require-dev": { + "mockery/mockery": "^1.3.6" + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "phpmock\\phpunit\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Mock built-in PHP functions (e.g. time()) with PHPUnit. This package relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock-phpunit", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "phpunit", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock-phpunit/issues", + "source": "https://github.com/php-mock/php-mock-phpunit/tree/2.10.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "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", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "ab4e9b4415a5fc9e4d27f7fe16c8bc9d067dcd6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ab4e9b4415a5fc9e4d27f7fe16c8bc9d067dcd6d", + "reference": "ab4e9b4415a5fc9e4d27f7fe16c8bc9d067dcd6d", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-11-11T15:43:04+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -5248,7 +5920,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5477,16 +6149,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.15", + "version": "9.6.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" + "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", - "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd", + "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd", "shasum": "" }, "require": { @@ -5571,20 +6243,20 @@ "type": "tidelift" } ], - "time": "2023-12-01T16:55:19+00:00" + "time": "2024-02-23T13:14:51+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -5623,7 +6295,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -5800,20 +6472,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -5849,20 +6521,20 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -5911,7 +6583,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -5974,16 +6646,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -6043,20 +6715,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -6103,24 +6775,24 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -6156,7 +6828,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -6323,16 +6995,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -6344,7 +7016,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -6370,7 +7042,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -6474,17 +7146,570 @@ "time": "2020-09-28T06:39:44+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.2", + "name": "symfony/config", + "version": "v5.4.46", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "url": "https://github.com/symfony/config.git", + "reference": "977c88a02d7d3f16904a81907531b19666a08e78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "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", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -6517,7 +7742,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], @@ -6543,9 +7768,9 @@ "ext-simplexml": "*", "ext-xml": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.6.0" } diff --git a/database.sql b/database.sql index f114bb5bfd..b59a3fc0d2 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ --- Friendica 2023.12 (Yellow archangel) --- DB_UPDATE_VERSION 1542 +-- Friendica 2025.02-dev (Interrupted Fern) +-- DB_UPDATE_VERSION 1580 -- ------------------------------------------ @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS `gserver` ( `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', `url` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `nurl` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `version` varchar(255) NOT NULL DEFAULT '' COMMENT '', + `version` varchar(255) NOT NULL DEFAULT '' COMMENT 'The version of this server software.', `site_name` varchar(255) NOT NULL DEFAULT '' COMMENT '', `info` text COMMENT '', `register_policy` tinyint NOT NULL DEFAULT 0 COMMENT '', @@ -23,10 +23,14 @@ CREATE TABLE IF NOT EXISTS `gserver` ( `local-comments` int unsigned COMMENT 'Number of local comments', `directory-type` tinyint DEFAULT 0 COMMENT 'Type of directory service (Poco, Mastodon)', `poco` varbinary(383) NOT NULL DEFAULT '' COMMENT '', + `openwebauth` varbinary(383) COMMENT 'Path to the OpenWebAuth endpoint', + `authredirect` varbinary(383) COMMENT 'Path to the authRedirect endpoint', `noscrape` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `network` char(4) NOT NULL DEFAULT '' COMMENT '', `protocol` tinyint unsigned COMMENT 'The protocol of the server', - `platform` varchar(255) NOT NULL DEFAULT '' COMMENT '', + `platform` varchar(255) NOT NULL DEFAULT '' COMMENT 'The canonical name of this server software.', + `repository` varbinary(383) COMMENT 'The url of the source code repository of this server software.', + `homepage` varbinary(383) COMMENT 'The url of the homepage of this server software.', `relay-subscribe` boolean NOT NULL DEFAULT '0' COMMENT 'Has the server subscribed to the relay system', `relay-scope` varchar(10) NOT NULL DEFAULT '' COMMENT 'The scope of messages that the server wants to get', `detection-method` tinyint unsigned COMMENT 'Method that had been used to detect that server', @@ -37,10 +41,13 @@ CREATE TABLE IF NOT EXISTS `gserver` ( `blocked` boolean COMMENT 'Server is blocked', `failed` boolean COMMENT 'Connection failed', `next_contact` datetime DEFAULT '0001-01-01 00:00:00' COMMENT 'Next connection request', + `redirect-gsid` int unsigned COMMENT 'Target Gserver id in case of a redirect', PRIMARY KEY(`id`), UNIQUE INDEX `nurl` (`nurl`(190)), INDEX `next_contact` (`next_contact`), - INDEX `network` (`network`) + INDEX `network` (`network`), + INDEX `redirect-gsid` (`redirect-gsid`), + FOREIGN KEY (`redirect-gsid`) REFERENCES `gserver` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Global servers'; -- @@ -66,15 +73,11 @@ CREATE TABLE IF NOT EXISTS `user` ( `theme` varchar(255) NOT NULL DEFAULT '' COMMENT 'user theme preference', `pubkey` text COMMENT 'RSA public key 4096 bit', `prvkey` text COMMENT 'RSA private key 4096 bit', - `spubkey` text COMMENT '', - `sprvkey` text COMMENT '', `verified` boolean NOT NULL DEFAULT '0' COMMENT 'user is verified through email', `blocked` boolean NOT NULL DEFAULT '0' COMMENT '1 for user is blocked', `blockwall` boolean NOT NULL DEFAULT '0' COMMENT 'Prohibit contacts to post to the profile page of the user', `hidewall` boolean NOT NULL DEFAULT '0' COMMENT 'Hide profile details from unknown viewers', `blocktags` boolean NOT NULL DEFAULT '0' COMMENT 'Prohibit contacts to tag the post of this user', - `unkmail` boolean NOT NULL DEFAULT '0' COMMENT 'Permit unknown people to send private mails to this user', - `cntunkmail` int unsigned NOT NULL DEFAULT 10 COMMENT '', `notify-flags` smallint unsigned NOT NULL DEFAULT 65535 COMMENT 'email notification options', `page-flags` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'page/profile type', `account-type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', @@ -181,7 +184,6 @@ CREATE TABLE IF NOT EXISTS `contact` ( `remote_self` boolean NOT NULL DEFAULT '0' COMMENT '', `rel` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'The kind of the relation between the user and the contact', `protocol` char(4) NOT NULL DEFAULT '' COMMENT 'Protocol of the contact', - `subhub` boolean NOT NULL DEFAULT '0' COMMENT '', `hub-verify` varbinary(383) NOT NULL DEFAULT '' COMMENT '', `rating` tinyint NOT NULL DEFAULT 0 COMMENT 'Automatically detected feed poll frequency', `priority` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Feed poll priority', @@ -371,6 +373,7 @@ CREATE TABLE IF NOT EXISTS `apcontact` ( `manually-approve` boolean COMMENT '', `discoverable` boolean COMMENT 'Mastodon extension: true if profile is published in their directory', `suspended` boolean COMMENT 'Mastodon extension: true if profile is suspended', + `posting-restricted` boolean COMMENT 'lemmy:postingRestrictedToMods', `nick` varchar(255) NOT NULL DEFAULT '' COMMENT '', `name` varchar(255) COMMENT '', `about` text COMMENT '', @@ -504,8 +507,13 @@ CREATE TABLE IF NOT EXISTS `channel` ( `access-key` varchar(1) COMMENT 'Access key', `include-tags` varchar(1023) COMMENT 'Comma separated list of tags that will be included in the channel', `exclude-tags` varchar(1023) COMMENT 'Comma separated list of tags that aren\'t allowed in the channel', + `min-size` int unsigned COMMENT 'Minimum post size', + `max-size` int unsigned COMMENT 'Maximum post size', `full-text-search` varchar(1023) COMMENT 'Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode', `media-type` smallint unsigned COMMENT 'Filtered media types', + `languages` mediumtext COMMENT 'Desired languages', + `publish` boolean COMMENT 'publish channel content', + `valid` boolean COMMENT 'Set, when the full-text-search is valid', PRIMARY KEY(`id`), INDEX `uid` (`uid`), FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE @@ -536,6 +544,7 @@ CREATE TABLE IF NOT EXISTS `contact-relation` ( `relation-score` smallint unsigned COMMENT 'score for interactions of relation-cid on cid', `thread-score` smallint unsigned COMMENT 'score for interactions of cid on threads of relation-cid', `relation-thread-score` smallint unsigned COMMENT 'score for interactions of relation-cid on threads of cid', + `post-score` smallint unsigned COMMENT 'score for the amount of posts from cid that can be seen by relation-cid', PRIMARY KEY(`cid`,`relation-cid`), INDEX `relation-cid` (`relation-cid`), FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, @@ -809,6 +818,7 @@ CREATE TABLE IF NOT EXISTS `inbox-entry` ( `activity-id` varbinary(383) COMMENT 'id of the incoming activity', `object-id` varbinary(383) COMMENT '', `in-reply-to-id` varbinary(383) COMMENT '', + `context` varbinary(383) COMMENT '', `conversation` varbinary(383) COMMENT '', `type` varchar(64) COMMENT 'Type of the activity', `object-type` varchar(64) COMMENT 'Type of the object activity', @@ -819,6 +829,7 @@ CREATE TABLE IF NOT EXISTS `inbox-entry` ( `push` boolean COMMENT 'Is the entry pushed or have pulled it?', `trust` boolean COMMENT 'Do we trust this entry?', `wid` int unsigned COMMENT 'Workerqueue id', + `retrial` tinyint unsigned DEFAULT 0 COMMENT 'Retrial counter', PRIMARY KEY(`id`), UNIQUE INDEX `activity-id` (`activity-id`), INDEX `object-id` (`object-id`), @@ -1177,6 +1188,7 @@ CREATE TABLE IF NOT EXISTS `post` ( `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri', `thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri', `external-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the external uri', + `replies-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the replies collection', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.', `edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last edit (default is created)', `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime', @@ -1195,6 +1207,7 @@ CREATE TABLE IF NOT EXISTS `post` ( INDEX `parent-uri-id` (`parent-uri-id`), INDEX `thr-parent-id` (`thr-parent-id`), INDEX `external-id` (`external-id`), + INDEX `replies-id` (`replies-id`), INDEX `owner-id` (`owner-id`), INDEX `author-id` (`author-id`), INDEX `causer-id` (`causer-id`), @@ -1203,6 +1216,7 @@ CREATE TABLE IF NOT EXISTS `post` ( FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`external-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`replies-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`causer-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, @@ -1236,6 +1250,23 @@ CREATE TABLE IF NOT EXISTS `post-category` ( FOREIGN KEY (`tid`) REFERENCES `tag` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='post relation to categories'; +-- +-- TABLE post-counts +-- +CREATE TABLE IF NOT EXISTS `post-counts` ( + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `vid` smallint unsigned NOT NULL COMMENT 'Id of the verb table entry that contains the activity verbs', + `reaction` varchar(4) NOT NULL COMMENT 'Emoji Reaction', + `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri', + `count` int unsigned DEFAULT 0 COMMENT 'Number of activities', + PRIMARY KEY(`uri-id`,`vid`,`reaction`), + INDEX `vid` (`vid`), + INDEX `parent-uri-id` (`parent-uri-id`), + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`vid`) REFERENCES `verb` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, + FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Original remote activity'; + -- -- TABLE post-collection -- @@ -1263,6 +1294,7 @@ CREATE TABLE IF NOT EXISTS `post-content` ( `location` varchar(255) NOT NULL DEFAULT '' COMMENT 'text location where this item originated', `coord` varchar(255) NOT NULL DEFAULT '' COMMENT 'longitude/latitude pair representing location where this item originated', `language` text COMMENT 'Language information about this post', + `sensitive` boolean COMMENT 'If true, this post contains sensitive content', `app` varchar(255) NOT NULL DEFAULT '' COMMENT 'application which generated this item', `rendered-hash` varchar(32) NOT NULL DEFAULT '' COMMENT '', `rendered-html` mediumtext COMMENT 'item.body converted to html', @@ -1275,7 +1307,6 @@ CREATE TABLE IF NOT EXISTS `post-content` ( PRIMARY KEY(`uri-id`), INDEX `plink` (`plink`(191)), INDEX `resource-id` (`resource-id`), - FULLTEXT INDEX `title-content-warning-body` (`title`,`content-warning`,`body`), INDEX `quote-uri-id` (`quote-uri-id`), FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`quote-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE @@ -1314,7 +1345,6 @@ CREATE TABLE IF NOT EXISTS `post-delivery-data` ( `dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via DFRN', `legacy_dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via legacy DFRN', `diaspora` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via Diaspora', - `ostatus` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via OStatus', PRIMARY KEY(`uri-id`), FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Delivery data for items'; @@ -1326,10 +1356,12 @@ CREATE TABLE IF NOT EXISTS `post-engagement` ( `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `contact-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Person, organisation, news, community, relay', - `media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio', - `language` varbinary(128) COMMENT 'Language information about this post', + `media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio)', + `language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format', `searchtext` mediumtext COMMENT 'Simplified text for the full text search', + `size` int unsigned COMMENT 'Body size', `created` datetime COMMENT '', + `network` char(4) COMMENT '', `restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network', `comments` mediumint unsigned COMMENT 'Number of comments', `activities` mediumint unsigned COMMENT 'Number of activities (like, dislike, ...)', @@ -1391,6 +1423,7 @@ CREATE TABLE IF NOT EXISTS `post-media` ( `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', `url` varbinary(1024) NOT NULL COMMENT 'Media URL', `media-uri-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the activities uri-id', + `attach-id` int unsigned COMMENT 'In case of a local attachment, this field is filled with the id in the attach table', `type` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Media type', `mimetype` varchar(60) COMMENT '', `height` smallint unsigned COMMENT 'Height of the media', @@ -1408,14 +1441,49 @@ CREATE TABLE IF NOT EXISTS `post-media` ( `publisher-url` varbinary(383) COMMENT 'URL of the publisher of the media', `publisher-name` varchar(255) COMMENT 'Name of the publisher of the media', `publisher-image` varbinary(383) COMMENT 'Image of the publisher of the media', + `language` char(3) COMMENT 'Language information about this media in the ISO 639 format', + `published` datetime COMMENT 'Publification date of this media', + `modified` datetime COMMENT 'Modification date of this media', PRIMARY KEY(`id`), UNIQUE INDEX `uri-id-url` (`uri-id`,`url`(512)), INDEX `uri-id-id` (`uri-id`,`id`), INDEX `media-uri-id` (`media-uri-id`), + INDEX `attach-id` (`attach-id`), FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, - FOREIGN KEY (`media-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE + FOREIGN KEY (`media-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`attach-id`) REFERENCES `attach` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Attached media'; +-- +-- TABLE post-origin +-- +CREATE TABLE IF NOT EXISTS `post-origin` ( + `id` int unsigned NOT NULL, + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `uid` mediumint unsigned NOT NULL COMMENT 'Owner id which owns this copy of the item', + `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri', + `thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri', + `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.', + `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime', + `gravity` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '', + `vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs', + `private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted', + `wall` boolean NOT NULL DEFAULT '0' COMMENT 'This item was posted to the wall of uid', + PRIMARY KEY(`id`), + UNIQUE INDEX `uid_uri-id` (`uid`,`uri-id`), + INDEX `uri-id` (`uri-id`), + INDEX `parent-uri-id` (`parent-uri-id`), + INDEX `thr-parent-id` (`thr-parent-id`), + INDEX `vid` (`vid`), + INDEX `parent-uri-id_uid` (`parent-uri-id`,`uid`), + INDEX `uid_wall_received` (`uid`,`wall`,`received`), + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`vid`) REFERENCES `verb` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Posts from local users'; + -- -- TABLE post-question -- @@ -1442,6 +1510,26 @@ CREATE TABLE IF NOT EXISTS `post-question-option` ( FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Question option'; +-- +-- TABLE post-searchindex +-- +CREATE TABLE IF NOT EXISTS `post-searchindex` ( + `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', + `media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio)', + `language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format', + `searchtext` mediumtext COMMENT 'Simplified text for the full text search', + `size` int unsigned COMMENT 'Body size', + `created` datetime COMMENT '', + `restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network', + PRIMARY KEY(`uri-id`), + INDEX `owner-id` (`owner-id`), + INDEX `created` (`created`), + FULLTEXT INDEX `searchtext` (`searchtext`), + FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE +) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Content for all posts'; + -- -- TABLE post-tag -- @@ -1463,6 +1551,7 @@ CREATE TABLE IF NOT EXISTS `post-tag` ( -- CREATE TABLE IF NOT EXISTS `post-thread` ( `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `context-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the context collection', `conversation-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the conversation uri', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `author-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item author', @@ -1473,6 +1562,7 @@ CREATE TABLE IF NOT EXISTS `post-thread` ( `changed` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date that something in the conversation changed, indicating clients should fetch the conversation again', `commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', PRIMARY KEY(`uri-id`), + INDEX `context-id` (`context-id`), INDEX `conversation-id` (`conversation-id`), INDEX `owner-id` (`owner-id`), INDEX `author-id` (`author-id`), @@ -1480,6 +1570,7 @@ CREATE TABLE IF NOT EXISTS `post-thread` ( INDEX `received` (`received`), INDEX `commented` (`commented`), FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`context-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`conversation-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, @@ -1495,6 +1586,7 @@ CREATE TABLE IF NOT EXISTS `post-user` ( `parent-uri-id` int unsigned COMMENT 'Id of the item-uri table that contains the parent uri', `thr-parent-id` int unsigned COMMENT 'Id of the item-uri table that contains the thread parent uri', `external-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the external uri', + `replies-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the replies collection', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Creation timestamp.', `edited` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last edit (default is created)', `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime', @@ -1507,6 +1599,7 @@ CREATE TABLE IF NOT EXISTS `post-user` ( `post-reason` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Reason why the post arrived at the user', `vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs', `private` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '0=public, 1=private, 2=unlisted', + `restrictions` tinyint unsigned COMMENT 'Bit array of post restrictions (1 = Reply, 2 = Like, 4 = Announce)', `global` boolean NOT NULL DEFAULT '0' COMMENT '', `visible` boolean NOT NULL DEFAULT '0' COMMENT '', `deleted` boolean NOT NULL DEFAULT '0' COMMENT 'item has been marked for deletion', @@ -1522,10 +1615,11 @@ CREATE TABLE IF NOT EXISTS `post-user` ( `psid` int unsigned COMMENT 'ID of the permission set of this post', PRIMARY KEY(`id`), UNIQUE INDEX `uid_uri-id` (`uid`,`uri-id`), - INDEX `uri-id` (`uri-id`), + INDEX `uri-id_origin_deleted` (`uri-id`,`origin`,`deleted`), INDEX `parent-uri-id` (`parent-uri-id`), INDEX `thr-parent-id` (`thr-parent-id`), INDEX `external-id` (`external-id`), + INDEX `replies-id` (`replies-id`), INDEX `owner-id` (`owner-id`), INDEX `author-id` (`author-id`), INDEX `causer-id` (`causer-id`), @@ -1546,6 +1640,7 @@ CREATE TABLE IF NOT EXISTS `post-user` ( FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`thr-parent-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`external-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`replies-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`causer-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, @@ -1561,6 +1656,7 @@ CREATE TABLE IF NOT EXISTS `post-user` ( -- CREATE TABLE IF NOT EXISTS `post-thread-user` ( `uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri', + `context-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the endpoint for the context collection', `conversation-id` int unsigned COMMENT 'Id of the item-uri table entry that contains the conversation uri', `owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner', `author-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item author', @@ -1586,6 +1682,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` ( `post-user-id` int unsigned COMMENT 'Id of the post-user table', PRIMARY KEY(`uid`,`uri-id`), INDEX `uri-id` (`uri-id`), + INDEX `context-id` (`context-id`), INDEX `conversation-id` (`conversation-id`), INDEX `owner-id` (`owner-id`), INDEX `author-id` (`author-id`), @@ -1608,6 +1705,7 @@ CREATE TABLE IF NOT EXISTS `post-thread-user` ( INDEX `contact-id_received` (`contact-id`,`received`), INDEX `contact-id_created` (`contact-id`,`created`), FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, + FOREIGN KEY (`context-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`conversation-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, FOREIGN KEY (`author-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, @@ -1693,7 +1791,6 @@ CREATE TABLE IF NOT EXISTS `profile` ( `net-publish` boolean NOT NULL DEFAULT '0' COMMENT 'publish profile in global directory', PRIMARY KEY(`id`), INDEX `uid_is-default` (`uid`,`is-default`), - FULLTEXT INDEX `pub_keywords` (`pub_keywords`), FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='user profiles data'; @@ -1717,26 +1814,6 @@ CREATE TABLE IF NOT EXISTS `profile_field` ( FOREIGN KEY (`psid`) REFERENCES `permissionset` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Custom profile fields'; --- --- TABLE push_subscriber --- -CREATE TABLE IF NOT EXISTS `push_subscriber` ( - `id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID', - `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'User id', - `callback_url` varbinary(383) NOT NULL DEFAULT '' COMMENT '', - `topic` varchar(255) NOT NULL DEFAULT '' COMMENT '', - `nickname` varchar(255) NOT NULL DEFAULT '' COMMENT '', - `push` tinyint NOT NULL DEFAULT 0 COMMENT 'Retrial counter', - `last_update` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last successful trial', - `next_try` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Next retrial date', - `renewed` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last subscription renewal', - `secret` varchar(255) NOT NULL DEFAULT '' COMMENT '', - PRIMARY KEY(`id`), - INDEX `next_try` (`next_try`), - INDEX `uid` (`uid`), - FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE -) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Used for OStatus: Contains feed subscribers'; - -- -- TABLE register -- @@ -1905,6 +1982,7 @@ CREATE TABLE IF NOT EXISTS `user-contact` ( `ignored` boolean COMMENT 'Posts from this contact are ignored', `collapsed` boolean COMMENT 'Posts from this contact are collapsed', `hidden` boolean COMMENT 'This contact is hidden from the others', + `channel-only` boolean COMMENT 'This contact is displayed only in channels, but not in the network stream.', `is-blocked` boolean COMMENT 'User is blocked by this contact', `channel-frequency` tinyint unsigned COMMENT 'Controls the frequency of the appearance of this contact in channels', `pending` boolean COMMENT '', @@ -1914,7 +1992,6 @@ CREATE TABLE IF NOT EXISTS `user-contact` ( `remote_self` tinyint unsigned COMMENT '0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare', `fetch_further_information` tinyint unsigned COMMENT '0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both', `ffi_keyword_denylist` text COMMENT '', - `subhub` boolean COMMENT '', `hub-verify` varbinary(383) COMMENT '', `protocol` char(4) COMMENT 'Protocol of the contact', `rating` tinyint COMMENT 'Automatically detected feed poll frequency', @@ -1975,7 +2052,8 @@ CREATE VIEW `application-view` AS SELECT `application-token`.`follow` AS `follow`, `application-token`.`push` AS `push` FROM `application-token` - INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`; + INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id` + INNER JOIN `user` ON `user`.`uid` = `application-token`.`uid` AND `user`.`verified` AND NOT `user`.`blocked` AND NOT `user`.`account_removed` AND NOT `user`.`account_expired`; -- -- VIEW circle-member-view @@ -2008,6 +2086,53 @@ CREATE VIEW `circle-member-view` AS SELECT INNER JOIN `contact` ON `group_member`.`contact-id` = `contact`.`id` INNER JOIN `group` ON `group_member`.`gid` = `group`.`id`; +-- +-- VIEW post-counts-view +-- +DROP VIEW IF EXISTS `post-counts-view`; +CREATE VIEW `post-counts-view` AS SELECT + `post-counts`.`uri-id` AS `uri-id`, + `post-counts`.`vid` AS `vid`, + `verb`.`name` AS `verb`, + `post-counts`.`reaction` AS `reaction`, + `post-counts`.`parent-uri-id` AS `parent-uri-id`, + `post-counts`.`count` AS `count` + FROM `post-counts` + INNER JOIN `verb` ON `verb`.`id` = `post-counts`.`vid`; + +-- +-- VIEW post-engagement-user-view +-- +DROP VIEW IF EXISTS `post-engagement-user-view`; +CREATE VIEW `post-engagement-user-view` AS SELECT + `post-thread-user`.`uid` AS `uid`, + `post-engagement`.`uri-id` AS `uri-id`, + `post-engagement`.`owner-id` AS `owner-id`, + `post-engagement`.`media-type` AS `media-type`, + `post-engagement`.`language` AS `language`, + `post-engagement`.`searchtext` AS `searchtext`, + `post-engagement`.`size` AS `size`, + `post-thread-user`.`commented` AS `commented`, + `post-thread-user`.`received` AS `received`, + `post-thread-user`.`created` AS `created`, + `post-thread-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, + `post-engagement`.`language` AS `restricted`, + 0 AS `comments`, + 0 AS `activities` + FROM `post-thread-user` + INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `post-thread-user`.`uri-id` + INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` + WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` + AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) + AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) + AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored` OR `is-blocked`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); + -- -- VIEW post-timeline-view -- @@ -2045,7 +2170,10 @@ CREATE VIEW `post-timeline-view` AS SELECT `owner`.`gsid` AS `owner-gsid`, `post-user`.`causer-id` AS `causer-id`, `causer`.`blocked` AS `causer-blocked`, - `causer`.`gsid` AS `causer-gsid` + `causer`.`gsid` AS `causer-gsid`, + `post-thread-user`.`network` AS `parent-network`, + `post-thread-user`.`owner-id` AS `parent-owner-id`, + `post-thread-user`.`author-id` AS `parent-author-id` FROM `post-user` LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` @@ -2054,52 +2182,135 @@ CREATE VIEW `post-timeline-view` AS SELECT LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`; -- --- VIEW post-user-view +-- VIEW post-timeline-origin-view -- -DROP VIEW IF EXISTS `post-user-view`; -CREATE VIEW `post-user-view` AS SELECT - `post-user`.`id` AS `id`, - `post-user`.`id` AS `post-user-id`, - `post-user`.`uid` AS `uid`, +DROP VIEW IF EXISTS `post-timeline-origin-view`; +CREATE VIEW `post-timeline-origin-view` AS SELECT + `post-origin`.`uid` AS `uid`, + `post-origin`.`uri-id` AS `uri-id`, + `post-origin`.`gravity` AS `gravity`, + `post-origin`.`created` AS `created`, + `post-user`.`edited` AS `edited`, + `post-thread-user`.`commented` AS `commented`, + `post-origin`.`received` AS `received`, + `post-thread-user`.`changed` AS `changed`, + `post-origin`.`private` AS `private`, + `post-user`.`visible` AS `visible`, + `post-user`.`deleted` AS `deleted`, + true AS `origin`, + `post-user`.`global` AS `global`, + `post-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, + `post-origin`.`vid` AS `vid`, + `post-user`.`contact-id` AS `contact-id`, + `contact`.`blocked` AS `contact-blocked`, + `contact`.`readonly` AS `contact-readonly`, + `contact`.`pending` AS `contact-pending`, + `contact`.`rel` AS `contact-rel`, + `contact`.`uid` AS `contact-uid`, + `contact`.`self` AS `self`, + `post-user`.`author-id` AS `author-id`, + `author`.`blocked` AS `author-blocked`, + `author`.`hidden` AS `author-hidden`, + `author`.`gsid` AS `author-gsid`, + `post-user`.`owner-id` AS `owner-id`, + `owner`.`blocked` AS `owner-blocked`, + `owner`.`gsid` AS `owner-gsid`, + `post-user`.`causer-id` AS `causer-id`, + `causer`.`blocked` AS `causer-blocked`, + `causer`.`gsid` AS `causer-gsid` + FROM `post-origin` + INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` + LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` + LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`; + +-- +-- VIEW post-searchindex-user-view +-- +DROP VIEW IF EXISTS `post-searchindex-user-view`; +CREATE VIEW `post-searchindex-user-view` AS SELECT + `post-thread-user`.`uid` AS `uid`, + `post-searchindex`.`uri-id` AS `uri-id`, + `post-searchindex`.`owner-id` AS `owner-id`, + `post-searchindex`.`media-type` AS `media-type`, + `post-searchindex`.`language` AS `language`, + `post-searchindex`.`searchtext` AS `searchtext`, + `post-searchindex`.`size` AS `size`, + `post-thread-user`.`commented` AS `commented`, + `post-thread-user`.`received` AS `received`, + `post-thread-user`.`created` AS `created`, + `post-thread-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, + `post-searchindex`.`language` AS `restricted`, + 0 AS `comments`, + 0 AS `activities` + FROM `post-thread-user` + INNER JOIN `post-searchindex` ON `post-searchindex`.`uri-id` = `post-thread-user`.`uri-id` + INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` + WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` + AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) + AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) + AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored` OR `is-blocked`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); + +-- +-- VIEW post-origin-view +-- +DROP VIEW IF EXISTS `post-origin-view`; +CREATE VIEW `post-origin-view` AS SELECT + `post-origin`.`id` AS `id`, + `post-origin`.`id` AS `post-user-id`, + `post-origin`.`uid` AS `uid`, `post-thread-user`.`post-user-id` AS `parent`, `item-uri`.`uri` AS `uri`, - `post-user`.`uri-id` AS `uri-id`, + `post-origin`.`uri-id` AS `uri-id`, `parent-item-uri`.`uri` AS `parent-uri`, - `post-user`.`parent-uri-id` AS `parent-uri-id`, + `post-origin`.`parent-uri-id` AS `parent-uri-id`, `thr-parent-item-uri`.`uri` AS `thr-parent`, - `post-user`.`thr-parent-id` AS `thr-parent-id`, + `post-origin`.`thr-parent-id` AS `thr-parent-id`, `conversation-item-uri`.`uri` AS `conversation`, `post-thread-user`.`conversation-id` AS `conversation-id`, + `context-item-uri`.`uri` AS `context`, + `post-thread-user`.`context-id` AS `context-id`, `quote-item-uri`.`uri` AS `quote-uri`, `post-content`.`quote-uri-id` AS `quote-uri-id`, `item-uri`.`guid` AS `guid`, - `post-user`.`wall` AS `wall`, - `post-user`.`gravity` AS `gravity`, + `post-origin`.`wall` AS `wall`, + `post-origin`.`gravity` AS `gravity`, `external-item-uri`.`uri` AS `extid`, `post-user`.`external-id` AS `external-id`, - `post-user`.`created` AS `created`, + `replies-item-uri`.`uri` AS `replies`, + `post-user`.`replies-id` AS `replies-id`, + `post-origin`.`created` AS `created`, `post-user`.`edited` AS `edited`, `post-thread-user`.`commented` AS `commented`, - `post-user`.`received` AS `received`, + `post-origin`.`received` AS `received`, `post-thread-user`.`changed` AS `changed`, `post-user`.`post-type` AS `post-type`, `post-user`.`post-reason` AS `post-reason`, - `post-user`.`private` AS `private`, + `post-origin`.`private` AS `private`, `post-thread-user`.`pubmail` AS `pubmail`, `post-user`.`visible` AS `visible`, `post-thread-user`.`starred` AS `starred`, `post-user`.`unseen` AS `unseen`, `post-user`.`deleted` AS `deleted`, - `post-user`.`origin` AS `origin`, + true AS `origin`, `post-thread-user`.`origin` AS `parent-origin`, `post-thread-user`.`mention` AS `mention`, `post-user`.`global` AS `global`, - EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-user`.`uri-id`) AS `featured`, + EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-origin`.`uri-id`) AS `featured`, `post-user`.`network` AS `network`, `post-user`.`protocol` AS `protocol`, - `post-user`.`vid` AS `vid`, + `post-origin`.`vid` AS `vid`, `post-user`.`psid` AS `psid`, - IF (`post-user`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, + IF (`post-origin`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, `post-content`.`title` AS `title`, `post-content`.`content-warning` AS `content-warning`, `post-content`.`raw-body` AS `raw-body`, @@ -2110,6 +2321,8 @@ CREATE VIEW `post-user-view` AS SELECT `post-content`.`plink` AS `plink`, `post-content`.`location` AS `location`, `post-content`.`coord` AS `coord`, + `post-content`.`sensitive` AS `sensitive`, + `post-user`.`restrictions` AS `restrictions`, `post-content`.`app` AS `app`, `post-content`.`object-type` AS `object-type`, `post-content`.`object` AS `object`, @@ -2207,83 +2420,92 @@ CREATE VIEW `post-user-view` AS SELECT `post-question`.`multiple` AS `question-multiple`, `post-question`.`voters` AS `question-voters`, `post-question`.`end-time` AS `question-end-time`, - EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-user`.`uri-id` AND `post-category`.`uid` = `post-user`.`uid`) AS `has-categories`, - EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-user`.`uri-id`) AS `has-media`, + EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-origin`.`uri-id` AND `post-category`.`uid` = `post-origin`.`uid`) AS `has-categories`, + EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-origin`.`uri-id`) AS `has-media`, `diaspora-interaction`.`interaction` AS `signed_text`, `parent-item-uri`.`guid` AS `parent-guid`, `post-thread-user`.`network` AS `parent-network`, + `post-thread-user`.`owner-id` AS `parent-owner-id`, `post-thread-user`.`author-id` AS `parent-author-id`, `parent-post-author`.`url` AS `parent-author-link`, `parent-post-author`.`name` AS `parent-author-name`, `parent-post-author`.`nick` AS `parent-author-nick`, `parent-post-author`.`network` AS `parent-author-network` - FROM `post-user` - INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` + FROM `post-origin` + INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` + INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id` - LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-user`.`uri-id` - LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` - LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` + LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-origin`.`uri-id` + LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-origin`.`thr-parent-id` + LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-origin`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` - LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` + LEFT JOIN `verb` ON `verb`.`id` = `post-origin`.`vid` LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` - LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-user`.`uri-id` - LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-origin`.`uri-id` LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id` - LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-user`.`uri-id` AND `post-user`.`origin` - LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-origin`.`uri-id` LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid` LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread-user`.`author-id`; -- --- VIEW post-thread-user-view +-- VIEW post-thread-origin-view -- -DROP VIEW IF EXISTS `post-thread-user-view`; -CREATE VIEW `post-thread-user-view` AS SELECT - `post-user`.`id` AS `id`, - `post-user`.`id` AS `post-user-id`, - `post-thread-user`.`uid` AS `uid`, +DROP VIEW IF EXISTS `post-thread-origin-view`; +CREATE VIEW `post-thread-origin-view` AS SELECT + `post-origin`.`id` AS `id`, + `post-origin`.`id` AS `post-user-id`, + `post-origin`.`uid` AS `uid`, `post-thread-user`.`post-user-id` AS `parent`, `item-uri`.`uri` AS `uri`, - `post-thread-user`.`uri-id` AS `uri-id`, + `post-origin`.`uri-id` AS `uri-id`, `parent-item-uri`.`uri` AS `parent-uri`, - `post-user`.`parent-uri-id` AS `parent-uri-id`, + `post-origin`.`parent-uri-id` AS `parent-uri-id`, `thr-parent-item-uri`.`uri` AS `thr-parent`, - `post-user`.`thr-parent-id` AS `thr-parent-id`, + `post-origin`.`thr-parent-id` AS `thr-parent-id`, `conversation-item-uri`.`uri` AS `conversation`, `post-thread-user`.`conversation-id` AS `conversation-id`, + `context-item-uri`.`uri` AS `context`, + `post-thread-user`.`context-id` AS `context-id`, `quote-item-uri`.`uri` AS `quote-uri`, `post-content`.`quote-uri-id` AS `quote-uri-id`, `item-uri`.`guid` AS `guid`, - `post-thread-user`.`wall` AS `wall`, - `post-user`.`gravity` AS `gravity`, + `post-origin`.`wall` AS `wall`, + `post-origin`.`gravity` AS `gravity`, `external-item-uri`.`uri` AS `extid`, `post-user`.`external-id` AS `external-id`, - `post-thread-user`.`created` AS `created`, + `replies-item-uri`.`uri` AS `replies`, + `post-user`.`replies-id` AS `replies-id`, + `post-origin`.`created` AS `created`, `post-user`.`edited` AS `edited`, `post-thread-user`.`commented` AS `commented`, - `post-thread-user`.`received` AS `received`, + `post-origin`.`received` AS `received`, `post-thread-user`.`changed` AS `changed`, `post-user`.`post-type` AS `post-type`, `post-user`.`post-reason` AS `post-reason`, - `post-user`.`private` AS `private`, + `post-origin`.`private` AS `private`, `post-thread-user`.`pubmail` AS `pubmail`, `post-thread-user`.`ignored` AS `ignored`, `post-user`.`visible` AS `visible`, `post-thread-user`.`starred` AS `starred`, `post-thread-user`.`unseen` AS `unseen`, `post-user`.`deleted` AS `deleted`, - `post-thread-user`.`origin` AS `origin`, + true AS `origin`, `post-thread-user`.`mention` AS `mention`, `post-user`.`global` AS `global`, EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread-user`.`uri-id`) AS `featured`, `post-thread-user`.`network` AS `network`, - `post-user`.`vid` AS `vid`, + `post-user`.`protocol` AS `protocol`, + `post-origin`.`vid` AS `vid`, `post-thread-user`.`psid` AS `psid`, - IF (`post-user`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, + IF (`post-origin`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, `post-content`.`title` AS `title`, `post-content`.`content-warning` AS `content-warning`, `post-content`.`raw-body` AS `raw-body`, @@ -2294,6 +2516,8 @@ CREATE VIEW `post-thread-user-view` AS SELECT `post-content`.`plink` AS `plink`, `post-content`.`location` AS `location`, `post-content`.`coord` AS `coord`, + `post-content`.`sensitive` AS `sensitive`, + `post-user`.`restrictions` AS `restrictions`, `post-content`.`app` AS `app`, `post-content`.`object-type` AS `object-type`, `post-content`.`object` AS `object`, @@ -2396,6 +2620,395 @@ CREATE VIEW `post-thread-user-view` AS SELECT `diaspora-interaction`.`interaction` AS `signed_text`, `parent-item-uri`.`guid` AS `parent-guid`, `post-thread-user`.`network` AS `parent-network`, + `post-thread-user`.`owner-id` AS `parent-owner-id`, + `post-thread-user`.`author-id` AS `parent-author-id`, + `author`.`url` AS `parent-author-link`, + `author`.`name` AS `parent-author-name`, + `author`.`nick` AS `parent-author-nick`, + `author`.`network` AS `parent-author-network` + FROM `post-origin` + INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` + INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-thread-user`.`owner-id` + LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-thread-user`.`causer-id` + LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-origin`.`uri-id` + LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-origin`.`thr-parent-id` + LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-origin`.`parent-uri-id` + LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` + LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` + LEFT JOIN `verb` ON `verb`.`id` = `post-origin`.`vid` + LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` + LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id` + LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-thread-user`.`psid`; + +-- +-- VIEW post-user-view +-- +DROP VIEW IF EXISTS `post-user-view`; +CREATE VIEW `post-user-view` AS SELECT + `post-user`.`id` AS `id`, + `post-user`.`id` AS `post-user-id`, + `post-user`.`uid` AS `uid`, + `post-thread-user`.`post-user-id` AS `parent`, + `item-uri`.`uri` AS `uri`, + `post-user`.`uri-id` AS `uri-id`, + `parent-item-uri`.`uri` AS `parent-uri`, + `post-user`.`parent-uri-id` AS `parent-uri-id`, + `thr-parent-item-uri`.`uri` AS `thr-parent`, + `post-user`.`thr-parent-id` AS `thr-parent-id`, + `conversation-item-uri`.`uri` AS `conversation`, + `post-thread-user`.`conversation-id` AS `conversation-id`, + `context-item-uri`.`uri` AS `context`, + `post-thread-user`.`context-id` AS `context-id`, + `quote-item-uri`.`uri` AS `quote-uri`, + `post-content`.`quote-uri-id` AS `quote-uri-id`, + `item-uri`.`guid` AS `guid`, + `post-user`.`wall` AS `wall`, + `post-user`.`gravity` AS `gravity`, + `external-item-uri`.`uri` AS `extid`, + `post-user`.`external-id` AS `external-id`, + `replies-item-uri`.`uri` AS `replies`, + `post-user`.`replies-id` AS `replies-id`, + `post-user`.`created` AS `created`, + `post-user`.`edited` AS `edited`, + `post-thread-user`.`commented` AS `commented`, + `post-user`.`received` AS `received`, + `post-thread-user`.`changed` AS `changed`, + `post-user`.`post-type` AS `post-type`, + `post-user`.`post-reason` AS `post-reason`, + `post-user`.`private` AS `private`, + `post-thread-user`.`pubmail` AS `pubmail`, + `post-user`.`visible` AS `visible`, + `post-thread-user`.`starred` AS `starred`, + `post-user`.`unseen` AS `unseen`, + `post-user`.`deleted` AS `deleted`, + `post-user`.`origin` AS `origin`, + `post-thread-user`.`origin` AS `parent-origin`, + `post-thread-user`.`mention` AS `mention`, + `post-user`.`global` AS `global`, + EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-user`.`uri-id`) AS `featured`, + `post-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, + `post-user`.`vid` AS `vid`, + `post-user`.`psid` AS `psid`, + IF (`post-user`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, + `post-content`.`title` AS `title`, + `post-content`.`content-warning` AS `content-warning`, + `post-content`.`raw-body` AS `raw-body`, + IFNULL (`post-content`.`body`, '') AS `body`, + `post-content`.`rendered-hash` AS `rendered-hash`, + `post-content`.`rendered-html` AS `rendered-html`, + `post-content`.`language` AS `language`, + `post-content`.`plink` AS `plink`, + `post-content`.`location` AS `location`, + `post-content`.`coord` AS `coord`, + `post-content`.`sensitive` AS `sensitive`, + `post-user`.`restrictions` AS `restrictions`, + `post-content`.`app` AS `app`, + `post-content`.`object-type` AS `object-type`, + `post-content`.`object` AS `object`, + `post-content`.`target-type` AS `target-type`, + `post-content`.`target` AS `target`, + `post-content`.`resource-id` AS `resource-id`, + `post-user`.`contact-id` AS `contact-id`, + `contact`.`uri-id` AS `contact-uri-id`, + `contact`.`url` AS `contact-link`, + `contact`.`addr` AS `contact-addr`, + `contact`.`name` AS `contact-name`, + `contact`.`nick` AS `contact-nick`, + `contact`.`thumb` AS `contact-avatar`, + `contact`.`network` AS `contact-network`, + `contact`.`blocked` AS `contact-blocked`, + `contact`.`hidden` AS `contact-hidden`, + `contact`.`readonly` AS `contact-readonly`, + `contact`.`archive` AS `contact-archive`, + `contact`.`pending` AS `contact-pending`, + `contact`.`rel` AS `contact-rel`, + `contact`.`uid` AS `contact-uid`, + `contact`.`contact-type` AS `contact-contact-type`, + IF (`post-user`.`network` IN ('apub', 'dfrn', 'dspr', 'stat'), true, `contact`.`writable`) AS `writable`, + `contact`.`self` AS `self`, + `contact`.`id` AS `cid`, + `contact`.`alias` AS `alias`, + `contact`.`photo` AS `photo`, + `contact`.`name-date` AS `name-date`, + `contact`.`uri-date` AS `uri-date`, + `contact`.`avatar-date` AS `avatar-date`, + `contact`.`thumb` AS `thumb`, + `post-user`.`author-id` AS `author-id`, + `author`.`uri-id` AS `author-uri-id`, + `author`.`url` AS `author-link`, + `author`.`addr` AS `author-addr`, + IF (`contact`.`url` = `author`.`url` AND `contact`.`name` != '', `contact`.`name`, `author`.`name`) AS `author-name`, + `author`.`nick` AS `author-nick`, + `author`.`alias` AS `author-alias`, + IF (`contact`.`url` = `author`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `author`.`thumb`) AS `author-avatar`, + `author`.`network` AS `author-network`, + `author`.`blocked` AS `author-blocked`, + `author`.`hidden` AS `author-hidden`, + `author`.`updated` AS `author-updated`, + `author`.`contact-type` AS `author-contact-type`, + `author`.`gsid` AS `author-gsid`, + `author`.`baseurl` AS `author-baseurl`, + `post-user`.`owner-id` AS `owner-id`, + `owner`.`uri-id` AS `owner-uri-id`, + `owner`.`url` AS `owner-link`, + `owner`.`addr` AS `owner-addr`, + IF (`contact`.`url` = `owner`.`url` AND `contact`.`name` != '', `contact`.`name`, `owner`.`name`) AS `owner-name`, + `owner`.`nick` AS `owner-nick`, + `owner`.`alias` AS `owner-alias`, + IF (`contact`.`url` = `owner`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `owner`.`thumb`) AS `owner-avatar`, + `owner`.`network` AS `owner-network`, + `owner`.`blocked` AS `owner-blocked`, + `owner`.`hidden` AS `owner-hidden`, + `owner`.`updated` AS `owner-updated`, + `owner`.`gsid` AS `owner-gsid`, + `owner`.`contact-type` AS `owner-contact-type`, + `post-user`.`causer-id` AS `causer-id`, + `causer`.`uri-id` AS `causer-uri-id`, + `causer`.`url` AS `causer-link`, + `causer`.`addr` AS `causer-addr`, + `causer`.`name` AS `causer-name`, + `causer`.`nick` AS `causer-nick`, + `causer`.`alias` AS `causer-alias`, + `causer`.`thumb` AS `causer-avatar`, + `causer`.`network` AS `causer-network`, + `causer`.`blocked` AS `causer-blocked`, + `causer`.`hidden` AS `causer-hidden`, + `causer`.`gsid` AS `causer-gsid`, + `causer`.`contact-type` AS `causer-contact-type`, + `post-delivery-data`.`postopts` AS `postopts`, + `post-delivery-data`.`inform` AS `inform`, + `post-delivery-data`.`queue_count` AS `delivery_queue_count`, + `post-delivery-data`.`queue_done` AS `delivery_queue_done`, + `post-delivery-data`.`queue_failed` AS `delivery_queue_failed`, + IF (`post-user`.`psid` IS NULL, '', `permissionset`.`allow_cid`) AS `allow_cid`, + IF (`post-user`.`psid` IS NULL, '', `permissionset`.`allow_gid`) AS `allow_gid`, + IF (`post-user`.`psid` IS NULL, '', `permissionset`.`deny_cid`) AS `deny_cid`, + IF (`post-user`.`psid` IS NULL, '', `permissionset`.`deny_gid`) AS `deny_gid`, + `post-user`.`event-id` AS `event-id`, + `event`.`created` AS `event-created`, + `event`.`edited` AS `event-edited`, + `event`.`start` AS `event-start`, + `event`.`finish` AS `event-finish`, + `event`.`summary` AS `event-summary`, + `event`.`desc` AS `event-desc`, + `event`.`location` AS `event-location`, + `event`.`type` AS `event-type`, + `event`.`nofinish` AS `event-nofinish`, + `event`.`ignore` AS `event-ignore`, + `post-question`.`id` AS `question-id`, + `post-question`.`multiple` AS `question-multiple`, + `post-question`.`voters` AS `question-voters`, + `post-question`.`end-time` AS `question-end-time`, + EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-user`.`uri-id` AND `post-category`.`uid` = `post-user`.`uid`) AS `has-categories`, + EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-user`.`uri-id`) AS `has-media`, + `diaspora-interaction`.`interaction` AS `signed_text`, + `parent-item-uri`.`guid` AS `parent-guid`, + `post-thread-user`.`network` AS `parent-network`, + `post-thread-user`.`owner-id` AS `parent-owner-id`, + `post-thread-user`.`author-id` AS `parent-author-id`, + `parent-post-author`.`url` AS `parent-author-link`, + `parent-post-author`.`name` AS `parent-author-name`, + `parent-post-author`.`nick` AS `parent-author-nick`, + `parent-post-author`.`network` AS `parent-author-network` + FROM `post-user` + INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` + LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id` + LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-user`.`uri-id` + LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` + LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` + LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` + LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` + LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` + LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` + LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id` + LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-user`.`uri-id` AND `post-user`.`origin` + LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid` + LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread-user`.`author-id`; + +-- +-- VIEW post-thread-user-view +-- +DROP VIEW IF EXISTS `post-thread-user-view`; +CREATE VIEW `post-thread-user-view` AS SELECT + `post-user`.`id` AS `id`, + `post-user`.`id` AS `post-user-id`, + `post-thread-user`.`uid` AS `uid`, + `post-thread-user`.`post-user-id` AS `parent`, + `item-uri`.`uri` AS `uri`, + `post-thread-user`.`uri-id` AS `uri-id`, + `parent-item-uri`.`uri` AS `parent-uri`, + `post-user`.`parent-uri-id` AS `parent-uri-id`, + `thr-parent-item-uri`.`uri` AS `thr-parent`, + `post-user`.`thr-parent-id` AS `thr-parent-id`, + `conversation-item-uri`.`uri` AS `conversation`, + `post-thread-user`.`conversation-id` AS `conversation-id`, + `context-item-uri`.`uri` AS `context`, + `post-thread-user`.`context-id` AS `context-id`, + `quote-item-uri`.`uri` AS `quote-uri`, + `post-content`.`quote-uri-id` AS `quote-uri-id`, + `item-uri`.`guid` AS `guid`, + `post-thread-user`.`wall` AS `wall`, + `post-user`.`gravity` AS `gravity`, + `external-item-uri`.`uri` AS `extid`, + `post-user`.`external-id` AS `external-id`, + `replies-item-uri`.`uri` AS `replies`, + `post-user`.`replies-id` AS `replies-id`, + `post-thread-user`.`created` AS `created`, + `post-user`.`edited` AS `edited`, + `post-thread-user`.`commented` AS `commented`, + `post-thread-user`.`received` AS `received`, + `post-thread-user`.`changed` AS `changed`, + `post-user`.`post-type` AS `post-type`, + `post-user`.`post-reason` AS `post-reason`, + `post-user`.`private` AS `private`, + `post-thread-user`.`pubmail` AS `pubmail`, + `post-thread-user`.`ignored` AS `ignored`, + `post-user`.`visible` AS `visible`, + `post-thread-user`.`starred` AS `starred`, + `post-thread-user`.`unseen` AS `unseen`, + `post-user`.`deleted` AS `deleted`, + `post-thread-user`.`origin` AS `origin`, + `post-thread-user`.`mention` AS `mention`, + `post-user`.`global` AS `global`, + EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread-user`.`uri-id`) AS `featured`, + `post-thread-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, + `post-user`.`vid` AS `vid`, + `post-thread-user`.`psid` AS `psid`, + IF (`post-user`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, + `post-content`.`title` AS `title`, + `post-content`.`content-warning` AS `content-warning`, + `post-content`.`raw-body` AS `raw-body`, + `post-content`.`body` AS `body`, + `post-content`.`rendered-hash` AS `rendered-hash`, + `post-content`.`rendered-html` AS `rendered-html`, + `post-content`.`language` AS `language`, + `post-content`.`plink` AS `plink`, + `post-content`.`location` AS `location`, + `post-content`.`coord` AS `coord`, + `post-content`.`sensitive` AS `sensitive`, + `post-user`.`restrictions` AS `restrictions`, + `post-content`.`app` AS `app`, + `post-content`.`object-type` AS `object-type`, + `post-content`.`object` AS `object`, + `post-content`.`target-type` AS `target-type`, + `post-content`.`target` AS `target`, + `post-content`.`resource-id` AS `resource-id`, + `post-thread-user`.`contact-id` AS `contact-id`, + `contact`.`uri-id` AS `contact-uri-id`, + `contact`.`url` AS `contact-link`, + `contact`.`addr` AS `contact-addr`, + `contact`.`name` AS `contact-name`, + `contact`.`nick` AS `contact-nick`, + `contact`.`thumb` AS `contact-avatar`, + `contact`.`network` AS `contact-network`, + `contact`.`blocked` AS `contact-blocked`, + `contact`.`hidden` AS `contact-hidden`, + `contact`.`readonly` AS `contact-readonly`, + `contact`.`archive` AS `contact-archive`, + `contact`.`pending` AS `contact-pending`, + `contact`.`rel` AS `contact-rel`, + `contact`.`uid` AS `contact-uid`, + `contact`.`gsid` AS `contact-gsid`, + `contact`.`contact-type` AS `contact-contact-type`, + IF (`post-user`.`network` IN ('apub', 'dfrn', 'dspr', 'stat'), true, `contact`.`writable`) AS `writable`, + `contact`.`self` AS `self`, + `contact`.`id` AS `cid`, + `contact`.`alias` AS `alias`, + `contact`.`photo` AS `photo`, + `contact`.`name-date` AS `name-date`, + `contact`.`uri-date` AS `uri-date`, + `contact`.`avatar-date` AS `avatar-date`, + `contact`.`thumb` AS `thumb`, + `post-thread-user`.`author-id` AS `author-id`, + `author`.`uri-id` AS `author-uri-id`, + `author`.`url` AS `author-link`, + `author`.`addr` AS `author-addr`, + IF (`contact`.`url` = `author`.`url` AND `contact`.`name` != '', `contact`.`name`, `author`.`name`) AS `author-name`, + `author`.`nick` AS `author-nick`, + `author`.`alias` AS `author-alias`, + IF (`contact`.`url` = `author`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `author`.`thumb`) AS `author-avatar`, + `author`.`network` AS `author-network`, + `author`.`blocked` AS `author-blocked`, + `author`.`hidden` AS `author-hidden`, + `author`.`updated` AS `author-updated`, + `author`.`contact-type` AS `author-contact-type`, + `author`.`gsid` AS `author-gsid`, + `post-thread-user`.`owner-id` AS `owner-id`, + `owner`.`uri-id` AS `owner-uri-id`, + `owner`.`url` AS `owner-link`, + `owner`.`addr` AS `owner-addr`, + IF (`contact`.`url` = `owner`.`url` AND `contact`.`name` != '', `contact`.`name`, `owner`.`name`) AS `owner-name`, + `owner`.`nick` AS `owner-nick`, + `owner`.`alias` AS `owner-alias`, + IF (`contact`.`url` = `owner`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `owner`.`thumb`) AS `owner-avatar`, + `owner`.`network` AS `owner-network`, + `owner`.`blocked` AS `owner-blocked`, + `owner`.`hidden` AS `owner-hidden`, + `owner`.`updated` AS `owner-updated`, + `owner`.`gsid` AS `owner-gsid`, + `owner`.`contact-type` AS `owner-contact-type`, + `post-thread-user`.`causer-id` AS `causer-id`, + `causer`.`uri-id` AS `causer-uri-id`, + `causer`.`url` AS `causer-link`, + `causer`.`addr` AS `causer-addr`, + `causer`.`name` AS `causer-name`, + `causer`.`nick` AS `causer-nick`, + `causer`.`alias` AS `causer-alias`, + `causer`.`thumb` AS `causer-avatar`, + `causer`.`network` AS `causer-network`, + `causer`.`blocked` AS `causer-blocked`, + `causer`.`hidden` AS `causer-hidden`, + `causer`.`gsid` AS `causer-gsid`, + `causer`.`contact-type` AS `causer-contact-type`, + `post-delivery-data`.`postopts` AS `postopts`, + `post-delivery-data`.`inform` AS `inform`, + `post-delivery-data`.`queue_count` AS `delivery_queue_count`, + `post-delivery-data`.`queue_done` AS `delivery_queue_done`, + `post-delivery-data`.`queue_failed` AS `delivery_queue_failed`, + IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`allow_cid`) AS `allow_cid`, + IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`allow_gid`) AS `allow_gid`, + IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`deny_cid`) AS `deny_cid`, + IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`deny_gid`) AS `deny_gid`, + `post-user`.`event-id` AS `event-id`, + `event`.`created` AS `event-created`, + `event`.`edited` AS `event-edited`, + `event`.`start` AS `event-start`, + `event`.`finish` AS `event-finish`, + `event`.`summary` AS `event-summary`, + `event`.`desc` AS `event-desc`, + `event`.`location` AS `event-location`, + `event`.`type` AS `event-type`, + `event`.`nofinish` AS `event-nofinish`, + `event`.`ignore` AS `event-ignore`, + `post-question`.`id` AS `question-id`, + `post-question`.`multiple` AS `question-multiple`, + `post-question`.`voters` AS `question-voters`, + `post-question`.`end-time` AS `question-end-time`, + EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-thread-user`.`uri-id` AND `post-category`.`uid` = `post-thread-user`.`uid`) AS `has-categories`, + EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-thread-user`.`uri-id`) AS `has-media`, + `diaspora-interaction`.`interaction` AS `signed_text`, + `parent-item-uri`.`guid` AS `parent-guid`, + `post-thread-user`.`network` AS `parent-network`, + `post-thread-user`.`owner-id` AS `parent-owner-id`, `post-thread-user`.`author-id` AS `parent-author-id`, `author`.`url` AS `parent-author-link`, `author`.`name` AS `parent-author-name`, @@ -2411,7 +3024,9 @@ CREATE VIEW `post-thread-user-view` AS SELECT LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread-user`.`uri-id` @@ -2434,12 +3049,16 @@ CREATE VIEW `post-view` AS SELECT `post`.`thr-parent-id` AS `thr-parent-id`, `conversation-item-uri`.`uri` AS `conversation`, `post-thread`.`conversation-id` AS `conversation-id`, + `context-item-uri`.`uri` AS `context`, + `post-thread`.`context-id` AS `context-id`, `quote-item-uri`.`uri` AS `quote-uri`, `post-content`.`quote-uri-id` AS `quote-uri-id`, `item-uri`.`guid` AS `guid`, `post`.`gravity` AS `gravity`, `external-item-uri`.`uri` AS `extid`, `post`.`external-id` AS `external-id`, + `replies-item-uri`.`uri` AS `replies`, + `post`.`replies-id` AS `replies-id`, `post`.`created` AS `created`, `post`.`edited` AS `edited`, `post-thread`.`commented` AS `commented`, @@ -2452,6 +3071,7 @@ CREATE VIEW `post-view` AS SELECT `post`.`global` AS `global`, EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post`.`uri-id`) AS `featured`, `post`.`network` AS `network`, + 255 AS `protocol`, `post`.`vid` AS `vid`, IF (`post`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, `post-content`.`title` AS `title`, @@ -2464,6 +3084,7 @@ CREATE VIEW `post-view` AS SELECT `post-content`.`plink` AS `plink`, `post-content`.`location` AS `location`, `post-content`.`coord` AS `coord`, + `post-content`.`sensitive` AS `sensitive`, `post-content`.`app` AS `app`, `post-content`.`object-type` AS `object-type`, `post-content`.`object` AS `object`, @@ -2545,6 +3166,7 @@ CREATE VIEW `post-view` AS SELECT `diaspora-interaction`.`interaction` AS `signed_text`, `parent-item-uri`.`guid` AS `parent-guid`, `post-thread`.`network` AS `parent-network`, + `post-thread`.`owner-id` AS `parent-owner-id`, `post-thread`.`author-id` AS `parent-author-id`, `parent-post-author`.`url` AS `parent-author-link`, `parent-post-author`.`name` AS `parent-author-name`, @@ -2559,7 +3181,9 @@ CREATE VIEW `post-view` AS SELECT LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post`.`replies-id` LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post`.`uri-id` LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post`.`uri-id` @@ -2580,12 +3204,16 @@ CREATE VIEW `post-thread-view` AS SELECT `post`.`thr-parent-id` AS `thr-parent-id`, `conversation-item-uri`.`uri` AS `conversation`, `post-thread`.`conversation-id` AS `conversation-id`, + `context-item-uri`.`uri` AS `context`, + `post-thread`.`context-id` AS `context-id`, `quote-item-uri`.`uri` AS `quote-uri`, `post-content`.`quote-uri-id` AS `quote-uri-id`, `item-uri`.`guid` AS `guid`, `post`.`gravity` AS `gravity`, `external-item-uri`.`uri` AS `extid`, `post`.`external-id` AS `external-id`, + `replies-item-uri`.`uri` AS `replies`, + `post`.`replies-id` AS `replies-id`, `post-thread`.`created` AS `created`, `post`.`edited` AS `edited`, `post-thread`.`commented` AS `commented`, @@ -2598,6 +3226,7 @@ CREATE VIEW `post-thread-view` AS SELECT `post`.`global` AS `global`, EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread`.`uri-id`) AS `featured`, `post-thread`.`network` AS `network`, + 255 AS `protocol`, `post`.`vid` AS `vid`, IF (`post`.`vid` IS NULL, '', `verb`.`name`) AS `verb`, `post-content`.`title` AS `title`, @@ -2610,6 +3239,7 @@ CREATE VIEW `post-thread-view` AS SELECT `post-content`.`plink` AS `plink`, `post-content`.`location` AS `location`, `post-content`.`coord` AS `coord`, + `post-content`.`sensitive` AS `sensitive`, `post-content`.`app` AS `app`, `post-content`.`object-type` AS `object-type`, `post-content`.`object` AS `object`, @@ -2693,6 +3323,7 @@ CREATE VIEW `post-thread-view` AS SELECT `diaspora-interaction`.`interaction` AS `signed_text`, `parent-item-uri`.`guid` AS `parent-guid`, `post-thread`.`network` AS `parent-network`, + `post-thread`.`owner-id` AS `parent-owner-id`, `post-thread`.`author-id` AS `parent-author-id`, `author`.`url` AS `parent-author-link`, `author`.`name` AS `parent-author-name`, @@ -2707,7 +3338,9 @@ CREATE VIEW `post-thread-view` AS SELECT LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post`.`replies-id` LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread`.`uri-id` LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-thread`.`uri-id` @@ -2783,36 +3416,6 @@ CREATE VIEW `tag-view` AS SELECT LEFT JOIN `tag` ON `post-tag`.`tid` = `tag`.`id` LEFT JOIN `contact` ON `post-tag`.`cid` = `contact`.`id`; --- --- VIEW network-item-view --- -DROP VIEW IF EXISTS `network-item-view`; -CREATE VIEW `network-item-view` AS SELECT - `post-user`.`uri-id` AS `uri-id`, - `post-thread-user`.`post-user-id` AS `parent`, - `post-user`.`received` AS `received`, - `post-thread-user`.`commented` AS `commented`, - `post-user`.`created` AS `created`, - `post-user`.`uid` AS `uid`, - `post-thread-user`.`starred` AS `starred`, - `post-thread-user`.`mention` AS `mention`, - `post-user`.`network` AS `network`, - `post-user`.`unseen` AS `unseen`, - `post-user`.`gravity` AS `gravity`, - `post-user`.`contact-id` AS `contact-id`, - `ownercontact`.`contact-type` AS `contact-type` - FROM `post-user` - INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` - STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` - STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` - STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` - WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` - AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) - AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`) - AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` - AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`)) - AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); - -- -- VIEW network-thread-view -- @@ -2827,6 +3430,7 @@ CREATE VIEW `network-thread-view` AS SELECT `post-thread-user`.`starred` AS `starred`, `post-thread-user`.`mention` AS `mention`, `post-thread-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, `post-thread-user`.`contact-id` AS `contact-id`, `ownercontact`.`contact-type` AS `contact-type` FROM `post-thread-user` @@ -2838,8 +3442,37 @@ CREATE VIEW `network-thread-view` AS SELECT AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` - AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`)) - AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`post-thread-user`.`author-id`, `post-thread-user`.`owner-id`, `post-thread-user`.`causer-id`) AND (`blocked` OR `ignored` OR `is-blocked` OR `channel-only`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); + +-- +-- VIEW network-thread-circle-view +-- +DROP VIEW IF EXISTS `network-thread-circle-view`; +CREATE VIEW `network-thread-circle-view` AS SELECT + `post-thread-user`.`uri-id` AS `uri-id`, + `post-thread-user`.`post-user-id` AS `parent`, + `post-thread-user`.`received` AS `received`, + `post-thread-user`.`commented` AS `commented`, + `post-thread-user`.`created` AS `created`, + `post-thread-user`.`uid` AS `uid`, + `post-thread-user`.`starred` AS `starred`, + `post-thread-user`.`mention` AS `mention`, + `post-thread-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, + `post-thread-user`.`contact-id` AS `contact-id`, + `ownercontact`.`contact-type` AS `contact-type` + FROM `post-thread-user` + INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` + WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` + AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) + AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) + AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`post-thread-user`.`author-id`, `post-thread-user`.`owner-id`, `post-thread-user`.`causer-id`) AND (`blocked` OR `ignored` OR `is-blocked`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`); -- -- VIEW owner-view @@ -2881,7 +3514,6 @@ CREATE VIEW `owner-view` AS SELECT `contact`.`poll` AS `poll`, `contact`.`confirm` AS `confirm`, `contact`.`poco` AS `poco`, - `contact`.`subhub` AS `subhub`, `contact`.`hub-verify` AS `hub-verify`, `contact`.`last-update` AS `last-update`, `contact`.`success_update` AS `success_update`, @@ -2908,6 +3540,7 @@ CREATE VIEW `owner-view` AS SELECT `contact`.`unsearchable` AS `unsearchable`, `contact`.`sensitive` AS `sensitive`, `contact`.`baseurl` AS `baseurl`, + `contact`.`gsid` AS `gsid`, `contact`.`reason` AS `reason`, `contact`.`info` AS `info`, `contact`.`bdyear` AS `bdyear`, @@ -2930,14 +3563,10 @@ CREATE VIEW `owner-view` AS SELECT `user`.`theme` AS `theme`, `user`.`pubkey` AS `upubkey`, `user`.`prvkey` AS `uprvkey`, - `user`.`sprvkey` AS `sprvkey`, - `user`.`spubkey` AS `spubkey`, `user`.`verified` AS `verified`, `user`.`blockwall` AS `blockwall`, `user`.`hidewall` AS `hidewall`, `user`.`blocktags` AS `blocktags`, - `user`.`unkmail` AS `unkmail`, - `user`.`cntunkmail` AS `cntunkmail`, `user`.`notify-flags` AS `notify-flags`, `user`.`page-flags` AS `page-flags`, `user`.`account-type` AS `account-type`, @@ -3044,6 +3673,7 @@ CREATE VIEW `account-view` AS SELECT `apcontact`.`outbox` AS `ap-outbox`, `apcontact`.`sharedinbox` AS `ap-sharedinbox`, `apcontact`.`generator` AS `ap-generator`, + `apcontact`.`posting-restricted` AS `ap-posting-restricted`, `apcontact`.`following_count` AS `ap-following_count`, `apcontact`.`followers_count` AS `ap-followers_count`, `apcontact`.`statuses_count` AS `ap-statuses_count`, @@ -3129,7 +3759,6 @@ CREATE VIEW `account-user-view` AS SELECT `ucontact`.`readonly` AS `readonly`, `ucontact`.`blocked` AS `blocked`, `ucontact`.`block_reason` AS `block_reason`, - `ucontact`.`subhub` AS `subhub`, `ucontact`.`hub-verify` AS `hub-verify`, `ucontact`.`reason` AS `reason`, `contact`.`notify` AS `dfrn-notify`, @@ -3150,6 +3779,7 @@ CREATE VIEW `account-user-view` AS SELECT `apcontact`.`outbox` AS `ap-outbox`, `apcontact`.`sharedinbox` AS `ap-sharedinbox`, `apcontact`.`generator` AS `ap-generator`, + `apcontact`.`posting-restricted` AS `ap-posting-restricted`, `apcontact`.`following_count` AS `ap-following_count`, `apcontact`.`followers_count` AS `ap-followers_count`, `apcontact`.`statuses_count` AS `ap-statuses_count`, @@ -3185,7 +3815,8 @@ CREATE VIEW `pending-view` AS SELECT `contact`.`nick` AS `nick` FROM `register` INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid` - INNER JOIN `user` ON `register`.`uid` = `user`.`uid`; + INNER JOIN `user` ON `register`.`uid` = `user`.`uid` + WHERE `register`.`uid` != 0; -- -- VIEW tag-search-view @@ -3202,6 +3833,7 @@ CREATE VIEW `tag-search-view` AS SELECT `post-user`.`gravity` AS `gravity`, `post-user`.`received` AS `received`, `post-user`.`network` AS `network`, + `post-user`.`protocol` AS `protocol`, `post-user`.`author-id` AS `author-id`, `tag`.`name` AS `name` FROM `post-tag` diff --git a/doc/API-Mastodon.md b/doc/API-Mastodon.md index 2798446ba9..2cf661611b 100644 --- a/doc/API-Mastodon.md +++ b/doc/API-Mastodon.md @@ -11,21 +11,7 @@ Authentication is the same as described in [Using the APIs](help/api#Authenticat ## Clients -### Supported apps - -For supported apps please have a look at the [FAQ](help/FAQ#clients) - -### Unsupported apps - -#### Android - -- [Fedilab](https://framagit.org/tom79/fedilab) Automatically uses the legacy API, see issue: https://framagit.org/tom79/fedilab/-/issues/520 -- [Mammut](https://github.com/jamiesanson/Mammut) There are problems with the token request, see issue https://github.com/jamiesanson/Mammut/issues/19 - -#### iOS - -- [Mast](https://github.com/Beesitech/Mast) Doesn't accept the entered instance name. Claims that it is invalid (Message is: "Not a valid instance (may be closed or dead)") -- [Toot!](https://apps.apple.com/app/toot/id1229021451) +Please find a list of supported apps at [FAQ](help/FAQ#clients). ## Entities @@ -170,7 +156,8 @@ Example: - [`GET /api/v1/followed_tags`](https://docs.joinmastodon.org/methods/followed_tags/) - [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance/#v1) -- `GET /api/v1/instance/rules` Undocumented, returns Terms of Service +- [`GET /api/v1/instance/extended_description`](https://docs.joinmastodon.org/methods/instance/#extended_description) +- [`GET /api/v1/instance/rules`](https://docs.joinmastodon.org/methods/instance/#rules) - [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains) - [`GET /api/v1/lists`](https://docs.joinmastodon.org/methods/timelines/lists/) - [`POST /api/v1/lists`](https://docs.joinmastodon.org/methods/timelines/lists/) @@ -314,7 +301,6 @@ They refer to features or data that don't exist in Friendica yet. - [`PUT /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/) - [`DELETE /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/) - [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity) -- [`POST /api/v1/markers`](https://docs.joinmastodon.org/methods/timelines/markers/) - [`PUT /api/v1/scheduled_statuses/:id`](https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/) - [`GET /api/v1/statuses/{id:\d+}/history`](https://github.com/mastodon/mastodon/pull/16697) - [`GET /api/v1/streaming`](https://docs.joinmastodon.org/methods/timelines/streaming/) diff --git a/doc/Accesskeys.md b/doc/Accesskeys.md index 7f85f37ef1..ed0c55f7f5 100644 --- a/doc/Accesskeys.md +++ b/doc/Accesskeys.md @@ -34,6 +34,7 @@ General * y - for you * f - followers * r - sharers of sharers +* q - quiet sharers * h - what's hot * i - Images * v - Videos diff --git a/doc/AddonStorageBackend.md b/doc/AddonStorageBackend.md index c3a6d1639c..5053b641cb 100644 --- a/doc/AddonStorageBackend.md +++ b/doc/AddonStorageBackend.md @@ -126,14 +126,14 @@ Override the two necessary instances: ```php use Friendica\Core\Storage\Capability\ICanWriteToStorage; -abstract class StorageTest +abstract class StorageTest { // returns an instance of your newly created storage class abstract protected function getInstance(); // Assertion for the option array you return for your new StorageClass abstract protected function assertOption(ICanWriteToStorage $storage); -} +} ``` ## Exception handling @@ -158,7 +158,7 @@ Example: ```php use Friendica\Core\Storage\Capability\ICanWriteToStorage; -class ExampleStorage implements ICanWriteToStorage +class ExampleStorage implements ICanWriteToStorage { public function get(string $reference) : string { @@ -168,7 +168,7 @@ class ExampleStorage implements ICanWriteToStorage throw new \Friendica\Core\Storage\Exception\StorageException(sprintf('The Example Storage throws an exception for reference %s', $reference), 500, $exception); } } -} +} ``` ## Example @@ -200,11 +200,11 @@ class SampleStorageBackend implements ICanWriteToStorage /** * SampleStorageBackend constructor. - * + * * You can add here every dynamic class as dependency you like and add them to a private field - * Friendica automatically creates these classes and passes them as argument to the constructor + * Friendica automatically creates these classes and passes them as argument to the constructor */ - public function __construct(string $filename) + public function __construct(string $filename) { $this->filename = $filename; } @@ -215,7 +215,7 @@ class SampleStorageBackend implements ICanWriteToStorage // a config key return file_get_contents($this->filename); } - + public function put(string $data, string $reference = '') { if ($reference === '') { @@ -224,13 +224,13 @@ class SampleStorageBackend implements ICanWriteToStorage // we don't save $data ! return $reference; } - + public function delete(string $reference) { // we pretend to delete the data return true; } - + public function __toString() { return self::NAME; @@ -261,11 +261,11 @@ class SampleStorageBackendConfig implements ICanConfigureStorage /** * SampleStorageBackendConfig constructor. - * + * * You can add here every dynamic class as dependency you like and add them to a private field - * Friendica automatically creates these classes and passes them as argument to the constructor + * Friendica automatically creates these classes and passes them as argument to the constructor */ - public function __construct(IManageConfigValues $config, L10n $l10n) + public function __construct(IManageConfigValues $config, L10n $l10n) { $this->config = $config; $this->l10n = $l10n; @@ -289,12 +289,12 @@ class SampleStorageBackendConfig implements ICanConfigureStorage ], ]; } - + public function saveOptions(array $data) { // the keys in $data are the same keys we defined in getOptions() $newfilename = trim($data['filename']); - + // this function should always validate the data. // in this example we check if file exists if (!file_exists($newfilename)) { @@ -302,9 +302,9 @@ class SampleStorageBackendConfig implements ICanConfigureStorage // ['optionname' => 'error message'] return ['filename' => 'The file doesn\'t exists']; } - + $this->config->set('storage', 'samplestorage', $newfilename); - + // no errors, return empty array return []; } @@ -341,13 +341,13 @@ function samplestorage_storage_uninstall() DI::storageManager()->unregister(SampleStorageBackend::class); } -function samplestorage_storage_instance(App $a, array &$data) +function samplestorage_storage_instance(AppHelper $appHelper, array &$data) { $config = new SampleStorageBackendConfig(DI::l10n(), DI::config()); $data['storage'] = new SampleStorageBackendConfig($config->getFileName()); } -function samplestorage_storage_config(App $a, array &$data) +function samplestorage_storage_config(AppHelper $appHelper, array &$data) { $data['storage_config'] = new SampleStorageBackendConfig(DI::l10n(), DI::config()); } @@ -360,7 +360,7 @@ function samplestorage_storage_config(App $a, array &$data) use Friendica\Core\Storage\Capability\ICanWriteToStorage; use Friendica\Test\src\Core\Storage\StorageTest; -class SampleStorageTest extends StorageTest +class SampleStorageTest extends StorageTest { // returns an instance of your newly created storage class protected function getInstance() @@ -382,5 +382,5 @@ class SampleStorageTest extends StorageTest ], ], $storage->getOptions()); } -} +} ``` diff --git a/doc/Addons.md b/doc/Addons.md index b89a48d26d..7b7e97e701 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -604,7 +604,7 @@ Hook data: ### follow -Called before adding a new contact for a user to handle non-native network remote contact (like Twitter). +Called before adding a new contact for a user to handle non-native network remote contact (like Bluesky). Hook data: @@ -613,7 +613,7 @@ Hook data: ### unfollow -Called when unfollowing a remote contact on a non-native network (like Twitter) +Called when unfollowing a remote contact on a non-native network (like Bluesky) Hook data: - **contact** (input): the target public contact (uid = 0) array. @@ -622,7 +622,7 @@ Hook data: ### revoke_follow -Called when making a remote contact on a non-native network (like Twitter) unfollow you. +Called when making a remote contact on a non-native network (like Bluesky) unfollow you. Hook data: - **contact** (input): the target public contact (uid = 0) array. @@ -631,7 +631,7 @@ Hook data: ### block -Called when blocking a remote contact on a non-native network (like Twitter). +Called when blocking a remote contact on a non-native network (like Bluesky). Hook data: - **contact** (input): the remote contact (uid = 0) array. @@ -640,7 +640,7 @@ Hook data: ### unblock -Called when unblocking a remote contact on a non-native network (like Twitter). +Called when unblocking a remote contact on a non-native network (like Bluesky). Hook data: - **contact** (input): the remote contact (uid = 0) array. @@ -661,7 +661,7 @@ Called when a custom storage is used (e.g. webdav_storage) Hook data: - **name** (input): the name of the used storage backend -- **data['storage']** (output): the storage instance to use (**must** implement `\Friendica\Core\Storage\IWritableStorage`) +- **data['storage']** (output): the storage instance to use (**must** implement `\Friendica\Core\Storage\IWritableStorage`) ### storage_config @@ -850,10 +850,6 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep- Hook::callAll('register_account', $uid); Hook::callAll('remove_user', $user); -### src/Module/Notifications/Ping.php - - Hook::callAll('network_ping', $arr); - ### src/Module/PermissionTooltip.php Hook::callAll('lockview_content', $item); @@ -880,7 +876,7 @@ Here is a complete list of all hook callbacks with file locations (as of 24-Sep- ### src/Content/ContactBlock.php - Hook::callAll('contact_block_end', $arr); + Hook::callAll('contact_block_end', $text); ### src/Content/Text/BBCode.php diff --git a/doc/BBCode.md b/doc/BBCode.md index ea6fa8e2b7..9fb10d8e5f 100644 --- a/doc/BBCode.md +++ b/doc/BBCode.md @@ -376,8 +376,8 @@ code   [li] Second list element
[/ul]
[list]
-  [*] First list element
-  [*] Second list element
+  [li] First list element
+  [li] Second list element
[/list]
    @@ -388,12 +388,12 @@ code [ol]
    -  [*] First list element
    -  [*] Second list element
    +  [li] First list element
    +  [li] Second list element
    [/ol]
    [list=1]
    -  [*] First list element
    -  [*] Second list element
    +  [li] First list element
    +  [li] Second list element
    [/list]
      @@ -404,8 +404,8 @@ code [list=]
      -  [*] First list element
      -  [*] Second list element
      +  [li] First list element
      +  [li] Second list element
      [/list]
        @@ -416,8 +416,8 @@ code [list=i]
        -  [*] First list element
        -  [*] Second list element
        +  [li] First list element
        +  [li] Second list element
        [/list]
          @@ -428,8 +428,8 @@ code [list=I]
          -  [*] First list element
          -  [*] Second list element
          +  [li] First list element
          +  [li] Second list element
          [/list]
            @@ -440,8 +440,8 @@ code [list=a]
            -  [*] First list element
            -  [*] Second list element
            +  [li] First list element
            +  [li] Second list element
            [/list]
              @@ -452,8 +452,8 @@ code [list=A]
              -  [*] First list element
              -  [*] Second list element
              +  [li] First list element
              +  [li] Second list element
              [/list]
                diff --git a/doc/Channels.md b/doc/Channels.md index de3f6718d8..63d2582170 100644 --- a/doc/Channels.md +++ b/doc/Channels.md @@ -23,12 +23,17 @@ Predefined Channels * For you: Posts from contacts you interact with and who interact with you. In detail, it consists of: * Posts from people you interact with on a more than average level. - * Posts from the accounts that you follow with a more than average number of interactions- + * Posts from the accounts that you follow with a more than average number of interactions. * Posts from accounts where you activated "notify on new posts" or where you have set the channel frequency accordingly. +* Discover: Posts from contacts you don't follow, but that might be of interest for you to follow. In detail, it consists of: + * Posts from people you don't follow but you interact with on a more than average level. + * Posts from people you don't follow but that interact with you on a more than average level. + * Popular posts from people you don't follow but you interacted with or who interacted with you on any level. * What's Hot: Posts with a more than average number of interactions. * Language: Posts in your language. * Followers: Posts from your followers that you don't follow. * Sharers of sharers: Posts from accounts that are followed by accounts that you follow. +* Quiet sharers: Posts from accounts that you follow but who don't post very often. * Images: Posts with images. * Audio: Posts with audio. * Videos: Posts with videos. @@ -43,43 +48,51 @@ Each channel is defined by these values: * Label: This value is mandatory and is used for the menu label. * Description: A short description of the content. This can help to keep the overview, when you have got a lot of channels. * Access Key: When you want to access this channel via an access key, you can define it here. Pay attention to not use an already used one. -* Circle: This defines the data source for this channel. By default it is set to the public timeline. There are some predefined values, like the accounts that you follow or the accounts that follow you. Also all of your circles can be selected. +* Circle: This defines the data source for this channel. By default it is set to the public timeline. There are some predefined values, like the accounts that you follow or the accounts that follow you. Also all of your circles can be selected. * Include Tags: Comma separated list of tags. A post will be used when it contains any of the listed tags. -* Exclude Tags: Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel. +* Exclude Tags: Comma separated list of tags. If a post contain any of these tags, then it will not be part of this channel. * Full Text Search: This can be used to include or exclude content, based on the content and some additional keywords. It uses the "boolean mode" operators from MariaDB: https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode -* Images, Videos, Audio: When selected, you will see content with the selected media type. This can be combined. If none of these fields are checked, you will see any content, with or without attacked media. +* Images, Videos, Audio: When selected, you will see content with the selected media type. This can be combined. If none of these fields are checked, you will see any content, with or without attached media. Additional keywords for the full text search --- -Additionally to the search for content, there are additional keywords that can be used in the full text search: +Additionally to the search for content, there are keywords that can be used in the full text search. +Alternatives are presented with "|". * from - Use "from:nickname" or "from:nickname@domain.tld" to search for posts from a specific author. -* to - Use "from:nickname" or "from:nickname@domain.tld" to search for posts with the given contact as receiver. -* group - Use "from:nickname" or "from:nickname@domain.tld" to search for group post of the given group. +* to - Use "to:nickname" or "to:nickname@domain.tld" to search for posts with the given contact as receiver. +* group - Use "group:nickname" or "group:nickname@domain.tld" to search for posts of the given group. +* application | relay - Use "application:nickname" or "application:nickname@domain.tld" to search for posts that had been reshared by the given relay application. * server - Use "server:hostname" to search for posts from a specific server. In the case of group postings, the search text contains both the hostname of the group server and the author's hostname. * source - The ActivityPub type of the post source. Use this for example to include or exclude group posts or posts from services (aka bots). * source:person - The post is created by a regular user account. * source:organization - The post is created by an organisation. * source:group - The post is created by or distributed via a group. - * source:service - The posts originates from a service account. This source type is often used to mark bot accounts. - * source:application - The post is created by an application. This is most likely unused in the fediverse for post creation. + * source:service | source:news - The posts originates from a service account. This source type is often used to mark bot accounts. + * source:application | source:relay - The post is created by an application. This is most likely unused in the fediverse for post creation. * tag - Use "tag:tagname" to search for a specific tag. -* network - Use this to include or exclude some networks from your channel. - * network:apub - ActivityPub (Used by the systems in the Fediverse) - * network:dfrn - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub. - * network:dspr - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo. +* media - With this keyword you can search for attached media. + * media:image | media:photo | media:picture - The post contains an image + * media:video - The post contains a video + * media:audio - The post contains audio + * media:card - The post contains a link preview card + * media:post - The post links another post, means it is a quoted post +* network | net - Use this to include or exclude some networks from your channel. + * network:apub | network:activitypub - ActivityPub (Used by the systems in the Fediverse) + * network:dfrn | network:friendica - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub. + * network:dspr | network:diaspora - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo. * network:feed - RSS/Atom feeds * network:mail - Mails that had been imported via IMAP. - * network:stat - The OStatus protocol is mainly used by old GNU Social installations. - * network:dscs - Posts that are received by the Discourse connector. - * network:tmbl - Posts that are received by the Tumblr connector. - * network:bsky - Posts that are received by the Bluesky connector. + * network:dscs | network:discourse - Posts that are received by the Discourse connector. + * network:tmbl | network:tumblr - Posts that are received by the Tumblr connector. + * network:bsky | network:bluesky - Posts that are received by the Bluesky connector. * platform - Use this to include or exclude some platforms from your channel, e.g. "+platform:friendica". In the case of group postings, the search text contains both the platform of the group server and the author's platform. * visibility - You have the choice between different visibilities. You can only see unlisted or private posts that you have the access for. * visibility:public * visibility:unlisted * visibility:private +* language | lang - Use "language:code" to search for posts with the given language in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. -Remember that you can combine these kerywords. -So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn" \ No newline at end of file +Remember that you can combine these keywords. +So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn" diff --git a/doc/Circles-and-Privacy.md b/doc/Circles-and-Privacy.md index d701173c07..abec72f15a 100644 --- a/doc/Circles-and-Privacy.md +++ b/doc/Circles-and-Privacy.md @@ -95,14 +95,6 @@ However we take privacy seriously and don't behave like some networks that __pre Your profile and "wall" may also be visited by your friends from other networks, and you can block access to these by web visitors that Friendica doesn't know. Be aware that this could include some of your friends on other networks. -This may produce undesired results when posting a long status message to (for instance) Twitter. -When Friendica sends a post to these networks which exceeds the service length limit, we truncate it and provide a link to the original. -The original is a link back to your Friendica profile. -As Friendica cannot prove who they are, it may not be possible for these people to view your post in full. - -For people in this situation we would recommend providing a "Twitter-length" summary, with more detail for friends that can see the post in full. -You can do so by including the BBCode tag *abstract* in your posting. - Blocking your profile or entire Friendica site from unknown web visitors also has serious implications for communicating with GNU Social members. These networks communicate with others via public protocols that are not authenticated. In order to view your posts, these networks have to access them as an "unknown web visitor". diff --git a/doc/Config.md b/doc/Config.md index 3e0459f9c8..afd4e65277 100644 --- a/doc/Config.md +++ b/doc/Config.md @@ -43,7 +43,7 @@ Some examples of common known configuration files: Addons can define their own default configuration values in `addon/[addon]/config/[addon].config.php` which is loaded when the addon is activated. If needed, an alternative `config` path can be used by using the `FRIENDICA_CONFIG_DIR` environment variable (full path required!). -This is useful in case of hardening the system by separating configuration from program binaries. +This is useful in case of hardening the system by separating configuration from program binaries. ### Static Configuration location @@ -160,16 +160,6 @@ $a->config['register_policy'] = REGISTER_CLOSED;
                -$a->path = "value";
                -
                -
                -'system' => [
                -	'urlpath' => 'value',
                -],
                -
                - - -
                 $default_timezone = "value";
                 
                @@ -313,7 +303,7 @@ Enabling the admin panel for an account, and thus making the account holder admi
                     'config' => [
                         'admin_email' => 'someone@example.com',
                     ]
                -    
                +
                 
                 Where you have to match the email address used for the account with the one you enter to the `config/local.config.php` file.
                 If more than one account should be able to access the admin panel, separate the email addresses with a comma.
                diff --git a/doc/Connectors.md b/doc/Connectors.md
                index 992e83bba9..77d44e909f 100644
                --- a/doc/Connectors.md
                +++ b/doc/Connectors.md
                @@ -4,15 +4,10 @@ Connectors
                 * [Home](help)
                 
                 Connectors allow you to connect with external social networks and services.
                -They are only required for posting to existing accounts on Twitter or GNU Social.
                +They are only required for posting to existing accounts on for example Bluesky, Tumblr or Twitter.
                +For Bluesky and Tumblr you can also enable a bidirectional synchronisation, so that you can use Friendica to read your timeline from Tumblr or Bluesky.
                 There is also a connector for accessing your email INBOX.
                 
                -If the following network connectors are installed on your system, select the following links to visit the appropriate settings page and configure them for your account:
                -
                -* [Twitter](/settings/addons)
                -* [GNU Social](/settings/addons)
                -* [Email](/settings)
                -
                 Instructions For Connecting To People On Specific Services
                 ==========================================================
                 
                @@ -28,23 +23,6 @@ Diaspora
                 
                 Add the Diaspora 'handle' to the 'Connect/Follow' text box on your [Contacts](contacts) page. 
                 
                -
                -GNU Social
                ----
                -
                -This is described as the "federated social web" or OStatus contacts. 
                -
                -Please note that there are **no** privacy provisions on the OStatus network.
                -Any message which is delivered to **any** OStatus member is visible to anybody in the world and will negate any privacy settings that you have in effect.
                -These messages will also turn up in public searches. 
                -
                -Since OStatus communications do not use authentication, if you select the profile privacy option to hide your profile and messages from unknown viewers, OStatus members will **not** be able to receive your communications. 
                -
                -To connect with an OStatus member insert their profile URL or Identity address into the Connect box on your [Contacts](contacts) page.
                -
                -The GNU Social connector may be used if you wish posts to appear on an OStatus site using an existing OStatus account. 
                -It is not necessary to do this, as you may 'follow' OStatus members from Friendica and they may follow you (by placing their own Identity Address into your 'Connect' page).
                -
                 Blogger, Wordpress, RSS feeds, arbitrary web pages
                 ---
                 
                @@ -54,14 +32,6 @@ PLease note that you will not be able to reply to these contacts.
                 This feed reader feature will allow you to _connect_ with millions of pages on the internet.
                 All that the pages need to have is a discoverable feed using either the RSS or Atom syndication format, and which provides an author name and a site image in a form which we can extract. 
                 
                -Twitter
                ----
                -
                -To follow a Twitter member, the Twitter-Connector (Addon) needs to be configured on your node.
                -If this is the case put the URL of the Twitter member's main page into the Connect box on your [Contacts](contacts) page.
                -To reply, you must have the Twitter connector installed, and reply using your own status editor.
                -Begin the message with @twitterperson replacing with the Twitter username.
                -
                 Email
                 ---
                 
                diff --git a/doc/Developers-Intro.md b/doc/Developers-Intro.md
                index c500b27741..5dbf1def4a 100644
                --- a/doc/Developers-Intro.md
                +++ b/doc/Developers-Intro.md
                @@ -62,7 +62,7 @@ If you want to have git automatically update the dependencies with composer, you
                     }
                     # `composer install` if the `composer.lock` file gets changed
                     # to update all the php dependencies
                -    check_run composer.lock "bin/composer.phar install --no-dev"
                +    check_run composer.lock "bin/composer.phar install"
                 
                 just place it into `.git/hooks/post-merge` and make it executable.
                 
                @@ -156,3 +156,98 @@ If you are interested in improving those clients, please contact the developers
                 * iOS: *currently no client*
                 * SailfishOS: **Friendiy** [src](https://kirgroup.com/projects/fabrixxm/harbour-friendly) - developed by [Fabio](https://kirgroup.com/profile/fabrixxm/profile)
                 * Windows: **Friendica Mobile** for Windows versions [before 8.1](http://windowsphone.com/s?appid=e3257730-c9cf-4935-9620-5261e3505c67) and [Windows 10](https://www.microsoft.com/store/apps/9nblggh0fhmn) - developed by [Gerhard Seeber](http://mozartweg.dyndns.org/friendica/profile/gerhard/profile)
                +
                +## Backward compatibility
                +
                +### Backward Compatibility Promise
                +
                +Friendica can be extended by addons.
                +These addons relies on many classes and conventions from Friendica.
                +As developers our work on Friendica should not break things in the addons without giving the addon maintainers a chance to fix their addons.
                +Our goal is to build trust for the addon maintainers but also allow Friendica developers to move on.
                +This is called the Backward Compatibility Promise.
                +
                +Inspired by the [Symonfy BC promise](https://symfony.com/doc/current/contributing/code/bc.html) we promise BC for every class, interface, trait, enum, function, constant, etc., but with the exception of:
                +
                +- Classes, interfaces, traits, enums, functions, methods, properties and constants marked as `@internal` or `@private`
                +- Extending or modifying any non-abstract class or method in any way
                +- Extending or modifying a `final` class or method in any way
                +- Calling `private` methods (via Reflection)
                +- Accessing `private` properties (via Reflection)
                +- Accessing `private` methods (via Reflection)
                +- Accessing `private` constants (via Reflection)
                +- New properties on overridden `protected` methods
                +- Possible name collisions with new methods in an extended class (addon developers should prefix their custom methods in the extending classes in an appropriate way)
                +- Dropping support for every PHP version that has reached end of life
                +
                +### Deprecation and removing features
                +
                +As the development goes by Friendica needs to get rid of old code and concepts.
                +This will be done in 3 steps to give addon maintainers a chance to adjust their addons.
                +
                +**1. Label deprecation**
                +
                +If we as the Friendica maintainers decide to remove some functions, classes, interface, etc. we start this by adding a `@deprecated` PHPDoc note on the code.
                +For instance the class `Friendica\Core\Logger` should be removed, so we add the following note with a possible replacement:
                +
                +```php
                +/**
                + * Logger functions
                + *
                + * @deprecated 2025.02 Use constructor injection or `DI::logger()` instead
                + */
                +class Logger {/* ... */}
                +```
                +
                +This way addon developers might be notified early by their IDE or other tools that the usage of the class is deprecated.
                +In Friendica we can now start to replace all occurrences and usage of this class with the alternative.
                +
                +The deprecation label COULD be remain over multiple releases.
                +As long as the code that is labeled with `@deprecated` is used inside Friendica or the official addon repository, it SHOULD NOT be hard deprecated.
                +
                +**2. Hard deprecation**
                +
                +If the deprecated code is no longer used inside Friendica or the official addons it MUST be hard deprecated.
                +The code MUST NOT be deleted.
                +Starting from the next release, it MUST be stay for at least 5 months.
                +Hard deprecated code COULD remain longer than 5 months, depending on when a release appears.
                +Addon developer SHOULD NOT consider that they have more than 5 months to adjust their code.
                +
                +Hard deprecation code means that the code triggers a muted `E_USER_DEPRECATION` error if it is called.
                +For instance with the deprecated class `Friendica\Core\Logger` the call of every method should trigger an error:
                +
                +```php
                +/**
                + * Logger functions
                + *
                + * @deprecated 2025.02 Use constructor injection or `DI::logger()` instead
                + */
                +class Logger {
                +	public static function info(string $message, array $context = [])
                +	{
                +		@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.05 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
                +
                +		self::getInstance()->info($message, $context);
                +	}
                +
                +	/* ... */
                +}
                +```
                +
                +This way the maintainer or users of addons will be notified in the logs that the addon will stop working in one of the next releases.
                +The addon maintainer now has at least 5 months or at least one release to fix the deprecations.
                +
                +Please note that the deprecation message contains the release that will be released next.
                +In the example the code was hard deprecated with release `2025.05`, so it COULD be removed earliest with the `2025.11` release.
                +
                +**3. Code Removing**
                +
                +We promise BC for deprecated code for at least 5 months, starting from the release the deprecation was announced.
                +After this time the deprecated code COULD be remove within the next release.
                +
                +Breaking changes MUST be happen only in a new release but MUST be hard deprecated first.
                +The BC promise refers only to releases, respective the `stable` branch.
                +Deprecated code on other branches like `develop` or RC branches could be removed earlier.
                +This is not a BC break as long as the release will be published 5 months after the hard deprecation.
                +
                +If a release breaks BC without deprecation or earlier than 5 months, this SHOULD considered as a bug and BC SHOULD be restored in a bugfix release.
                diff --git a/doc/Export-Import-Contacts.md b/doc/Export-Import-Contacts.md
                index 95b33f380c..824c9e2ebb 100644
                --- a/doc/Export-Import-Contacts.md
                +++ b/doc/Export-Import-Contacts.md
                @@ -11,8 +11,7 @@ To export the list of accounts that you follow, go to the [Settings Export perso
                 
                 ## Import of followed Contacts
                 
                -To import contacts from a CSV file, go to the [Settings page](settings).
                -At the bottom of the *account settings* page you'll find the *import contacts* section.
                +To import contacts from a CSV file, go to the [import contacts](settings/importcontacts).
                 Upload the CSV file there.
                 
                 ### Supported File Format
                diff --git a/doc/FAQ.md b/doc/FAQ.md
                index ebb8c041b5..4e8d9b19eb 100644
                --- a/doc/FAQ.md
                +++ b/doc/FAQ.md
                @@ -103,6 +103,9 @@ When a certain language is forced, the language remains until session is closed.
                 
                 ### How do blocked, ignored, archived and hidden contacts behave?
                 
                +These are various categories of contacts that are restricted in some way.
                +Many of these types are related to [Safety](help/Safety).
                +
                 ##### Blocked
                 
                 Direct communication will be blocked.
                @@ -126,7 +129,7 @@ However, unlike blocking, existing posts this person made before being archived
                 
                 ##### Hidden
                 
                -Contact not be displayed in your public friend list.
                +The contact not be displayed in your public friend list.
                 However, a hidden contact will appear normally in conversations and this may expose their hidden status to anybody who can see the conversation.
                 
                 
                @@ -174,16 +177,19 @@ Example: Friendica Support
                 Friendica supports [Mastodon API](help/API-Mastodon) and [Twitter API | gnusocial](help/api).
                 This means you can use some of the Mastodon and Twitter clients for Friendica.
                 The available features are client specific and may differ.
                +Clients dedicated to Friendica are marked in **bold**.
                 
                 #### Android
                 
                 * [AndStatus](http://andstatus.org) ([F-Droid](https://f-droid.org/repository/browse/?fdid=org.andstatus.app), [Google Play](https://play.google.com/store/apps/details?id=org.andstatus.app))
                -* [Fedi](https://github.com/Big-Fig/Fediverse.app) ([Google Play](https://play.google.com/store/apps/details?id=com.fediverse.app))
                 * [Fedilab](https://fedilab.app) ([F-Droid](https://f-droid.org/app/fr.gouv.etalab.mastodon), [Google Play](https://play.google.com/store/apps/details?id=app.fedilab.android))
                -* [Friendiqa](https://git.friendi.ca/lubuwest/Friendiqa) ([F-Droid](https://git.friendi.ca/lubuwest/Friendiqa#install), [Google Play](https://play.google.com/store/apps/details?id=org.qtproject.friendiqa))
                -* [Husky](https://git.sr.ht/~captainepoch/husky) ([F-Droid](https://f-droid.org/repository/browse/?fdid=su.xash.husky), [Google Play](https://play.google.com/store/apps/details?id=su.xash.husky))
                +* **[Friendiqa](https://git.friendi.ca/lubuwest/Friendiqa)** ([F-Droid](https://git.friendi.ca/lubuwest/Friendiqa#install), [Google Play](https://play.google.com/store/apps/details?id=org.qtproject.friendiqa))
                +* [Husky](https://codeberg.org/husky/husky) ([F-Droid](https://f-droid.org/repository/browse/?fdid=su.xash.husky), [Google Play](https://play.google.com/store/apps/details?id=su.xash.husky))
                 * [Mastodon](https://github.com/mastodon/mastodon-android) ([F-Droid](https://f-droid.org/en/packages/org.joinmastodon.android/), [Google Play](https://play.google.com/store/apps/details?id=org.joinmastodon.android))
                -* [Subway Tooter](https://github.com/tateisu/SubwayTooter) ([F-Droid](https://android.izzysoft.de/repo/apk/jp.juggler.subwaytooter))
                +* [Pachli](https://pachli.app/) ([F-Droid](https://f-droid.org/en/packages/app.pachli/), [Google Play](https://play.google.com/store/apps/details?id=app.pachli))
                +* **[Raccoon for Friendica](https://github.com/LiveFastEatTrashRaccoon/RaccoonForFriendica)** ([F-Droid](https://f-droid.org/packages/com.livefast.eattrash.raccoonforfriendica),  [Google Play](https://play.google.com/apps/testing/com.livefast.eattrash.raccoonforfriendica))
                +* **[Relatica](https://gitlab.com/mysocialportal/relatica)**
                +* [Subway Tooter](https://github.com/tateisu/SubwayTooter) ([F-Droid via Izzy](https://android.izzysoft.de/repo/apk/jp.juggler.subwaytooter.noFcm))
                 * [Tooot](https://tooot.app/) ([Google Play](https://play.google.com/store/apps/details?id=com.xmflsct.app.tooot))
                 * [Tusky](https://tusky.app) ([F-Droid](https://f-droid.org/repository/browse/?fdid=com.keylesspalace.tusky), [Google Play](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky))
                 * [TwidereX](https://github.com/TwidereProject/TwidereX-Android) ([F-Droid](https://f-droid.org/en/packages/com.twidere.twiderex/), [Google Play](https://play.google.com/store/apps/details?id=com.twidere.twiderex))
                @@ -192,26 +198,30 @@ The available features are client specific and may differ.
                 #### iOS
                 
                 * [Mastodon](https://joinmastodon.org/apps) ([App Store](https://apps.apple.com/us/app/mastodon-for-iphone/id1571998974))
                +* **[Relatica](https://gitlab.com/mysocialportal/relatica)**
                 * [Stella*](https://www.stella-app.net/) ([App Store](https://apps.apple.com/us/app/stella-for-mastodon-twitter/id921372048))
                -* [Tooot](https://github.com/tooot-app) ([App Store](https://apps.apple.com/app/id1549772269)
                +* [Tooot](https://github.com/tooot-app) ([App Store](https://apps.apple.com/app/id1549772269))
                 * [TwidereX](https://github.com/TwidereProject/TwidereX-iOS) ([App Store](https://apps.apple.com/app/twidere-x/id1530314034))
                 
                 #### Linux
                 
                 * [Choqok](https://choqok.kde.org)
                -* [Whalebird](https://whalebird.social/en/desktop/contents) ([GitHub](https://github.com/h3poteto/whalebird-desktop))
                +* **[Relatica](https://gitlab.com/mysocialportal/relatica)**
                 * [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk))
                 * [Toot](https://toot.readthedocs.io/en/latest/)
                +* [Whalebird](https://whalebird.social/en/desktop/contents) ([GitHub](https://github.com/h3poteto/whalebird-desktop))
                 
                 #### macOS
                 
                +* **[Relatica](https://gitlab.com/mysocialportal/relatica)**
                 * [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk))
                 * [Whalebird](https://whalebird.social/en/desktop/contents) ([App Store](https://apps.apple.com/de/app/whalebird/id1378283354), [GitHub](https://github.com/h3poteto/whalebird-desktop))
                 
                 #### Windows
                 
                +* **[Relatica](https://gitlab.com/mysocialportal/relatica)**
                 * [TheDesk](https://thedesk.top/en/) ([GitHub](https://github.com/cutls/TheDesk))
                -* [Whalebird](https://whalebird.social/en/desktop/contents) ([Website Download](https://whalebird.social/en/desktop/contents/downloads#windows), [GitHub](https://github.com/h3poteto/whalebird-desktop))
                +* [Whalebird](https://whalebird.social/en/desktop/contents) ([Microsoft Store](https://apps.microsoft.com/detail/9nbw4csdv5hc), [GitHub](https://github.com/h3poteto/whalebird-desktop))
                 
                 #### Web Frontend
                 
                diff --git a/doc/Home.md b/doc/Home.md
                index 33ed640746..b8b3517a45 100644
                --- a/doc/Home.md
                +++ b/doc/Home.md
                @@ -1,5 +1,5 @@
                -Friendica Documentation and Resources
                -=====================================
                +Help
                +====
                 
                 **User Manual**
                 
                @@ -14,6 +14,7 @@ Friendica Documentation and Resources
                 * You and other users
                 	* [Connectors](help/Connectors)
                 	* [Making Friends](help/Making-Friends)
                +	* [Safety](help/Safety)
                 	* [Circles and Privacy](help/Circles-and-Privacy)
                 	* [Tags and Mentions](help/Tags-and-Mentions)
                 	* [Community Groups](help/Groups)
                @@ -30,7 +31,7 @@ Friendica Documentation and Resources
                 * [Install](help/Install)
                 * [Update](help/Update)
                 * [Settings & Admin Panel](help/Settings)
                -* [Installing Connectors (Twitter/GNU Social)](help/Installing-Connectors)
                +* [Installing Connectors](help/Installing-Connectors)
                 * [Install an ejabberd server (XMPP chat) with synchronized credentials](help/install-ejabberd)
                 * [Using SSL with Friendica](help/SSL)
                 * [Config values that can only be set in config/local.config.php](help/Config)
                @@ -64,18 +65,18 @@ Friendica Documentation and Resources
                 	* [Database schema documentation](help/database)
                 	* [Class Autoloading](help/autoloader)
                 
                -**External Resources**
                +**Links**
                +
                +* Website: [https://friendi.ca](https://friendi.ca)
                +* Help Group: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
                +* XMPP: [support@forum.friendi.ca](xmpp:support@forum.friendi.ca?join)
                +* IRC: [https://web.libera.chat/?channels=#friendica](https://web.libera.chat/?channels=#friendica)
                +* Matrix: [https://matrix.to/#/#friendi.ca:matrix.org](https://matrix.to/#/#friendi.ca:matrix.org)
                +* Mailing List: [https://mailman.friendi.ca/mailman/listinfo/support-friendi.ca](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
                 
                -* [Main Website](https://friendi.ca)
                -* Ways to get Support
                -  * Friendica Support Group: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
                -  * [Mailing List Archive](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca) you can subscribe to the list by sending an email to ``support-request(at)friendi.ca?subject=subscribe``
                -  * Community chat rooms (the IRC, Matrix and XMPP rooms are bridged) these public chats are logged [from IRC](https://gnusociarg.nsupdate.info/2021/%23friendica/) and [Matrix](https://view.matrix.org/alias/%23friendi.ca:matrix.org/)
                -    * XMPP/Jabber MUC: support(at)forum.friendi.ca
                -    * IRC: #friendica at [libera.chat](https://web.libera.chat/?channels=#friendica)
                -    * Matrix: [#friendi.ca](https://matrix.to/#/#friendi.ca:matrix.org) or [#friendica-en](https://matrix.to/#/#friendica-en:matrix.org) at matrix.org
                 
                 **About**
                 
                -* [Site/Version Info](friendica)
                -* [Friendica Credits](credits)
                +* [Server Information](friendica)
                +* [Terms of Service](tos)
                +* [Credits](credits)
                diff --git a/doc/Improve-Performance.md b/doc/Improve-Performance.md
                index 727fa104be..9134cb6c74 100644
                --- a/doc/Improve-Performance.md
                +++ b/doc/Improve-Performance.md
                @@ -14,10 +14,6 @@ Please go to /admin/site/ on your system and change the following values:
                 
                 This value reduces the data that is send from the server to the client. 50 is a value that doesn't influences image quality too much.
                 
                -    Set "OStatus conversation completion interval" to "never".
                -
                -If you have many OStatus contacts then completing of conversations can take some time. Since you will miss several comments in OStatus threads, you maybe should consider the option "At post arrival" instead.
                -
                     Enable "Use MySQL full text engine"
                 
                 When using MyISAM (default) or InnoDB on MariaDB 10 this speeds up search.
                diff --git a/doc/Install.md b/doc/Install.md
                index 4715c27233..3c162d519b 100644
                --- a/doc/Install.md
                +++ b/doc/Install.md
                @@ -27,10 +27,10 @@ Due to the large variety of operating systems and PHP platforms in existence we
                 
                 ### Requirements
                 
                -* Apache with mod-rewrite enabled and "Options All" so you can use a local `.htaccess` file
                +* Apache with mod_rewrite enabled and "[AllowOverride All](https://httpd.apache.org/docs/2.4/mod/core.html#allowoverride)" so you can use a local `.htaccess` file
                 * PHP 7.4+
                   * PHP *command line* access with register_argc_argv set to true in the php.ini file
                -  * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar and OpenSSL extensions
                +  * Curl, GD, GMP, PDO, mbstring, MySQLi, xml, zip, IntlChar, IDN and OpenSSL extensions
                   * The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it)
                   * Some form of email server or email gateway such that PHP mail() works.
                     If you cannot set up your own email server, you can use the [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) addon and use a remote SMTP server.
                @@ -44,7 +44,7 @@ For alternative server configurations (such as Nginx server and MariaDB database
                 
                 ### Optional
                 
                -* PHP ImageMagick extension (php-imagick) for animated GIF support.
                +* PHP ImageMagick extension (php-imagick) for animated GIF and animated WebP support.
                 
                 ## Installation procedure
                 
                @@ -58,6 +58,7 @@ If this is nothing for you, you might be interested in the following:
                 * [Tutorial: Creating a Friendica Server with Ubuntu 22.04](https://nequalsonelifestyle.com/2022/07/30/creating-friendica-server-ubuntu/)
                   * [Setting Up Friendica Daemon as a Systemd Service Tutorial](https://nequalsonelifestyle.com/2022/08/04/setting-up-friendica-daemon-systemd-service/)
                 * [Setting up Friendica on Unraid](https://www.jenovarain.com/2023/03/setting-up-friendica-on-unraid/) (NAS)
                +* [Installing Friendica with Elastio](https://elest.io/open-source/friendica)
                 
                 ### Get Friendica
                 
                @@ -75,14 +76,6 @@ This makes the software much easier to update.
                 The Linux commands to clone the repository into a directory "mywebsite" would be
                 
                     git clone https://github.com/friendica/friendica.git -b stable mywebsite
                -    cd mywebsite
                -    bin/composer.phar install --no-dev
                -
                -Make sure the folder *view/smarty3* exists and is writable by the webserver user, in this case *www-data*
                -
                -    mkdir -p view/smarty3
                -    chown www-data:www-data view/smarty3
                -    chmod 775 view/smarty3
                 
                 Get the addons by going into your website folder.
                 
                @@ -90,12 +83,22 @@ Get the addons by going into your website folder.
                 
                 Clone the addon repository (separately):
                 
                -    git clone https://github.com/friendica/friendica-addons.git -b stable addon
                +	git clone https://github.com/friendica/friendica-addons.git -b stable addon
                +
                +Install the dependencies:
                +
                +    bin/composer.phar run install:prod
                +
                +Make sure the folder *view/smarty3* exists and is writable by the webserver user, in this case *www-data*
                +
                +    mkdir -p view/smarty3
                +    chown www-data:www-data view/smarty3
                +    chmod 775 view/smarty3
                 
                 If you want to use the development version of Friendica you can switch to the develop branch in the repository by running
                 
                     git checkout develop
                -    bin/composer.phar install
                +    bin/composer.phar run install:prod
                     cd addon
                     git checkout develop
                 
                @@ -215,7 +218,6 @@ All options will be saved in the `config/local.config.php` and are overruling th
                 -	`-U|--dbuser ` The username of the mysql/mariadb database login (env `MYSQL_USER` or `MYSQL_USERNAME`)
                 -	`-P|--dbpass ` The password of the mysql/mariadb database login (env `MYSQL_PASSWORD`)
                 -	`-d|--dbdata ` The name of the mysql/mariadb database (env `MYSQL_DATABASE`)
                --	`-u|--urlpath ` The URL path of Friendica - f.e. '/friendica' (env `FRIENDICA_URL_PATH`)
                 -	`-b|--phppath ` The path of the PHP binary (env `FRIENDICA_PHP_PATH`)
                 -	`-A|--admin ` The admin email address of Friendica (env `FRIENDICA_ADMIN_MAIL`)
                 -	`-T|--tz ` The timezone of Friendica (env `FRIENDICA_TZ`)
                @@ -267,7 +269,7 @@ You might wish to delete/rename `config/local.config.php` to another name and dr
                 Set up a cron job or scheduled task to run the worker once every 5-10 minutes in order to perform background processing.
                 Example:
                 
                -    cd /base/directory; /path/to/php bin/worker.php
                +    cd /base/directory; /path/to/php bin/console.php worker
                 
                 Change "/base/directory", and "/path/to/php" as appropriate for your situation.
                 
                @@ -276,7 +278,7 @@ Change "/base/directory", and "/path/to/php" as appropriate for your situation.
                 If you are using a Linux server, run "crontab -e" and add a line like the
                 one shown, substituting for your unique paths and settings:
                 
                -    */10 * * * * cd /home/myname/mywebsite; /usr/bin/php bin/worker.php
                +    */10 * * * * cd /home/myname/mywebsite; /usr/bin/php bin/console.php worker
                 
                 You can generally find the location of PHP by executing "which php".
                 If you run into trouble with this section please contact your hosting provider for assistance.
                @@ -289,11 +291,11 @@ Once you have installed Friendica and created an admin account as part of the pr
                 #### worker alternative: daemon
                 Otherwise, you’ll need to use the command line on your remote server and start the Friendica daemon (background task) using the following command:
                 
                -    cd /path/to/friendica; php bin/daemon.php start
                +    cd /path/to/friendica; php bin/console.php daemon start
                 
                 Once started, you can check the daemon status using the following command:
                 
                -    cd /path/to/friendica; php bin/daemon.php status
                +    cd /path/to/friendica; php bin/console.php daemon status
                 
                 After a server restart or any other failure, the daemon needs to be restarted.
                 This could be achieved by a cronjob.
                @@ -425,7 +427,7 @@ provided by one of our members.
                 >
                 > 	*/10 * * * * cd /var/www/friendica/friendica/ && sudo -u www-data /usr/bin/php \
                 >       -d suhosin.executor.func.blacklist=none \
                ->       -d suhosin.executor.eval.blacklist=none -f bin/worker.php
                +>       -d suhosin.executor.eval.blacklist=none -f bin/console.php
                 >
                 > This worked well for simple test cases, but the friendica-cron still failed
                 > with a fatal error:
                @@ -434,7 +436,7 @@ provided by one of our members.
                 >     (attacker 'REMOTE_ADDR not set', file '/var/www/friendica/friendica/boot.php',
                 >     line 1341)
                 >
                -> After a while I noticed, that `bin/worker.php` calls further PHP script via `proc_open`.
                +> After a while I noticed, that `bin/console.php worker` calls further PHP script via `proc_open`.
                 > These scripts themselves also use `proc_open` and fail, because they are NOT
                 > called with `-d suhosin.executor.func.blacklist=none`.
                 >
                diff --git a/doc/Installing-Connectors.md b/doc/Installing-Connectors.md
                index d63d2c2e1c..bd564eb466 100644
                --- a/doc/Installing-Connectors.md
                +++ b/doc/Installing-Connectors.md
                @@ -1,89 +1,34 @@
                -Installing Connectors (Twitter/GNU Social)
                +Installing Connectors
                 ==================================================
                 
                 * [Home](help)
                 
                +Friendica uses add-ons to connect to some networks, such as Tumblr or Bluesky.
                 
                -Friendica uses addons to provide connectivity to some networks, such as Twitter.
                +All of these add-ons require an account on the target network.
                +In addition, you (or usually the server administrator) will need to obtain an API key to allow authenticated access to your Friendica server.
                 
                -There is also a addon to post through to an existing account on a GNU Social service.
                -You only need this to post to an already existing GNU Social account, but not to communicate with GNU Social members in general.
                -
                -All three addons require an account on the target network.
                -In addition you (or typically the server administrator) will need to obtain an API key to provide authenticated access to your Friendica server.
                -
                -Site Configuration
                +Site configuration
                 ---
                 
                -Addons must be installed by the site administrator before they can be used.
                -This is accomplished through the site administration panel.
                +Addons need to be installed by the site administrator before they can be used.
                +This is done through the site administration panel.
                 
                -Each of the connectors also requires an "API key" from the service you wish to connect with.
                -Some addons allow you to enter this information in the site administration pages, while others may require you to edit your configuration file (config/local.config.php).
                -The ways to obtain these keys vary between the services, but they all require an existing account on the target service.
                -Once installed, these API keys can usually be shared by all site members.
                +Some of the connectors also require an "API key" from the service you wish to connect to.
                +For Tumblr, this information can be found in the site administration pages, while for Twitter (X) each user has to create their own API key.
                +Other connectors, such as Bluesky, don't require an API key at all.
                 
                -The details of configuring each service follow (much of this information comes directly from the addon source files):
                +You can find more information about specific requirements on each addon's settings page, either on the admin page or the user page.
                 
                -Twitter Addon for Friendica
                +Bluesky Jetstream
                 ---
                 
                -* Author: Tobias Diekershoff
                -* tobias.diekershoff@gmx.net
                -* License: 3-clause BSD license
                +To further improve connectivity to Bluesky, Admins can choose to enable 'Jetstream' connectivity.
                +Jetstream is a service that connects to the Bluesky firehose.
                +With Jetstream, messages arrive in real time rather than having to be polled.
                +It also enables real-time processing of blocks or tracking activities performed by the user via the Bluesky website or application.
                 
                -### Configuration
                -To use this addon you need a OAuth Consumer key pair (key & secret).
                -You can get it from [Twitter](https://twitter.com/apps).
                +To enable Jetstream processing, run `bin/console.php jetstream' from the command line.
                +You will need to define the process id file in local.config.php in the 'jetstream' section using the key 'pidfile'.
                 
                -Register your Friendica site as "Client" application with "Read & Write" access.
                -We do not need "Twitter as login".
                -When you've registered the app you get a key pair with an OAuth Consumer key and a secret key for your application/site.
                -Add this key pair to your config/local.config.php:
                -
                -	[twitter]
                -	consumerkey = your consumer_key here
                -	consumersecret = your consumer_secret here
                -
                -After this, your users can configure their Twitter account settings from "Settings -> Connector Settings".
                -
                -### More documentation
                -
                -Find the author's documentation here: [http://diekershoff.homeunix.net/redmine/wiki/friendikaplugin/Twitter_Plugin](http://diekershoff.homeunix.net/redmine/wiki/friendikaplugin/Twitter_Plugin)
                -
                -
                -GNU Social Addon for Friendica
                ----
                -
                -* Author: Tobias Diekershoff
                -* tobias.diekershoff@gmx.net
                -* License: 3-clause BSD license
                -
                -### Configuration
                -
                -When the addon is activated the user has to acquire the following in order to connect to the GNU Social account of choice.
                -
                -* The base URL for the GNU Social API, for quitter.se this is https://quitter.se/api/
                -* OAuth Consumer key & secret
                -
                -To get the OAuth Consumer key pair the user has to
                -
                -1 ask her Friendica admin if a pair already exists or
                -2 has to register the Friendica server as a client application on the GNU Social server.
                -
                -This can be done from the account settings under "Settings -> Connections -> Register an OAuth client application -> Register a new application" on the GNU Social server.
                -
                -During the registration of the OAuth client remember the following:
                -
                -* Application names must be unique on the GNU Social site, so we recommend a Name of 'friendica-nnnn', replace 'nnnn' with a random number or your website name.
                -* there is no callback url
                -* register a desktop client
                -* with read & write access
                -* the Source URL should be the URL of your Friendica server
                -
                -After the required credentials for the application are stored in the configuration you have to actually connect your Friendica account with GNU Social.
                -This is done from the Settings -> Connector Settings page.
                -Follow the Sign in with GNU Social button, allow access and then copy the security code into the box provided.
                -Friendica will then try to acquire the final OAuth credentials from the API.
                -
                -If successful, the addon settings will allow you to select to post your public messages to your GNU Social account (have a look behind the little lock symbol beneath the status "editor" on your Home or Network pages).
                +To keep track of the messages processed and the drift (the time difference between the date of the message and the date the system processed that message), some fields are added to the statistics endpoint.
                diff --git a/doc/Making-Friends.md b/doc/Making-Friends.md
                index d928995865..85d4044cef 100644
                --- a/doc/Making-Friends.md
                +++ b/doc/Making-Friends.md
                @@ -98,14 +98,19 @@ As a friend, you can both communicate with each other.
                 
                 diaspora* uses a different terminology, and you are given the option of allowing them to "share with you", or being full friends.
                 
                -Ignoring, blocking and deleting contacts
                +Deleting
                 ---
                -Once you have become friends, if you find the person constantly sends you spam or worthless information, you can "Ignore" them - without breaking off the friendship or even alerting them to the fact that you aren't interested in anything they are saying.
                -In many ways they are like a "follower" - but they don't know this.
                -They think they are a friend.
                 
                -You can also "block" a person.
                -This completely blocks communications with that person.
                -They may still be able to see your public posts, as can anybody in the world, but they cannot communicate with you directly.
                +You can delete a friend no matter what the friendship status - which completely removes everything relating to that person from your website.
                 
                -You can also delete a friend no matter what the friendship status - which completely removes everything relating to that person from your website.
                +Unwanted Behaviour
                +---
                +
                +If a contact engages in abuse, harrassment, or other unwanted behaviour, there are various actions you can take.
                +These include:
                +
                +* Reporting them to your administrator, optionally also their administrator
                +* Ignoring the contact, so that you will not see their posts
                +* Blocking the contact from receiving your posts
                +
                +For more information, see [Safety](help/Safety).
                diff --git a/doc/Migrate.md b/doc/Migrate.md
                index e116d029f6..ca0e0d0d95 100644
                --- a/doc/Migrate.md
                +++ b/doc/Migrate.md
                @@ -55,9 +55,9 @@ You should see an output like this:
                 
                 Finally, you may also want to optimise your database with the following command: ``mysqloptimize -p friendica-db``
                 
                -### Going offline 
                +### Going offline
                 Stop background tasks and put your server in maintenance mode.
                -1.  If you had set up a worker cron job like this ``*/10 * * * * cd /var/www/friendica; /usr/bin/php bin/worker.php`` run ``crontab -e`` and comment out this line. Alternatively if you deploy a worker daemon, disable this instead.
                +1.  If you had set up a worker cron job like this ``*/10 * * * * cd /var/www/friendica; /usr/bin/php bin/console.php worker`` run ``crontab -e`` and comment out this line. Alternatively if you deploy a worker daemon, disable this instead.
                 2.  Put your server into maintenance mode: ``bin/console maintenance 1 "We are currently upgrading our system and will be back soon."``
                 
                 ## Dumping DB
                @@ -73,12 +73,12 @@ Import your database on your new server: ``mysql -p friendica_db < your-friendic
                 
                 ### Configuration file
                 Copy your old server's configuration file to ``config/local.config.php``.
                -Ensure the newly created database credentials are identical to the setting in the configuration file; otherwise update them accordingly. 
                +Ensure the newly created database credentials are identical to the setting in the configuration file; otherwise update them accordingly.
                 
                 ### Cron job for worker
                 Set up the required daily cron job.
                 Run ``crontab -e`` and add the following line according to your system specification
                -``*/10 * * * * cd /var/www/friendica; /usr/bin/php bin/worker.php`` 
                +``*/10 * * * * cd /var/www/friendica; /usr/bin/php bin/console.php worker``
                 
                 ### DNS settings
                 Adjust your DNS records by pointing them to your new server.
                diff --git a/doc/Move-Account.md b/doc/Move-Account.md
                index 2983184be6..9e13d7cad8 100644
                --- a/doc/Move-Account.md
                +++ b/doc/Move-Account.md
                @@ -21,11 +21,6 @@ Friendica will recreate your account on the new server, with your contacts and c
                 A message is sent to Friendica contacts, to inform them about your move:
                 If your contacts are running on an updated server, your details on their side will be automatically updated.
                 
                -GNU Social contacts
                ----
                -Contacts on GNU Social will be archived, as we can't inform them about your move.
                -You should ask them to remove your contact from their lists and re-add you, and you should do the same with their contact.
                -
                 Diaspora contacts
                 ---
                 Newer Diaspora servers are able to process "account migration" messages.
                diff --git a/doc/Protocol.md b/doc/Protocol.md
                index 85fe09a5f1..af0002472c 100644
                --- a/doc/Protocol.md
                +++ b/doc/Protocol.md
                @@ -20,23 +20,9 @@ Additional types are used for non standard activities.
                 * [Link to the specification](http://activitystrea.ms/head/activity-schema.html)
                 * [List of used ActivityStreams verbs and object types.](https://github.com/friendica/friendica/wiki/ActivityStreams)
                 
                -Salmon
                ----
                -
                -Salmon is used as a message exchange protocol for replies and mentions.
                -
                -* [Link to the protocol summary](http://www.salmon-protocol.org/salmon-protocol-summary)
                -
                 Portable Contacts
                 ---
                 
                 Portable Contacts is used for friends lists.
                 
                 * [Link to the specification](https://web.archive.org/web/20160426223008/http://portablecontacts.net/draft-spec.html) (Link to archive.org)
                -
                -pubsubhubbub
                ----
                -
                -pubsubhubbub is used for OStatus.
                -
                -* [Link to the specification](https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html)
                diff --git a/doc/Quick-Start-makingnewfriends.md b/doc/Quick-Start-makingnewfriends.md
                index 39edd19b41..d642905698 100644
                --- a/doc/Quick-Start-makingnewfriends.md
                +++ b/doc/Quick-Start-makingnewfriends.md
                @@ -1,5 +1,5 @@
                 This is your Suggested Friends page.
                -If you get lost, you can click this link to bring yourself back here.
                +If you get lost, you can click this link to bring yourself back here.
                 
                 This is a bit like the Friend Suggestions page of Facebook.
                 Everybody on this list has agreed that they may be suggested as a friend.
                @@ -16,6 +16,6 @@ Click the link at the top of this page to go back to the suggested friends list
                 Feel uncomfortable adding people you don't know?
                 Don't worry - that's where Groups and Pages come in!
                 
                -
                +
                 
                 
                diff --git a/doc/Safety.md b/doc/Safety.md
                new file mode 100644
                index 0000000000..4ecd927e87
                --- /dev/null
                +++ b/doc/Safety.md
                @@ -0,0 +1,73 @@
                +Safety
                +======
                +
                +* [Home](help)
                +
                +Each Friendica instance is linked together with a global network of other servers, some running Friendica and some running other software.
                +These servers support a diverse global community of millions of users.
                +Inevitably, some of these users are malicious.
                +Friendica provides several features to keep you safe from abuse and harrassment.
                +
                +Terms of Service
                +---
                +
                +Each instance is entitled to define its own Terms of Service, also often called a Code of Conduct.
                +These terms include rules for behaviour.
                +These Terms of Service are visible when creating an account.
                +By creating an account on a server, you agree to be bound by these Terms of Service.
                +Users who violate the terms of service of their instance may face sanctions including being suspended or banned from their instance.
                +
                +Remember that conversations frequently involve participants from different instances with different rules.
                +Participants are only bound by the rules of the server where they have an account, not by the rules of your server.
                +Terms of Service may even be completely incompatible.
                +
                +Reporting
                +---
                +
                +Your local administrators are responsible for ensuring a safe online environment for all users on your server.
                +They rely on reports from users to highlight behaviour that puts other users at risk.
                +If you see problematic behaviour from a user, whether they are from another server or are local to your server, you can report that behaviour to your administrator.
                +You can also choose to send that report to their administrator at the same time.
                +You will be given an opportunity to select the type of problem you are seeing.
                +Your local administrator then has the option of blocking that user for all users on your local instance.
                +If a remote server is a constant source of abuse and their administrators are unable or unwilling to control their users behaviour, your administrator can even block the entire remote server.
                +
                +Ignoring
                +---
                +
                +Once you have become friends, if you find the person constantly sends you spam or worthless information, you can "Ignore" them - without breaking off the friendship or even alerting them to the fact that you aren't interested in anything they are saying.
                +In many ways they are like a "follower" - but they don't know this.
                +They think they are a friend.
                +Your own posts will still be delivered to them.
                +Other people will see their replies to you normally.
                +
                +Some servers are frequent sources of abusive or other unwanted behaviour.
                +For this reason you can also choose to ignore entire servers.
                +Users on that server can still follow you as normal.
                +
                +Blocking
                +---
                +
                +You can also "block" a person.
                +This completely blocks communications with that person.
                +Your public comments will no longer be sent to them.  Their own public posts will not be visible in your stream.
                +If they participate in other conversations you are following, their replies will still be part of the thread.
                +However, their replies will be collapsed by default, and only visible if you open the reply.
                +Other people will see their replies normally.
                +
                +A blocked contact will still believe that they are following you.
                +If they delete you as a contact and then add you again, they will return to apparently following you but in fact still being blocked.
                +You will not be notified when this happens.
                +
                +When you block a person, your server will notify their server that the block has occurred.
                +This is so that if, for example, Alice and Bob follow you from the same server, and you want to block Alice but not Bob, their server can decide which accounts should see your post.
                +This does mean that the administrator of their server can see that you have applied the block, and may inform the blocked person.
                +In some cases this could lead to retaliation.
                +There are several other ways someone can determine that you have blocked them, and see your public posts despite the block.
                +For example, they can simply log out and view your posts.
                +
                +Archiving
                +---
                +
                +Archiving is similar to blocking.
                +However, existing posts this person made before being archived will be visible in your stream.
                diff --git a/doc/Settings.md b/doc/Settings.md
                index 00c8be2c2e..7ca0e22286 100644
                --- a/doc/Settings.md
                +++ b/doc/Settings.md
                @@ -419,7 +419,7 @@ We strongly discourage you from doing so, as this will break federation to other
                 Say you have a subdirectory for tests and put Friendica into a further subdirectory, the config would be:
                 
                 	'system' => [
                -		'urlpath' => 'tests/friendica',
                +		'url' => 'https://example.com/tests/friendica',
                 	],
                 
                 ## Other exceptions
                diff --git a/doc/StrategyHooks.md b/doc/StrategyHooks.md
                index 2960ceeaad..440728783c 100644
                --- a/doc/StrategyHooks.md
                +++ b/doc/StrategyHooks.md
                @@ -83,6 +83,8 @@ return [
                 
                 ## Addons
                 
                +> ⚠️ Since Friendica 2025.02 the strategy hooks for addons are deprecated, please use PHP hooks instead.
                +
                 The hook logic is useful for decoupling the Friendica core logic, but its primary goal is to modularize Friendica in creating addons.
                 
                 Therefor you can either use the interfaces directly as shown above, or you can place your own `hooks.config.php` file inside a `static` directory directly under your addon core directory.
                diff --git a/doc/Update.md b/doc/Update.md
                index 40de14082c..b6c5c53e19 100644
                --- a/doc/Update.md
                +++ b/doc/Update.md
                @@ -14,7 +14,7 @@ If you installed Friendica in the ``path/to/friendica`` folder:
                    * ``.htaccess`` if using Apache web server
                 
                     The following items only need to be copied if they are located inside your friendica path:
                -   * your storage folder as set in **Admin -> Site -> File Upload -> Storage base path** 
                +   * your storage folder as set in **Admin -> Site -> File Upload -> Storage base path**
                    * your item cache as set in **Admin -> Site -> Performance -> Path to item cache**
                    * your temp folder as set in **Admin -> Site -> Advanced -> Temp path**
                 3. Rename the ``path/to/friendica`` folder to ``path/to/friendica_old``.
                @@ -30,7 +30,7 @@ You can get the latest changes at any time with
                 
                     cd path/to/friendica
                     git pull
                -    bin/composer.phar install --no-dev
                +    bin/composer.phar run install:prod
                 
                 The addon tree has to be updated separately like so:
                 
                @@ -89,7 +89,7 @@ Some of the updates include the use of foreign keys now that will bump into issu
                 ```
                 Error 1452 occurred during database update:
                 Cannot add or update a child row: a foreign key constraint fails (`friendica`.`#sql-10ea6_5a6d`, CONSTRAINT `#sql-10ea6_5a6d_ibfk_1` FOREIGN KEY (`contact-id`) REFERENCES `contact` (`id`))
                -ALTER TABLE `thread` ADD FOREIGN KEY (`iid`) REFERENCES `item` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE; 
                +ALTER TABLE `thread` ADD FOREIGN KEY (`iid`) REFERENCES `item` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE;
                 ```
                 
                 All current known fixes for possible items that can go wrong are as below.
                diff --git a/doc/Vagrant.md b/doc/Vagrant.md
                index 16088cd37e..29a2871bd8 100644
                --- a/doc/Vagrant.md
                +++ b/doc/Vagrant.md
                @@ -18,7 +18,7 @@ What you need to do:
                 Please use an up-to-date vagrant version from https://www.vagrantup.com/downloads.html.
                 2. Git clone your Friendica repository.
                 Inside, you'll find a `Vagrantfile` and some scripts in the `bin/dev` folder.
                -Pull the PHP requirements with `bin/composer install`.
                +Pull the PHP requirements with `bin/composer.phar install`.
                 3. Run `vagrant up` from inside the friendica clone.
                 This will start the virtual machine.
                 Be patient: When it runs for the first time, it downloads a Debian Server image and installs Friendica.
                @@ -60,7 +60,7 @@ Trouble Shooting
                 If you see a version mis-match for the _VirtualBox Guest Additions_ between host and guest during the initial setup of the Vagrant VM, you will need to install an addon to Vagrant (ref. [Stack Overflow](https://stackoverflow.com/a/38010683)).
                 Stop the Vagrant VM and run the following command:
                 
                -	$> vagrant plugin install vagrant-vbguest 
                +	$> vagrant plugin install vagrant-vbguest
                 
                 On the next Vagrant up, the version problem should be fixed.
                 
                diff --git a/doc/autoloader.md b/doc/autoloader.md
                index 954c28813c..5bc0bfe9b3 100644
                --- a/doc/autoloader.md
                +++ b/doc/autoloader.md
                @@ -46,9 +46,9 @@ The code will be something like:
                 // mod/network.php
                 getAll();
                 
                diff --git a/doc/database.md b/doc/database.md
                index 26519ccfb4..4036b7ec14 100644
                --- a/doc/database.md
                +++ b/doc/database.md
                @@ -61,14 +61,17 @@ Database Tables
                 | [post-category](help/database/db_post-category) | post relation to categories |
                 | [post-collection](help/database/db_post-collection) | Collection of posts |
                 | [post-content](help/database/db_post-content) | Content for all posts |
                +| [post-counts](help/database/db_post-counts) | Original remote activity |
                 | [post-delivery](help/database/db_post-delivery) | Delivery data for posts for the batch processing |
                 | [post-delivery-data](help/database/db_post-delivery-data) | Delivery data for items |
                 | [post-engagement](help/database/db_post-engagement) | Engagement data per post |
                 | [post-history](help/database/db_post-history) | Post history |
                 | [post-link](help/database/db_post-link) | Post related external links |
                 | [post-media](help/database/db_post-media) | Attached media |
                +| [post-origin](help/database/db_post-origin) | Posts from local users |
                 | [post-question](help/database/db_post-question) | Question |
                 | [post-question-option](help/database/db_post-question-option) | Question option |
                +| [post-searchindex](help/database/db_post-searchindex) | Content for all posts |
                 | [post-tag](help/database/db_post-tag) | post relation to tags |
                 | [post-thread](help/database/db_post-thread) | Thread related data |
                 | [post-thread-user](help/database/db_post-thread-user) | Thread related data per user |
                @@ -77,7 +80,6 @@ Database Tables
                 | [process](help/database/db_process) | Currently running system processes |
                 | [profile](help/database/db_profile) | user profiles data |
                 | [profile_field](help/database/db_profile_field) | Custom profile fields |
                -| [push_subscriber](help/database/db_push_subscriber) | Used for OStatus: Contains feed subscribers |
                 | [register](help/database/db_register) | registrations requiring admin approval |
                 | [report](help/database/db_report) |  |
                 | [report-post](help/database/db_report-post) | Individual posts attached to a moderation report |
                diff --git a/doc/database/db_apcontact.md b/doc/database/db_apcontact.md
                index 1632378ea8..902d41ae6b 100644
                --- a/doc/database/db_apcontact.md
                +++ b/doc/database/db_apcontact.md
                @@ -6,40 +6,41 @@ ActivityPub compatible contacts - used in the ActivityPub implementation
                 Fields
                 ------
                 
                -| Field            | Description                                                         | Type           | Null | Key | Default             | Extra |
                -| ---------------- | ------------------------------------------------------------------- | -------------- | ---- | --- | ------------------- | ----- |
                -| url              | URL of the contact                                                  | varbinary(383) | NO   | PRI | NULL                |       |
                -| uri-id           | Id of the item-uri table entry that contains the apcontact url      | int unsigned   | YES  |     | NULL                |       |
                -| uuid             |                                                                     | varbinary(255) | YES  |     | NULL                |       |
                -| type             |                                                                     | varchar(20)    | NO   |     | NULL                |       |
                -| following        |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| followers        |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| inbox            |                                                                     | varbinary(383) | NO   |     | NULL                |       |
                -| outbox           |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| sharedinbox      |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| featured         | Address for the collection of featured posts                        | varbinary(383) | YES  |     | NULL                |       |
                -| featured-tags    | Address for the collection of featured tags                         | varbinary(383) | YES  |     | NULL                |       |
                -| manually-approve |                                                                     | boolean        | YES  |     | NULL                |       |
                -| discoverable     | Mastodon extension: true if profile is published in their directory | boolean        | YES  |     | NULL                |       |
                -| suspended        | Mastodon extension: true if profile is suspended                    | boolean        | YES  |     | NULL                |       |
                -| nick             |                                                                     | varchar(255)   | NO   |     |                     |       |
                -| name             |                                                                     | varchar(255)   | YES  |     | NULL                |       |
                -| about            |                                                                     | text           | YES  |     | NULL                |       |
                -| xmpp             | XMPP address                                                        | varchar(255)   | YES  |     | NULL                |       |
                -| matrix           | Matrix address                                                      | varchar(255)   | YES  |     | NULL                |       |
                -| photo            |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| header           | Header picture                                                      | varbinary(383) | YES  |     | NULL                |       |
                -| addr             |                                                                     | varchar(255)   | YES  |     | NULL                |       |
                -| alias            |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| pubkey           |                                                                     | text           | YES  |     | NULL                |       |
                -| subscribe        |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                -| baseurl          | baseurl of the ap contact                                           | varbinary(383) | YES  |     | NULL                |       |
                -| gsid             | Global Server ID                                                    | int unsigned   | YES  |     | NULL                |       |
                -| generator        | Name of the contact's system                                        | varchar(255)   | YES  |     | NULL                |       |
                -| following_count  | Number of following contacts                                        | int unsigned   | YES  |     | 0                   |       |
                -| followers_count  | Number of followers                                                 | int unsigned   | YES  |     | 0                   |       |
                -| statuses_count   | Number of posts                                                     | int unsigned   | YES  |     | 0                   |       |
                -| updated          |                                                                     | datetime       | NO   |     | 0001-01-01 00:00:00 |       |
                +| Field              | Description                                                         | Type           | Null | Key | Default             | Extra |
                +| ------------------ | ------------------------------------------------------------------- | -------------- | ---- | --- | ------------------- | ----- |
                +| url                | URL of the contact                                                  | varbinary(383) | NO   | PRI | NULL                |       |
                +| uri-id             | Id of the item-uri table entry that contains the apcontact url      | int unsigned   | YES  |     | NULL                |       |
                +| uuid               |                                                                     | varbinary(255) | YES  |     | NULL                |       |
                +| type               |                                                                     | varchar(20)    | NO   |     | NULL                |       |
                +| following          |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| followers          |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| inbox              |                                                                     | varbinary(383) | NO   |     | NULL                |       |
                +| outbox             |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| sharedinbox        |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| featured           | Address for the collection of featured posts                        | varbinary(383) | YES  |     | NULL                |       |
                +| featured-tags      | Address for the collection of featured tags                         | varbinary(383) | YES  |     | NULL                |       |
                +| manually-approve   |                                                                     | boolean        | YES  |     | NULL                |       |
                +| discoverable       | Mastodon extension: true if profile is published in their directory | boolean        | YES  |     | NULL                |       |
                +| suspended          | Mastodon extension: true if profile is suspended                    | boolean        | YES  |     | NULL                |       |
                +| posting-restricted | lemmy:postingRestrictedToMods                                       | boolean        | YES  |     | NULL                |       |
                +| nick               |                                                                     | varchar(255)   | NO   |     |                     |       |
                +| name               |                                                                     | varchar(255)   | YES  |     | NULL                |       |
                +| about              |                                                                     | text           | YES  |     | NULL                |       |
                +| xmpp               | XMPP address                                                        | varchar(255)   | YES  |     | NULL                |       |
                +| matrix             | Matrix address                                                      | varchar(255)   | YES  |     | NULL                |       |
                +| photo              |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| header             | Header picture                                                      | varbinary(383) | YES  |     | NULL                |       |
                +| addr               |                                                                     | varchar(255)   | YES  |     | NULL                |       |
                +| alias              |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| pubkey             |                                                                     | text           | YES  |     | NULL                |       |
                +| subscribe          |                                                                     | varbinary(383) | YES  |     | NULL                |       |
                +| baseurl            | baseurl of the ap contact                                           | varbinary(383) | YES  |     | NULL                |       |
                +| gsid               | Global Server ID                                                    | int unsigned   | YES  |     | NULL                |       |
                +| generator          | Name of the contact's system                                        | varchar(255)   | YES  |     | NULL                |       |
                +| following_count    | Number of following contacts                                        | int unsigned   | YES  |     | 0                   |       |
                +| followers_count    | Number of followers                                                 | int unsigned   | YES  |     | 0                   |       |
                +| statuses_count     | Number of posts                                                     | int unsigned   | YES  |     | 0                   |       |
                +| updated            |                                                                     | datetime       | NO   |     | 0001-01-01 00:00:00 |       |
                 
                 Indexes
                 ------------
                diff --git a/doc/database/db_channel.md b/doc/database/db_channel.md
                index 9b9486fa32..4a469b4ee3 100644
                --- a/doc/database/db_channel.md
                +++ b/doc/database/db_channel.md
                @@ -16,8 +16,13 @@ Fields
                 | access-key       | Access key                                                                                        | varchar(1)         | YES  |     | NULL    |                |
                 | include-tags     | Comma separated list of tags that will be included in the channel                                 | varchar(1023)      | YES  |     | NULL    |                |
                 | exclude-tags     | Comma separated list of tags that aren't allowed in the channel                                   | varchar(1023)      | YES  |     | NULL    |                |
                +| min-size         | Minimum post size                                                                                 | int unsigned       | YES  |     | NULL    |                |
                +| max-size         | Maximum post size                                                                                 | int unsigned       | YES  |     | NULL    |                |
                 | full-text-search | Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode | varchar(1023)      | YES  |     | NULL    |                |
                 | media-type       | Filtered media types                                                                              | smallint unsigned  | YES  |     | NULL    |                |
                +| languages        | Desired languages                                                                                 | mediumtext         | YES  |     | NULL    |                |
                +| publish          | publish channel content                                                                           | boolean            | YES  |     | NULL    |                |
                +| valid            | Set, when the full-text-search is valid                                                           | boolean            | YES  |     | NULL    |                |
                 
                 Indexes
                 ------------
                diff --git a/doc/database/db_contact-relation.md b/doc/database/db_contact-relation.md
                index f11fd95a0f..c83c9b8326 100644
                --- a/doc/database/db_contact-relation.md
                +++ b/doc/database/db_contact-relation.md
                @@ -6,17 +6,18 @@ Contact relations
                 Fields
                 ------
                 
                -| Field                 | Description                                              | Type              | Null | Key | Default             | Extra |
                -| --------------------- | -------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
                -| cid                   | contact the related contact had interacted with          | int unsigned      | NO   | PRI | 0                   |       |
                -| relation-cid          | related contact who had interacted with the contact      | int unsigned      | NO   | PRI | 0                   |       |
                -| last-interaction      | Date of the last interaction by relation-cid on cid      | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                -| follow-updated        | Date of the last update of the contact relationship      | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                -| follows               | if true, relation-cid follows cid                        | boolean           | NO   |     | 0                   |       |
                -| score                 | score for interactions of cid on relation-cid            | smallint unsigned | YES  |     | NULL                |       |
                -| relation-score        | score for interactions of relation-cid on cid            | smallint unsigned | YES  |     | NULL                |       |
                -| thread-score          | score for interactions of cid on threads of relation-cid | smallint unsigned | YES  |     | NULL                |       |
                -| relation-thread-score | score for interactions of relation-cid on threads of cid | smallint unsigned | YES  |     | NULL                |       |
                +| Field                 | Description                                                             | Type              | Null | Key | Default             | Extra |
                +| --------------------- | ----------------------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
                +| cid                   | contact the related contact had interacted with                         | int unsigned      | NO   | PRI | 0                   |       |
                +| relation-cid          | related contact who had interacted with the contact                     | int unsigned      | NO   | PRI | 0                   |       |
                +| last-interaction      | Date of the last interaction by relation-cid on cid                     | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                +| follow-updated        | Date of the last update of the contact relationship                     | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                +| follows               | if true, relation-cid follows cid                                       | boolean           | NO   |     | 0                   |       |
                +| score                 | score for interactions of cid on relation-cid                           | smallint unsigned | YES  |     | NULL                |       |
                +| relation-score        | score for interactions of relation-cid on cid                           | smallint unsigned | YES  |     | NULL                |       |
                +| thread-score          | score for interactions of cid on threads of relation-cid                | smallint unsigned | YES  |     | NULL                |       |
                +| relation-thread-score | score for interactions of relation-cid on threads of cid                | smallint unsigned | YES  |     | NULL                |       |
                +| post-score            | score for the amount of posts from cid that can be seen by relation-cid | smallint unsigned | YES  |     | NULL                |       |
                 
                 Indexes
                 ------------
                diff --git a/doc/database/db_contact.md b/doc/database/db_contact.md
                index 8221f279e2..cd66ce12de 100644
                --- a/doc/database/db_contact.md
                +++ b/doc/database/db_contact.md
                @@ -59,7 +59,6 @@ Fields
                 | remote_self               |                                                                                                                | boolean            | NO   |     | 0                   |                |
                 | rel                       | The kind of the relation between the user and the contact                                                      | tinyint unsigned   | NO   |     | 0                   |                |
                 | protocol                  | Protocol of the contact                                                                                        | char(4)            | NO   |     |                     |                |
                -| subhub                    |                                                                                                                | boolean            | NO   |     | 0                   |                |
                 | hub-verify                |                                                                                                                | varbinary(383)     | NO   |     |                     |                |
                 | rating                    | Automatically detected feed poll frequency                                                                     | tinyint            | NO   |     | 0                   |                |
                 | priority                  | Feed poll priority                                                                                             | tinyint unsigned   | NO   |     | 0                   |                |
                diff --git a/doc/database/db_gserver.md b/doc/database/db_gserver.md
                index 8ba9e3d9b1..22939ec2bc 100644
                --- a/doc/database/db_gserver.md
                +++ b/doc/database/db_gserver.md
                @@ -6,47 +6,59 @@ Global servers
                 Fields
                 ------
                 
                -| Field                 | Description                                        | Type             | Null | Key | Default             | Extra          |
                -| --------------------- | -------------------------------------------------- | ---------------- | ---- | --- | ------------------- | -------------- |
                -| id                    | sequential ID                                      | int unsigned     | NO   | PRI | NULL                | auto_increment |
                -| url                   |                                                    | varbinary(383)   | NO   |     |                     |                |
                -| nurl                  |                                                    | varbinary(383)   | NO   |     |                     |                |
                -| version               |                                                    | varchar(255)     | NO   |     |                     |                |
                -| site_name             |                                                    | varchar(255)     | NO   |     |                     |                |
                -| info                  |                                                    | text             | YES  |     | NULL                |                |
                -| register_policy       |                                                    | tinyint          | NO   |     | 0                   |                |
                -| registered-users      | Number of registered users                         | int unsigned     | NO   |     | 0                   |                |
                -| active-week-users     | Number of active users in the last week            | int unsigned     | YES  |     | NULL                |                |
                -| active-month-users    | Number of active users in the last month           | int unsigned     | YES  |     | NULL                |                |
                -| active-halfyear-users | Number of active users in the last six month       | int unsigned     | YES  |     | NULL                |                |
                -| local-posts           | Number of local posts                              | int unsigned     | YES  |     | NULL                |                |
                -| local-comments        | Number of local comments                           | int unsigned     | YES  |     | NULL                |                |
                -| directory-type        | Type of directory service (Poco, Mastodon)         | tinyint          | YES  |     | 0                   |                |
                -| poco                  |                                                    | varbinary(383)   | NO   |     |                     |                |
                -| noscrape              |                                                    | varbinary(383)   | NO   |     |                     |                |
                -| network               |                                                    | char(4)          | NO   |     |                     |                |
                -| protocol              | The protocol of the server                         | tinyint unsigned | YES  |     | NULL                |                |
                -| platform              |                                                    | varchar(255)     | NO   |     |                     |                |
                -| relay-subscribe       | Has the server subscribed to the relay system      | boolean          | NO   |     | 0                   |                |
                -| relay-scope           | The scope of messages that the server wants to get | varchar(10)      | NO   |     |                     |                |
                -| detection-method      | Method that had been used to detect that server    | tinyint unsigned | YES  |     | NULL                |                |
                -| created               |                                                    | datetime         | NO   |     | 0001-01-01 00:00:00 |                |
                -| last_poco_query       |                                                    | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                -| last_contact          | Last successful connection request                 | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                -| last_failure          | Last failed connection request                     | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                -| blocked               | Server is blocked                                  | boolean          | YES  |     | NULL                |                |
                -| failed                | Connection failed                                  | boolean          | YES  |     | NULL                |                |
                -| next_contact          | Next connection request                            | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                +| Field                 | Description                                                    | Type             | Null | Key | Default             | Extra          |
                +| --------------------- | -------------------------------------------------------------- | ---------------- | ---- | --- | ------------------- | -------------- |
                +| id                    | sequential ID                                                  | int unsigned     | NO   | PRI | NULL                | auto_increment |
                +| url                   |                                                                | varbinary(383)   | NO   |     |                     |                |
                +| nurl                  |                                                                | varbinary(383)   | NO   |     |                     |                |
                +| version               | The version of this server software.                           | varchar(255)     | NO   |     |                     |                |
                +| site_name             |                                                                | varchar(255)     | NO   |     |                     |                |
                +| info                  |                                                                | text             | YES  |     | NULL                |                |
                +| register_policy       |                                                                | tinyint          | NO   |     | 0                   |                |
                +| registered-users      | Number of registered users                                     | int unsigned     | NO   |     | 0                   |                |
                +| active-week-users     | Number of active users in the last week                        | int unsigned     | YES  |     | NULL                |                |
                +| active-month-users    | Number of active users in the last month                       | int unsigned     | YES  |     | NULL                |                |
                +| active-halfyear-users | Number of active users in the last six month                   | int unsigned     | YES  |     | NULL                |                |
                +| local-posts           | Number of local posts                                          | int unsigned     | YES  |     | NULL                |                |
                +| local-comments        | Number of local comments                                       | int unsigned     | YES  |     | NULL                |                |
                +| directory-type        | Type of directory service (Poco, Mastodon)                     | tinyint          | YES  |     | 0                   |                |
                +| poco                  |                                                                | varbinary(383)   | NO   |     |                     |                |
                +| openwebauth           | Path to the OpenWebAuth endpoint                               | varbinary(383)   | YES  |     | NULL                |                |
                +| authredirect          | Path to the authRedirect endpoint                              | varbinary(383)   | YES  |     | NULL                |                |
                +| noscrape              |                                                                | varbinary(383)   | NO   |     |                     |                |
                +| network               |                                                                | char(4)          | NO   |     |                     |                |
                +| protocol              | The protocol of the server                                     | tinyint unsigned | YES  |     | NULL                |                |
                +| platform              | The canonical name of this server software.                    | varchar(255)     | NO   |     |                     |                |
                +| repository            | The url of the source code repository of this server software. | varbinary(383)   | YES  |     | NULL                |                |
                +| homepage              | The url of the homepage of this server software.               | varbinary(383)   | YES  |     | NULL                |                |
                +| relay-subscribe       | Has the server subscribed to the relay system                  | boolean          | NO   |     | 0                   |                |
                +| relay-scope           | The scope of messages that the server wants to get             | varchar(10)      | NO   |     |                     |                |
                +| detection-method      | Method that had been used to detect that server                | tinyint unsigned | YES  |     | NULL                |                |
                +| created               |                                                                | datetime         | NO   |     | 0001-01-01 00:00:00 |                |
                +| last_poco_query       |                                                                | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                +| last_contact          | Last successful connection request                             | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                +| last_failure          | Last failed connection request                                 | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                +| blocked               | Server is blocked                                              | boolean          | YES  |     | NULL                |                |
                +| failed                | Connection failed                                              | boolean          | YES  |     | NULL                |                |
                +| next_contact          | Next connection request                                        | datetime         | YES  |     | 0001-01-01 00:00:00 |                |
                +| redirect-gsid         | Target Gserver id in case of a redirect                        | int unsigned     | YES  |     | NULL                |                |
                 
                 Indexes
                 ------------
                 
                -| Name         | Fields            |
                -| ------------ | ----------------- |
                -| PRIMARY      | id                |
                -| nurl         | UNIQUE, nurl(190) |
                -| next_contact | next_contact      |
                -| network      | network           |
                +| Name          | Fields            |
                +| ------------- | ----------------- |
                +| PRIMARY       | id                |
                +| nurl          | UNIQUE, nurl(190) |
                +| next_contact  | next_contact      |
                +| network       | network           |
                +| redirect-gsid | redirect-gsid     |
                 
                +Foreign Keys
                +------------
                +
                +| Field | Target Table | Target Field |
                +|-------|--------------|--------------|
                +| redirect-gsid | [gserver](help/database/db_gserver) | id |
                 
                 Return to [database documentation](help/database)
                diff --git a/doc/database/db_inbox-entry.md b/doc/database/db_inbox-entry.md
                index 834d4bfd64..e18e66f240 100644
                --- a/doc/database/db_inbox-entry.md
                +++ b/doc/database/db_inbox-entry.md
                @@ -6,22 +6,24 @@ Incoming activity
                 Fields
                 ------
                 
                -| Field              | Description                            | Type           | Null | Key | Default | Extra          |
                -| ------------------ | -------------------------------------- | -------------- | ---- | --- | ------- | -------------- |
                -| id                 | sequential ID                          | int unsigned   | NO   | PRI | NULL    | auto_increment |
                -| activity-id        | id of the incoming activity            | varbinary(383) | YES  |     | NULL    |                |
                -| object-id          |                                        | varbinary(383) | YES  |     | NULL    |                |
                -| in-reply-to-id     |                                        | varbinary(383) | YES  |     | NULL    |                |
                -| conversation       |                                        | varbinary(383) | YES  |     | NULL    |                |
                -| type               | Type of the activity                   | varchar(64)    | YES  |     | NULL    |                |
                -| object-type        | Type of the object activity            | varchar(64)    | YES  |     | NULL    |                |
                -| object-object-type | Type of the object's object activity   | varchar(64)    | YES  |     | NULL    |                |
                -| received           | Receiving date                         | datetime       | YES  |     | NULL    |                |
                -| activity           | The JSON activity                      | mediumtext     | YES  |     | NULL    |                |
                -| signer             |                                        | varchar(255)   | YES  |     | NULL    |                |
                -| push               | Is the entry pushed or have pulled it? | boolean        | YES  |     | NULL    |                |
                -| trust              | Do we trust this entry?                | boolean        | YES  |     | NULL    |                |
                -| wid                | Workerqueue id                         | int unsigned   | YES  |     | NULL    |                |
                +| Field              | Description                            | Type             | Null | Key | Default | Extra          |
                +| ------------------ | -------------------------------------- | ---------------- | ---- | --- | ------- | -------------- |
                +| id                 | sequential ID                          | int unsigned     | NO   | PRI | NULL    | auto_increment |
                +| activity-id        | id of the incoming activity            | varbinary(383)   | YES  |     | NULL    |                |
                +| object-id          |                                        | varbinary(383)   | YES  |     | NULL    |                |
                +| in-reply-to-id     |                                        | varbinary(383)   | YES  |     | NULL    |                |
                +| context            |                                        | varbinary(383)   | YES  |     | NULL    |                |
                +| conversation       |                                        | varbinary(383)   | YES  |     | NULL    |                |
                +| type               | Type of the activity                   | varchar(64)      | YES  |     | NULL    |                |
                +| object-type        | Type of the object activity            | varchar(64)      | YES  |     | NULL    |                |
                +| object-object-type | Type of the object's object activity   | varchar(64)      | YES  |     | NULL    |                |
                +| received           | Receiving date                         | datetime         | YES  |     | NULL    |                |
                +| activity           | The JSON activity                      | mediumtext       | YES  |     | NULL    |                |
                +| signer             |                                        | varchar(255)     | YES  |     | NULL    |                |
                +| push               | Is the entry pushed or have pulled it? | boolean          | YES  |     | NULL    |                |
                +| trust              | Do we trust this entry?                | boolean          | YES  |     | NULL    |                |
                +| wid                | Workerqueue id                         | int unsigned     | YES  |     | NULL    |                |
                +| retrial            | Retrial counter                        | tinyint unsigned | YES  |     | 0       |                |
                 
                 Indexes
                 ------------
                diff --git a/doc/database/db_post-content.md b/doc/database/db_post-content.md
                index 27b372093a..d1ae90f278 100644
                --- a/doc/database/db_post-content.md
                +++ b/doc/database/db_post-content.md
                @@ -17,6 +17,7 @@ Fields
                 | location        | text location where this item originated                                                                                  | varchar(255)   | NO   |     |         |       |
                 | coord           | longitude/latitude pair representing location where this item originated                                                  | varchar(255)   | NO   |     |         |       |
                 | language        | Language information about this post                                                                                      | text           | YES  |     | NULL    |       |
                +| sensitive       | If true, this post contains sensitive content                                                                             | boolean        | YES  |     | NULL    |       |
                 | app             | application which generated this item                                                                                     | varchar(255)   | NO   |     |         |       |
                 | rendered-hash   |                                                                                                                           | varchar(32)    | NO   |     |         |       |
                 | rendered-html   | item.body converted to html                                                                                               | mediumtext     | YES  |     | NULL    |       |
                @@ -30,13 +31,12 @@ Fields
                 Indexes
                 ------------
                 
                -| Name                       | Fields                                 |
                -| -------------------------- | -------------------------------------- |
                -| PRIMARY                    | uri-id                                 |
                -| plink                      | plink(191)                             |
                -| resource-id                | resource-id                            |
                -| title-content-warning-body | FULLTEXT, title, content-warning, body |
                -| quote-uri-id               | quote-uri-id                           |
                +| Name         | Fields       |
                +| ------------ | ------------ |
                +| PRIMARY      | uri-id       |
                +| plink        | plink(191)   |
                +| resource-id  | resource-id  |
                +| quote-uri-id | quote-uri-id |
                 
                 Foreign Keys
                 ------------
                diff --git a/doc/database/db_post-counts.md b/doc/database/db_post-counts.md
                new file mode 100644
                index 0000000000..db2a8fd36d
                --- /dev/null
                +++ b/doc/database/db_post-counts.md
                @@ -0,0 +1,35 @@
                +Table post-counts
                +===========
                +
                +Original remote activity
                +
                +Fields
                +------
                +
                +| Field         | Description                                                 | Type              | Null | Key | Default | Extra |
                +| ------------- | ----------------------------------------------------------- | ----------------- | ---- | --- | ------- | ----- |
                +| uri-id        | Id of the item-uri table entry that contains the item uri   | int unsigned      | NO   | PRI | NULL    |       |
                +| vid           | Id of the verb table entry that contains the activity verbs | smallint unsigned | NO   | PRI | NULL    |       |
                +| reaction      | Emoji Reaction                                              | varchar(4)        | NO   | PRI | NULL    |       |
                +| parent-uri-id | Id of the item-uri table that contains the parent uri       | int unsigned      | YES  |     | NULL    |       |
                +| count         | Number of activities                                        | int unsigned      | YES  |     | 0       |       |
                +
                +Indexes
                +------------
                +
                +| Name          | Fields                |
                +| ------------- | --------------------- |
                +| PRIMARY       | uri-id, vid, reaction |
                +| vid           | vid                   |
                +| parent-uri-id | parent-uri-id         |
                +
                +Foreign Keys
                +------------
                +
                +| Field | Target Table | Target Field |
                +|-------|--------------|--------------|
                +| uri-id | [item-uri](help/database/db_item-uri) | id |
                +| vid | [verb](help/database/db_verb) | id |
                +| parent-uri-id | [item-uri](help/database/db_item-uri) | id |
                +
                +Return to [database documentation](help/database)
                diff --git a/doc/database/db_post-delivery-data.md b/doc/database/db_post-delivery-data.md
                index ad4ca6af32..332cfbcb3b 100644
                --- a/doc/database/db_post-delivery-data.md
                +++ b/doc/database/db_post-delivery-data.md
                @@ -18,7 +18,6 @@ Fields
                 | dfrn         | Number of successful deliveries via DFRN                                                                                                                   | mediumint    | NO   |     | 0       |       |
                 | legacy_dfrn  | Number of successful deliveries via legacy DFRN                                                                                                            | mediumint    | NO   |     | 0       |       |
                 | diaspora     | Number of successful deliveries via Diaspora                                                                                                               | mediumint    | NO   |     | 0       |       |
                -| ostatus      | Number of successful deliveries via OStatus                                                                                                                | mediumint    | NO   |     | 0       |       |
                 
                 Indexes
                 ------------
                diff --git a/doc/database/db_post-engagement.md b/doc/database/db_post-engagement.md
                index edca447f3d..31ae5e4e41 100644
                --- a/doc/database/db_post-engagement.md
                +++ b/doc/database/db_post-engagement.md
                @@ -11,10 +11,12 @@ Fields
                 | uri-id       | Id of the item-uri table entry that contains the item uri             | int unsigned       | NO   | PRI | NULL    |       |
                 | owner-id     | Item owner                                                            | int unsigned       | NO   |     | 0       |       |
                 | contact-type | Person, organisation, news, community, relay                          | tinyint            | NO   |     | 0       |       |
                -| media-type   | Type of media in a bit array (1 = image, 2 = video, 4 = audio         | tinyint            | NO   |     | 0       |       |
                -| language     | Language information about this post                                  | varbinary(128)     | YES  |     | NULL    |       |
                +| media-type   | Type of media in a bit array (1 = image, 2 = video, 4 = audio)        | tinyint            | NO   |     | 0       |       |
                +| language     | Language information about this post in the ISO 639-1 format          | char(2)            | YES  |     | NULL    |       |
                 | searchtext   | Simplified text for the full text search                              | mediumtext         | YES  |     | NULL    |       |
                +| size         | Body size                                                             | int unsigned       | YES  |     | NULL    |       |
                 | created      |                                                                       | datetime           | YES  |     | NULL    |       |
                +| network      |                                                                       | char(4)            | YES  |     | NULL    |       |
                 | restricted   | If true, this post is either unlisted or not from a federated network | boolean            | NO   |     | 0       |       |
                 | comments     | Number of comments                                                    | mediumint unsigned | YES  |     | NULL    |       |
                 | activities   | Number of activities (like, dislike, ...)                             | mediumint unsigned | YES  |     | NULL    |       |
                diff --git a/doc/database/db_post-media.md b/doc/database/db_post-media.md
                index 2d73a39cfc..acbd1f75cb 100644
                --- a/doc/database/db_post-media.md
                +++ b/doc/database/db_post-media.md
                @@ -6,29 +6,33 @@ Attached media
                 Fields
                 ------
                 
                -| Field           | Description                                                        | Type              | Null | Key | Default | Extra          |
                -| --------------- | ------------------------------------------------------------------ | ----------------- | ---- | --- | ------- | -------------- |
                -| id              | sequential ID                                                      | int unsigned      | NO   | PRI | NULL    | auto_increment |
                -| uri-id          | Id of the item-uri table entry that contains the item uri          | int unsigned      | NO   |     | NULL    |                |
                -| url             | Media URL                                                          | varbinary(1024)   | NO   |     | NULL    |                |
                -| media-uri-id    | Id of the item-uri table entry that contains the activities uri-id | int unsigned      | YES  |     | NULL    |                |
                -| type            | Media type                                                         | tinyint unsigned  | NO   |     | 0       |                |
                -| mimetype        |                                                                    | varchar(60)       | YES  |     | NULL    |                |
                -| height          | Height of the media                                                | smallint unsigned | YES  |     | NULL    |                |
                -| width           | Width of the media                                                 | smallint unsigned | YES  |     | NULL    |                |
                -| size            | Media size                                                         | bigint unsigned   | YES  |     | NULL    |                |
                -| blurhash        | BlurHash representation of the image                               | varbinary(255)    | YES  |     | NULL    |                |
                -| preview         | Preview URL                                                        | varbinary(512)    | YES  |     | NULL    |                |
                -| preview-height  | Height of the preview picture                                      | smallint unsigned | YES  |     | NULL    |                |
                -| preview-width   | Width of the preview picture                                       | smallint unsigned | YES  |     | NULL    |                |
                -| description     |                                                                    | text              | YES  |     | NULL    |                |
                -| name            | Name of the media                                                  | varchar(255)      | YES  |     | NULL    |                |
                -| author-url      | URL of the author of the media                                     | varbinary(383)    | YES  |     | NULL    |                |
                -| author-name     | Name of the author of the media                                    | varchar(255)      | YES  |     | NULL    |                |
                -| author-image    | Image of the author of the media                                   | varbinary(383)    | YES  |     | NULL    |                |
                -| publisher-url   | URL of the publisher of the media                                  | varbinary(383)    | YES  |     | NULL    |                |
                -| publisher-name  | Name of the publisher of the media                                 | varchar(255)      | YES  |     | NULL    |                |
                -| publisher-image | Image of the publisher of the media                                | varbinary(383)    | YES  |     | NULL    |                |
                +| Field           | Description                                                                         | Type              | Null | Key | Default | Extra          |
                +| --------------- | ----------------------------------------------------------------------------------- | ----------------- | ---- | --- | ------- | -------------- |
                +| id              | sequential ID                                                                       | int unsigned      | NO   | PRI | NULL    | auto_increment |
                +| uri-id          | Id of the item-uri table entry that contains the item uri                           | int unsigned      | NO   |     | NULL    |                |
                +| url             | Media URL                                                                           | varbinary(1024)   | NO   |     | NULL    |                |
                +| media-uri-id    | Id of the item-uri table entry that contains the activities uri-id                  | int unsigned      | YES  |     | NULL    |                |
                +| attach-id       | In case of a local attachment, this field is filled with the id in the attach table | int unsigned      | YES  |     | NULL    |                |
                +| type            | Media type                                                                          | tinyint unsigned  | NO   |     | 0       |                |
                +| mimetype        |                                                                                     | varchar(60)       | YES  |     | NULL    |                |
                +| height          | Height of the media                                                                 | smallint unsigned | YES  |     | NULL    |                |
                +| width           | Width of the media                                                                  | smallint unsigned | YES  |     | NULL    |                |
                +| size            | Media size                                                                          | bigint unsigned   | YES  |     | NULL    |                |
                +| blurhash        | BlurHash representation of the image                                                | varbinary(255)    | YES  |     | NULL    |                |
                +| preview         | Preview URL                                                                         | varbinary(512)    | YES  |     | NULL    |                |
                +| preview-height  | Height of the preview picture                                                       | smallint unsigned | YES  |     | NULL    |                |
                +| preview-width   | Width of the preview picture                                                        | smallint unsigned | YES  |     | NULL    |                |
                +| description     |                                                                                     | text              | YES  |     | NULL    |                |
                +| name            | Name of the media                                                                   | varchar(255)      | YES  |     | NULL    |                |
                +| author-url      | URL of the author of the media                                                      | varbinary(383)    | YES  |     | NULL    |                |
                +| author-name     | Name of the author of the media                                                     | varchar(255)      | YES  |     | NULL    |                |
                +| author-image    | Image of the author of the media                                                    | varbinary(383)    | YES  |     | NULL    |                |
                +| publisher-url   | URL of the publisher of the media                                                   | varbinary(383)    | YES  |     | NULL    |                |
                +| publisher-name  | Name of the publisher of the media                                                  | varchar(255)      | YES  |     | NULL    |                |
                +| publisher-image | Image of the publisher of the media                                                 | varbinary(383)    | YES  |     | NULL    |                |
                +| language        | Language information about this media in the ISO 639 format                         | char(3)           | YES  |     | NULL    |                |
                +| published       | Publification date of this media                                                    | datetime          | YES  |     | NULL    |                |
                +| modified        | Modification date of this media                                                     | datetime          | YES  |     | NULL    |                |
                 
                 Indexes
                 ------------
                @@ -39,6 +43,7 @@ Indexes
                 | uri-id-url   | UNIQUE, uri-id, url(512) |
                 | uri-id-id    | uri-id, id               |
                 | media-uri-id | media-uri-id             |
                +| attach-id    | attach-id                |
                 
                 Foreign Keys
                 ------------
                @@ -47,5 +52,6 @@ Foreign Keys
                 |-------|--------------|--------------|
                 | uri-id | [item-uri](help/database/db_item-uri) | id |
                 | media-uri-id | [item-uri](help/database/db_item-uri) | id |
                +| attach-id | [attach](help/database/db_attach) | id |
                 
                 Return to [database documentation](help/database)
                diff --git a/doc/database/db_post-origin.md b/doc/database/db_post-origin.md
                new file mode 100644
                index 0000000000..dc72b0134f
                --- /dev/null
                +++ b/doc/database/db_post-origin.md
                @@ -0,0 +1,48 @@
                +Table post-origin
                +===========
                +
                +Posts from local users
                +
                +Fields
                +------
                +
                +| Field         | Description                                                  | Type               | Null | Key | Default             | Extra |
                +| ------------- | ------------------------------------------------------------ | ------------------ | ---- | --- | ------------------- | ----- |
                +| id            |                                                              | int unsigned       | NO   | PRI | NULL                |       |
                +| uri-id        | Id of the item-uri table entry that contains the item uri    | int unsigned       | NO   |     | NULL                |       |
                +| uid           | Owner id which owns this copy of the item                    | mediumint unsigned | NO   |     | NULL                |       |
                +| parent-uri-id | Id of the item-uri table that contains the parent uri        | int unsigned       | YES  |     | NULL                |       |
                +| thr-parent-id | Id of the item-uri table that contains the thread parent uri | int unsigned       | YES  |     | NULL                |       |
                +| created       | Creation timestamp.                                          | datetime           | NO   |     | 0001-01-01 00:00:00 |       |
                +| received      | datetime                                                     | datetime           | NO   |     | 0001-01-01 00:00:00 |       |
                +| gravity       |                                                              | tinyint unsigned   | NO   |     | 0                   |       |
                +| vid           | Id of the verb table entry that contains the activity verbs  | smallint unsigned  | YES  |     | NULL                |       |
                +| private       | 0=public, 1=private, 2=unlisted                              | tinyint unsigned   | NO   |     | 0                   |       |
                +| wall          | This item was posted to the wall of uid                      | boolean            | NO   |     | 0                   |       |
                +
                +Indexes
                +------------
                +
                +| Name              | Fields              |
                +| ----------------- | ------------------- |
                +| PRIMARY           | id                  |
                +| uid_uri-id        | UNIQUE, uid, uri-id |
                +| uri-id            | uri-id              |
                +| parent-uri-id     | parent-uri-id       |
                +| thr-parent-id     | thr-parent-id       |
                +| vid               | vid                 |
                +| parent-uri-id_uid | parent-uri-id, uid  |
                +| uid_wall_received | uid, wall, received |
                +
                +Foreign Keys
                +------------
                +
                +| Field | Target Table | Target Field |
                +|-------|--------------|--------------|
                +| uri-id | [item-uri](help/database/db_item-uri) | id |
                +| uid | [user](help/database/db_user) | uid |
                +| parent-uri-id | [item-uri](help/database/db_item-uri) | id |
                +| thr-parent-id | [item-uri](help/database/db_item-uri) | id |
                +| vid | [verb](help/database/db_verb) | id |
                +
                +Return to [database documentation](help/database)
                diff --git a/doc/database/db_post-searchindex.md b/doc/database/db_post-searchindex.md
                new file mode 100644
                index 0000000000..203a2dbab3
                --- /dev/null
                +++ b/doc/database/db_post-searchindex.md
                @@ -0,0 +1,38 @@
                +Table post-searchindex
                +===========
                +
                +Content for all posts
                +
                +Fields
                +------
                +
                +| Field      | Description                                                           | Type         | Null | Key | Default | Extra |
                +| ---------- | --------------------------------------------------------------------- | ------------ | ---- | --- | ------- | ----- |
                +| uri-id     | Id of the item-uri table entry that contains the item uri             | int unsigned | NO   | PRI | NULL    |       |
                +| owner-id   | Item owner                                                            | int unsigned | NO   |     | 0       |       |
                +| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio)        | tinyint      | NO   |     | 0       |       |
                +| language   | Language information about this post in the ISO 639-1 format          | char(2)      | YES  |     | NULL    |       |
                +| searchtext | Simplified text for the full text search                              | mediumtext   | YES  |     | NULL    |       |
                +| size       | Body size                                                             | int unsigned | YES  |     | NULL    |       |
                +| created    |                                                                       | datetime     | YES  |     | NULL    |       |
                +| restricted | If true, this post is either unlisted or not from a federated network | boolean      | NO   |     | 0       |       |
                +
                +Indexes
                +------------
                +
                +| Name       | Fields               |
                +| ---------- | -------------------- |
                +| PRIMARY    | uri-id               |
                +| owner-id   | owner-id             |
                +| created    | created              |
                +| searchtext | FULLTEXT, searchtext |
                +
                +Foreign Keys
                +------------
                +
                +| Field | Target Table | Target Field |
                +|-------|--------------|--------------|
                +| uri-id | [item-uri](help/database/db_item-uri) | id |
                +| owner-id | [contact](help/database/db_contact) | id |
                +
                +Return to [database documentation](help/database)
                diff --git a/doc/database/db_post-thread-user.md b/doc/database/db_post-thread-user.md
                index a89d94c96f..d00e852547 100644
                --- a/doc/database/db_post-thread-user.md
                +++ b/doc/database/db_post-thread-user.md
                @@ -9,6 +9,7 @@ Fields
                 | Field           | Description                                                                                             | Type               | Null | Key | Default             | Extra |
                 | --------------- | ------------------------------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------------------- | ----- |
                 | uri-id          | Id of the item-uri table entry that contains the item uri                                               | int unsigned       | NO   | PRI | NULL                |       |
                +| context-id      | Id of the item-uri table entry that contains the endpoint for the context collection                    | int unsigned       | YES  |     | NULL                |       |
                 | conversation-id | Id of the item-uri table entry that contains the conversation uri                                       | int unsigned       | YES  |     | NULL                |       |
                 | owner-id        | Item owner                                                                                              | int unsigned       | NO   |     | 0                   |       |
                 | author-id       | Item author                                                                                             | int unsigned       | NO   |     | 0                   |       |
                @@ -40,6 +41,7 @@ Indexes
                 | -------------------- | --------------------- |
                 | PRIMARY              | uid, uri-id           |
                 | uri-id               | uri-id                |
                +| context-id           | context-id            |
                 | conversation-id      | conversation-id       |
                 | owner-id             | owner-id              |
                 | author-id            | author-id             |
                @@ -68,6 +70,7 @@ Foreign Keys
                 | Field | Target Table | Target Field |
                 |-------|--------------|--------------|
                 | uri-id | [item-uri](help/database/db_item-uri) | id |
                +| context-id | [item-uri](help/database/db_item-uri) | id |
                 | conversation-id | [item-uri](help/database/db_item-uri) | id |
                 | owner-id | [contact](help/database/db_contact) | id |
                 | author-id | [contact](help/database/db_contact) | id |
                diff --git a/doc/database/db_post-thread.md b/doc/database/db_post-thread.md
                index b90fb2ab55..959927a468 100644
                --- a/doc/database/db_post-thread.md
                +++ b/doc/database/db_post-thread.md
                @@ -9,6 +9,7 @@ Fields
                 | Field           | Description                                                                                             | Type         | Null | Key | Default             | Extra |
                 | --------------- | ------------------------------------------------------------------------------------------------------- | ------------ | ---- | --- | ------------------- | ----- |
                 | uri-id          | Id of the item-uri table entry that contains the item uri                                               | int unsigned | NO   | PRI | NULL                |       |
                +| context-id      | Id of the item-uri table entry that contains the endpoint for the context collection                    | int unsigned | YES  |     | NULL                |       |
                 | conversation-id | Id of the item-uri table entry that contains the conversation uri                                       | int unsigned | YES  |     | NULL                |       |
                 | owner-id        | Item owner                                                                                              | int unsigned | NO   |     | 0                   |       |
                 | author-id       | Item author                                                                                             | int unsigned | NO   |     | 0                   |       |
                @@ -25,6 +26,7 @@ Indexes
                 | Name            | Fields          |
                 | --------------- | --------------- |
                 | PRIMARY         | uri-id          |
                +| context-id      | context-id      |
                 | conversation-id | conversation-id |
                 | owner-id        | owner-id        |
                 | author-id       | author-id       |
                @@ -38,6 +40,7 @@ Foreign Keys
                 | Field | Target Table | Target Field |
                 |-------|--------------|--------------|
                 | uri-id | [item-uri](help/database/db_item-uri) | id |
                +| context-id | [item-uri](help/database/db_item-uri) | id |
                 | conversation-id | [item-uri](help/database/db_item-uri) | id |
                 | owner-id | [contact](help/database/db_contact) | id |
                 | author-id | [contact](help/database/db_contact) | id |
                diff --git a/doc/database/db_post-user.md b/doc/database/db_post-user.md
                index 2823391d47..502c49c3ac 100644
                --- a/doc/database/db_post-user.md
                +++ b/doc/database/db_post-user.md
                @@ -6,66 +6,69 @@ User specific post data
                 Fields
                 ------
                 
                -| Field             | Description                                                                       | Type               | Null | Key | Default             | Extra          |
                -| ----------------- | --------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------------------- | -------------- |
                -| id                |                                                                                   | int unsigned       | NO   | PRI | NULL                | auto_increment |
                -| uri-id            | Id of the item-uri table entry that contains the item uri                         | int unsigned       | NO   |     | NULL                |                |
                -| parent-uri-id     | Id of the item-uri table that contains the parent uri                             | int unsigned       | YES  |     | NULL                |                |
                -| thr-parent-id     | Id of the item-uri table that contains the thread parent uri                      | int unsigned       | YES  |     | NULL                |                |
                -| external-id       | Id of the item-uri table entry that contains the external uri                     | int unsigned       | YES  |     | NULL                |                |
                -| created           | Creation timestamp.                                                               | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                -| edited            | Date of last edit (default is created)                                            | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                -| received          | datetime                                                                          | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                -| gravity           |                                                                                   | tinyint unsigned   | NO   |     | 0                   |                |
                -| network           | Network from where the item comes from                                            | char(4)            | NO   |     |                     |                |
                -| owner-id          | Link to the contact table with uid=0 of the owner of this item                    | int unsigned       | NO   |     | 0                   |                |
                -| author-id         | Link to the contact table with uid=0 of the author of this item                   | int unsigned       | NO   |     | 0                   |                |
                -| causer-id         | Link to the contact table with uid=0 of the contact that caused the item creation | int unsigned       | YES  |     | NULL                |                |
                -| post-type         | Post type (personal note, image, article, ...)                                    | tinyint unsigned   | NO   |     | 0                   |                |
                -| post-reason       | Reason why the post arrived at the user                                           | tinyint unsigned   | NO   |     | 0                   |                |
                -| vid               | Id of the verb table entry that contains the activity verbs                       | smallint unsigned  | YES  |     | NULL                |                |
                -| private           | 0=public, 1=private, 2=unlisted                                                   | tinyint unsigned   | NO   |     | 0                   |                |
                -| global            |                                                                                   | boolean            | NO   |     | 0                   |                |
                -| visible           |                                                                                   | boolean            | NO   |     | 0                   |                |
                -| deleted           | item has been marked for deletion                                                 | boolean            | NO   |     | 0                   |                |
                -| uid               | Owner id which owns this copy of the item                                         | mediumint unsigned | NO   |     | NULL                |                |
                -| protocol          | Protocol used to deliver the item for this user                                   | tinyint unsigned   | YES  |     | NULL                |                |
                -| contact-id        | contact.id                                                                        | int unsigned       | NO   |     | 0                   |                |
                -| event-id          | Used to link to the event.id                                                      | int unsigned       | YES  |     | NULL                |                |
                -| unseen            | post has not been seen                                                            | boolean            | NO   |     | 1                   |                |
                -| hidden            | Marker to hide the post from the user                                             | boolean            | NO   |     | 0                   |                |
                -| notification-type |                                                                                   | smallint unsigned  | NO   |     | 0                   |                |
                -| wall              | This item was posted to the wall of uid                                           | boolean            | NO   |     | 0                   |                |
                -| origin            | item originated at this site                                                      | boolean            | NO   |     | 0                   |                |
                -| psid              | ID of the permission set of this post                                             | int unsigned       | YES  |     | NULL                |                |
                +| Field             | Description                                                                          | Type               | Null | Key | Default             | Extra          |
                +| ----------------- | ------------------------------------------------------------------------------------ | ------------------ | ---- | --- | ------------------- | -------------- |
                +| id                |                                                                                      | int unsigned       | NO   | PRI | NULL                | auto_increment |
                +| uri-id            | Id of the item-uri table entry that contains the item uri                            | int unsigned       | NO   |     | NULL                |                |
                +| parent-uri-id     | Id of the item-uri table that contains the parent uri                                | int unsigned       | YES  |     | NULL                |                |
                +| thr-parent-id     | Id of the item-uri table that contains the thread parent uri                         | int unsigned       | YES  |     | NULL                |                |
                +| external-id       | Id of the item-uri table entry that contains the external uri                        | int unsigned       | YES  |     | NULL                |                |
                +| replies-id        | Id of the item-uri table entry that contains the endpoint for the replies collection | int unsigned       | YES  |     | NULL                |                |
                +| created           | Creation timestamp.                                                                  | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                +| edited            | Date of last edit (default is created)                                               | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                +| received          | datetime                                                                             | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                +| gravity           |                                                                                      | tinyint unsigned   | NO   |     | 0                   |                |
                +| network           | Network from where the item comes from                                               | char(4)            | NO   |     |                     |                |
                +| owner-id          | Link to the contact table with uid=0 of the owner of this item                       | int unsigned       | NO   |     | 0                   |                |
                +| author-id         | Link to the contact table with uid=0 of the author of this item                      | int unsigned       | NO   |     | 0                   |                |
                +| causer-id         | Link to the contact table with uid=0 of the contact that caused the item creation    | int unsigned       | YES  |     | NULL                |                |
                +| post-type         | Post type (personal note, image, article, ...)                                       | tinyint unsigned   | NO   |     | 0                   |                |
                +| post-reason       | Reason why the post arrived at the user                                              | tinyint unsigned   | NO   |     | 0                   |                |
                +| vid               | Id of the verb table entry that contains the activity verbs                          | smallint unsigned  | YES  |     | NULL                |                |
                +| private           | 0=public, 1=private, 2=unlisted                                                      | tinyint unsigned   | NO   |     | 0                   |                |
                +| restrictions      | Bit array of post restrictions (1 = Reply, 2 = Like, 4 = Announce)                   | tinyint unsigned   | YES  |     | NULL                |                |
                +| global            |                                                                                      | boolean            | NO   |     | 0                   |                |
                +| visible           |                                                                                      | boolean            | NO   |     | 0                   |                |
                +| deleted           | item has been marked for deletion                                                    | boolean            | NO   |     | 0                   |                |
                +| uid               | Owner id which owns this copy of the item                                            | mediumint unsigned | NO   |     | NULL                |                |
                +| protocol          | Protocol used to deliver the item for this user                                      | tinyint unsigned   | YES  |     | NULL                |                |
                +| contact-id        | contact.id                                                                           | int unsigned       | NO   |     | 0                   |                |
                +| event-id          | Used to link to the event.id                                                         | int unsigned       | YES  |     | NULL                |                |
                +| unseen            | post has not been seen                                                               | boolean            | NO   |     | 1                   |                |
                +| hidden            | Marker to hide the post from the user                                                | boolean            | NO   |     | 0                   |                |
                +| notification-type |                                                                                      | smallint unsigned  | NO   |     | 0                   |                |
                +| wall              | This item was posted to the wall of uid                                              | boolean            | NO   |     | 0                   |                |
                +| origin            | item originated at this site                                                         | boolean            | NO   |     | 0                   |                |
                +| psid              | ID of the permission set of this post                                                | int unsigned       | YES  |     | NULL                |                |
                 
                 Indexes
                 ------------
                 
                -| Name                 | Fields                  |
                -| -------------------- | ----------------------- |
                -| PRIMARY              | id                      |
                -| uid_uri-id           | UNIQUE, uid, uri-id     |
                -| uri-id               | uri-id                  |
                -| parent-uri-id        | parent-uri-id           |
                -| thr-parent-id        | thr-parent-id           |
                -| external-id          | external-id             |
                -| owner-id             | owner-id                |
                -| author-id            | author-id               |
                -| causer-id            | causer-id               |
                -| vid                  | vid                     |
                -| contact-id           | contact-id              |
                -| event-id             | event-id                |
                -| psid                 | psid                    |
                -| author-id_uid        | author-id, uid          |
                -| author-id_created    | author-id, created      |
                -| owner-id_created     | owner-id, created       |
                -| parent-uri-id_uid    | parent-uri-id, uid      |
                -| uid_wall_received    | uid, wall, received     |
                -| uid_contactid        | uid, contact-id         |
                -| uid_unseen_contactid | uid, unseen, contact-id |
                -| uid_unseen           | uid, unseen             |
                -| uid_hidden_uri-id    | uid, hidden, uri-id     |
                +| Name                  | Fields                  |
                +| --------------------- | ----------------------- |
                +| PRIMARY               | id                      |
                +| uid_uri-id            | UNIQUE, uid, uri-id     |
                +| uri-id_origin_deleted | uri-id, origin, deleted |
                +| parent-uri-id         | parent-uri-id           |
                +| thr-parent-id         | thr-parent-id           |
                +| external-id           | external-id             |
                +| replies-id            | replies-id              |
                +| owner-id              | owner-id                |
                +| author-id             | author-id               |
                +| causer-id             | causer-id               |
                +| vid                   | vid                     |
                +| contact-id            | contact-id              |
                +| event-id              | event-id                |
                +| psid                  | psid                    |
                +| author-id_uid         | author-id, uid          |
                +| author-id_created     | author-id, created      |
                +| owner-id_created      | owner-id, created       |
                +| parent-uri-id_uid     | parent-uri-id, uid      |
                +| uid_wall_received     | uid, wall, received     |
                +| uid_contactid         | uid, contact-id         |
                +| uid_unseen_contactid  | uid, unseen, contact-id |
                +| uid_unseen            | uid, unseen             |
                +| uid_hidden_uri-id     | uid, hidden, uri-id     |
                 
                 Foreign Keys
                 ------------
                @@ -76,6 +79,7 @@ Foreign Keys
                 | parent-uri-id | [item-uri](help/database/db_item-uri) | id |
                 | thr-parent-id | [item-uri](help/database/db_item-uri) | id |
                 | external-id | [item-uri](help/database/db_item-uri) | id |
                +| replies-id | [item-uri](help/database/db_item-uri) | id |
                 | owner-id | [contact](help/database/db_contact) | id |
                 | author-id | [contact](help/database/db_contact) | id |
                 | causer-id | [contact](help/database/db_contact) | id |
                diff --git a/doc/database/db_post.md b/doc/database/db_post.md
                index 303269b1c6..8f6690be06 100644
                --- a/doc/database/db_post.md
                +++ b/doc/database/db_post.md
                @@ -6,26 +6,27 @@ Structure for all posts
                 Fields
                 ------
                 
                -| Field         | Description                                                                       | Type              | Null | Key | Default             | Extra |
                -| ------------- | --------------------------------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
                -| uri-id        | Id of the item-uri table entry that contains the item uri                         | int unsigned      | NO   | PRI | NULL                |       |
                -| parent-uri-id | Id of the item-uri table that contains the parent uri                             | int unsigned      | YES  |     | NULL                |       |
                -| thr-parent-id | Id of the item-uri table that contains the thread parent uri                      | int unsigned      | YES  |     | NULL                |       |
                -| external-id   | Id of the item-uri table entry that contains the external uri                     | int unsigned      | YES  |     | NULL                |       |
                -| created       | Creation timestamp.                                                               | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                -| edited        | Date of last edit (default is created)                                            | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                -| received      | datetime                                                                          | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                -| gravity       |                                                                                   | tinyint unsigned  | NO   |     | 0                   |       |
                -| network       | Network from where the item comes from                                            | char(4)           | NO   |     |                     |       |
                -| owner-id      | Link to the contact table with uid=0 of the owner of this item                    | int unsigned      | NO   |     | 0                   |       |
                -| author-id     | Link to the contact table with uid=0 of the author of this item                   | int unsigned      | NO   |     | 0                   |       |
                -| causer-id     | Link to the contact table with uid=0 of the contact that caused the item creation | int unsigned      | YES  |     | NULL                |       |
                -| post-type     | Post type (personal note, image, article, ...)                                    | tinyint unsigned  | NO   |     | 0                   |       |
                -| vid           | Id of the verb table entry that contains the activity verbs                       | smallint unsigned | YES  |     | NULL                |       |
                -| private       | 0=public, 1=private, 2=unlisted                                                   | tinyint unsigned  | NO   |     | 0                   |       |
                -| global        |                                                                                   | boolean           | NO   |     | 0                   |       |
                -| visible       |                                                                                   | boolean           | NO   |     | 0                   |       |
                -| deleted       | item has been marked for deletion                                                 | boolean           | NO   |     | 0                   |       |
                +| Field         | Description                                                                          | Type              | Null | Key | Default             | Extra |
                +| ------------- | ------------------------------------------------------------------------------------ | ----------------- | ---- | --- | ------------------- | ----- |
                +| uri-id        | Id of the item-uri table entry that contains the item uri                            | int unsigned      | NO   | PRI | NULL                |       |
                +| parent-uri-id | Id of the item-uri table that contains the parent uri                                | int unsigned      | YES  |     | NULL                |       |
                +| thr-parent-id | Id of the item-uri table that contains the thread parent uri                         | int unsigned      | YES  |     | NULL                |       |
                +| external-id   | Id of the item-uri table entry that contains the external uri                        | int unsigned      | YES  |     | NULL                |       |
                +| replies-id    | Id of the item-uri table entry that contains the endpoint for the replies collection | int unsigned      | YES  |     | NULL                |       |
                +| created       | Creation timestamp.                                                                  | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                +| edited        | Date of last edit (default is created)                                               | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                +| received      | datetime                                                                             | datetime          | NO   |     | 0001-01-01 00:00:00 |       |
                +| gravity       |                                                                                      | tinyint unsigned  | NO   |     | 0                   |       |
                +| network       | Network from where the item comes from                                               | char(4)           | NO   |     |                     |       |
                +| owner-id      | Link to the contact table with uid=0 of the owner of this item                       | int unsigned      | NO   |     | 0                   |       |
                +| author-id     | Link to the contact table with uid=0 of the author of this item                      | int unsigned      | NO   |     | 0                   |       |
                +| causer-id     | Link to the contact table with uid=0 of the contact that caused the item creation    | int unsigned      | YES  |     | NULL                |       |
                +| post-type     | Post type (personal note, image, article, ...)                                       | tinyint unsigned  | NO   |     | 0                   |       |
                +| vid           | Id of the verb table entry that contains the activity verbs                          | smallint unsigned | YES  |     | NULL                |       |
                +| private       | 0=public, 1=private, 2=unlisted                                                      | tinyint unsigned  | NO   |     | 0                   |       |
                +| global        |                                                                                      | boolean           | NO   |     | 0                   |       |
                +| visible       |                                                                                      | boolean           | NO   |     | 0                   |       |
                +| deleted       | item has been marked for deletion                                                    | boolean           | NO   |     | 0                   |       |
                 
                 Indexes
                 ------------
                @@ -36,6 +37,7 @@ Indexes
                 | parent-uri-id | parent-uri-id |
                 | thr-parent-id | thr-parent-id |
                 | external-id   | external-id   |
                +| replies-id    | replies-id    |
                 | owner-id      | owner-id      |
                 | author-id     | author-id     |
                 | causer-id     | causer-id     |
                @@ -50,6 +52,7 @@ Foreign Keys
                 | parent-uri-id | [item-uri](help/database/db_item-uri) | id |
                 | thr-parent-id | [item-uri](help/database/db_item-uri) | id |
                 | external-id | [item-uri](help/database/db_item-uri) | id |
                +| replies-id | [item-uri](help/database/db_item-uri) | id |
                 | owner-id | [contact](help/database/db_contact) | id |
                 | author-id | [contact](help/database/db_contact) | id |
                 | causer-id | [contact](help/database/db_contact) | id |
                diff --git a/doc/database/db_profile.md b/doc/database/db_profile.md
                index c4a8b01709..09f10770be 100644
                --- a/doc/database/db_profile.md
                +++ b/doc/database/db_profile.md
                @@ -56,11 +56,10 @@ Fields
                 Indexes
                 ------------
                 
                -| Name           | Fields                 |
                -| -------------- | ---------------------- |
                -| PRIMARY        | id                     |
                -| uid_is-default | uid, is-default        |
                -| pub_keywords   | FULLTEXT, pub_keywords |
                +| Name           | Fields          |
                +| -------------- | --------------- |
                +| PRIMARY        | id              |
                +| uid_is-default | uid, is-default |
                 
                 Foreign Keys
                 ------------
                diff --git a/doc/database/db_push_subscriber.md b/doc/database/db_push_subscriber.md
                deleted file mode 100644
                index fbb2ebba47..0000000000
                --- a/doc/database/db_push_subscriber.md
                +++ /dev/null
                @@ -1,38 +0,0 @@
                -Table push_subscriber
                -===========
                -
                -Used for OStatus: Contains feed subscribers
                -
                -Fields
                -------
                -
                -| Field        | Description                       | Type               | Null | Key | Default             | Extra          |
                -| ------------ | --------------------------------- | ------------------ | ---- | --- | ------------------- | -------------- |
                -| id           | sequential ID                     | int unsigned       | NO   | PRI | NULL                | auto_increment |
                -| uid          | User id                           | mediumint unsigned | NO   |     | 0                   |                |
                -| callback_url |                                   | varbinary(383)     | NO   |     |                     |                |
                -| topic        |                                   | varchar(255)       | NO   |     |                     |                |
                -| nickname     |                                   | varchar(255)       | NO   |     |                     |                |
                -| push         | Retrial counter                   | tinyint            | NO   |     | 0                   |                |
                -| last_update  | Date of last successful trial     | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                -| next_try     | Next retrial date                 | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                -| renewed      | Date of last subscription renewal | datetime           | NO   |     | 0001-01-01 00:00:00 |                |
                -| secret       |                                   | varchar(255)       | NO   |     |                     |                |
                -
                -Indexes
                -------------
                -
                -| Name     | Fields   |
                -| -------- | -------- |
                -| PRIMARY  | id       |
                -| next_try | next_try |
                -| uid      | uid      |
                -
                -Foreign Keys
                -------------
                -
                -| Field | Target Table | Target Field |
                -|-------|--------------|--------------|
                -| uid | [user](help/database/db_user) | uid |
                -
                -Return to [database documentation](help/database)
                diff --git a/doc/database/db_user-contact.md b/doc/database/db_user-contact.md
                index bfba9cb651..e52d8d495c 100644
                --- a/doc/database/db_user-contact.md
                +++ b/doc/database/db_user-contact.md
                @@ -6,29 +6,29 @@ User specific public contact data
                 Fields
                 ------
                 
                -| Field                     | Description                                                             | Type               | Null | Key | Default | Extra |
                -| ------------------------- | ----------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | ----- |
                -| cid                       | Contact id of the linked public contact                                 | int unsigned       | NO   | PRI | 0       |       |
                -| uid                       | User id                                                                 | mediumint unsigned | NO   | PRI | 0       |       |
                -| uri-id                    | Id of the item-uri table entry that contains the contact url            | int unsigned       | YES  |     | NULL    |       |
                -| blocked                   | Contact is completely blocked for this user                             | boolean            | YES  |     | NULL    |       |
                -| ignored                   | Posts from this contact are ignored                                     | boolean            | YES  |     | NULL    |       |
                -| collapsed                 | Posts from this contact are collapsed                                   | boolean            | YES  |     | NULL    |       |
                -| hidden                    | This contact is hidden from the others                                  | boolean            | YES  |     | NULL    |       |
                -| is-blocked                | User is blocked by this contact                                         | boolean            | YES  |     | NULL    |       |
                -| channel-frequency         | Controls the frequency of the appearance of this contact in channels    | tinyint unsigned   | YES  |     | NULL    |       |
                -| pending                   |                                                                         | boolean            | YES  |     | NULL    |       |
                -| rel                       | The kind of the relation between the user and the contact               | tinyint unsigned   | YES  |     | NULL    |       |
                -| info                      |                                                                         | mediumtext         | YES  |     | NULL    |       |
                -| notify_new_posts          |                                                                         | boolean            | YES  |     | NULL    |       |
                -| remote_self               | 0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare    | tinyint unsigned   | YES  |     | NULL    |       |
                -| fetch_further_information | 0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both | tinyint unsigned   | YES  |     | NULL    |       |
                -| ffi_keyword_denylist      |                                                                         | text               | YES  |     | NULL    |       |
                -| subhub                    |                                                                         | boolean            | YES  |     | NULL    |       |
                -| hub-verify                |                                                                         | varbinary(383)     | YES  |     | NULL    |       |
                -| protocol                  | Protocol of the contact                                                 | char(4)            | YES  |     | NULL    |       |
                -| rating                    | Automatically detected feed poll frequency                              | tinyint            | YES  |     | NULL    |       |
                -| priority                  | Feed poll priority                                                      | tinyint unsigned   | YES  |     | NULL    |       |
                +| Field                     | Description                                                                | Type               | Null | Key | Default | Extra |
                +| ------------------------- | -------------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | ----- |
                +| cid                       | Contact id of the linked public contact                                    | int unsigned       | NO   | PRI | 0       |       |
                +| uid                       | User id                                                                    | mediumint unsigned | NO   | PRI | 0       |       |
                +| uri-id                    | Id of the item-uri table entry that contains the contact url               | int unsigned       | YES  |     | NULL    |       |
                +| blocked                   | Contact is completely blocked for this user                                | boolean            | YES  |     | NULL    |       |
                +| ignored                   | Posts from this contact are ignored                                        | boolean            | YES  |     | NULL    |       |
                +| collapsed                 | Posts from this contact are collapsed                                      | boolean            | YES  |     | NULL    |       |
                +| hidden                    | This contact is hidden from the others                                     | boolean            | YES  |     | NULL    |       |
                +| channel-only              | This contact is displayed only in channels, but not in the network stream. | boolean            | YES  |     | NULL    |       |
                +| is-blocked                | User is blocked by this contact                                            | boolean            | YES  |     | NULL    |       |
                +| channel-frequency         | Controls the frequency of the appearance of this contact in channels       | tinyint unsigned   | YES  |     | NULL    |       |
                +| pending                   |                                                                            | boolean            | YES  |     | NULL    |       |
                +| rel                       | The kind of the relation between the user and the contact                  | tinyint unsigned   | YES  |     | NULL    |       |
                +| info                      |                                                                            | mediumtext         | YES  |     | NULL    |       |
                +| notify_new_posts          |                                                                            | boolean            | YES  |     | NULL    |       |
                +| remote_self               | 0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare       | tinyint unsigned   | YES  |     | NULL    |       |
                +| fetch_further_information | 0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both    | tinyint unsigned   | YES  |     | NULL    |       |
                +| ffi_keyword_denylist      |                                                                            | text               | YES  |     | NULL    |       |
                +| hub-verify                |                                                                            | varbinary(383)     | YES  |     | NULL    |       |
                +| protocol                  | Protocol of the contact                                                    | char(4)            | YES  |     | NULL    |       |
                +| rating                    | Automatically detected feed poll frequency                                 | tinyint            | YES  |     | NULL    |       |
                +| priority                  | Feed poll priority                                                         | tinyint unsigned   | YES  |     | NULL    |       |
                 
                 Indexes
                 ------------
                diff --git a/doc/database/db_user.md b/doc/database/db_user.md
                index c1aefd6bee..eb8ed06532 100644
                --- a/doc/database/db_user.md
                +++ b/doc/database/db_user.md
                @@ -27,15 +27,11 @@ Fields
                 | theme                    | user theme preference                                                             | varchar(255)       | NO   |     |                     |                |
                 | pubkey                   | RSA public key 4096 bit                                                           | text               | YES  |     | NULL                |                |
                 | prvkey                   | RSA private key 4096 bit                                                          | text               | YES  |     | NULL                |                |
                -| spubkey                  |                                                                                   | text               | YES  |     | NULL                |                |
                -| sprvkey                  |                                                                                   | text               | YES  |     | NULL                |                |
                 | verified                 | user is verified through email                                                    | boolean            | NO   |     | 0                   |                |
                 | blocked                  | 1 for user is blocked                                                             | boolean            | NO   |     | 0                   |                |
                 | blockwall                | Prohibit contacts to post to the profile page of the user                         | boolean            | NO   |     | 0                   |                |
                 | hidewall                 | Hide profile details from unknown viewers                                         | boolean            | NO   |     | 0                   |                |
                 | blocktags                | Prohibit contacts to tag the post of this user                                    | boolean            | NO   |     | 0                   |                |
                -| unkmail                  | Permit unknown people to send private mails to this user                          | boolean            | NO   |     | 0                   |                |
                -| cntunkmail               |                                                                                   | int unsigned       | NO   |     | 10                  |                |
                 | notify-flags             | email notification options                                                        | smallint unsigned  | NO   |     | 65535               |                |
                 | page-flags               | page/profile type                                                                 | tinyint unsigned   | NO   |     | 0                   |                |
                 | account-type             |                                                                                   | tinyint unsigned   | NO   |     | 0                   |                |
                diff --git a/doc/de/Addons.md b/doc/de/Addons.md
                index 0843c103ab..bfc0107528 100644
                --- a/doc/de/Addons.md
                +++ b/doc/de/Addons.md
                @@ -359,10 +359,10 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
                 
                     Hook::callAll('register_account', $uid);
                     Hook::callAll('remove_user', $user);
                -    
                +
                 ### src/Content/ContactBlock.php
                 
                -    Hook::callAll('contact_block_end', $arr);
                +    Hook::callAll('contact_block_end', $text);
                 
                 ### src/Content/Text/BBCode.php
                 
                @@ -418,10 +418,6 @@ Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 01-Ap
                     Hook::callAll('storage_instance', $data);
                     Hook::callAll('storage_config', $data);
                 
                -### src/Module/Notifications/Ping.php
                -
                -    Hook::callAll('network_ping', $arr);
                -
                 ### src/Module/PermissionTooltip.php
                 
                     Hook::callAll('lockview_content', $item);
                diff --git a/doc/de/BBCode.md b/doc/de/BBCode.md
                index a6df8b67b9..068af666d1 100644
                --- a/doc/de/BBCode.md
                +++ b/doc/de/BBCode.md
                @@ -356,8 +356,8 @@ Zeilen
                   [li] Zweites Listenelement
                [/ul]
                [list]
                -  [*] Erstes Listenelement
                -  [*] Zweites Listenelement
                +  [li] Erstes Listenelement
                +  [li] Zweites Listenelement
                [/list]
                  @@ -368,12 +368,12 @@ Zeilen [ol]
                  -  [*] Erstes Listenelement
                  -  [*] Zweites Listenelement
                  +  [li] Erstes Listenelement
                  +  [li] Zweites Listenelement
                  [/ol]
                  [list=1]
                  -  [*] Erstes Listenelement
                  -  [*] Zweites Listenelement
                  +  [li] Erstes Listenelement
                  +  [li] Zweites Listenelement
                  [/list]
                    @@ -384,8 +384,8 @@ Zeilen [list=]
                    -  [*] Erstes Listenelement
                    -  [*] Zweites Listenelement
                    +  [li] Erstes Listenelement
                    +  [li] Zweites Listenelement
                    [/list]
                      @@ -396,8 +396,8 @@ Zeilen [list=i]
                      -  [*] Erstes Listenelement
                      -  [*] Zweites Listenelement
                      +  [li] Erstes Listenelement
                      +  [li] Zweites Listenelement
                      [/list]
                        @@ -408,8 +408,8 @@ Zeilen [list=I]
                        -  [*] Erstes Listenelement
                        -  [*] Zweites Listenelement
                        +  [li] Erstes Listenelement
                        +  [li] Zweites Listenelement
                        [/list]
                          @@ -420,8 +420,8 @@ Zeilen [list=a]
                          -  [*] Erstes Listenelement
                          -  [*] Zweites Listenelement
                          +  [li] Erstes Listenelement
                          +  [li] Zweites Listenelement
                          [/list]
                            @@ -432,8 +432,8 @@ Zeilen [list=A]
                            -  [*] Erstes Listenelement
                            -  [*] Zweites Listenelement
                            +  [li] Erstes Listenelement
                            +  [li] Zweites Listenelement
                            [/list]
                              diff --git a/doc/de/Channels.md b/doc/de/Channels.md new file mode 100644 index 0000000000..686fd129b1 --- /dev/null +++ b/doc/de/Channels.md @@ -0,0 +1,98 @@ +Kanäle (Channels) +===== + +* [Home](help) + +Kanäle sind eine Möglichkeit neue Inhalte zu finden, oder Inhalte anzuzeigen, die du sonst möglicherweise verpasst hättest. +Es gibt mehrere vordefinierte Kanäle und zusätzlich kannst du deine eigenen, basierend auf ein paar Regeln, erstellen. +Kanäle zeigen nur Beiträge aus den letzten 24 Stunden an. (Dieser Wert kann vom Administrator geändert werden.) + +In den Anzeige-Einstellungen, im Bereich "Timelines", kannst du definieren, welche Kanäle und andere Timelines du im "Kanäle"-Widget auf der "Network"-Seite sehen möchtest und welche Kanäle in der Menüleiste oben auf der Seite erscheinen sollen. + +Ebenfalls in den Anzeige-Einstellungen, im Bereich "Kanäle", kannst du alle die Sprachen einstellen, die du in deinen Kanälen sehen möchtest. Hier kannst du mehr als eine Sprache auswählen. + +Auf der Profilseite kannst du die Kanal-Frequenz für jeden Kontakt definieren. Die Optionen sind: + +* Standardhäufigkeit: Beiträge dieses Kontakts werden im "Für Dich"-Kanal angezeigt, wenn du häufig mit diesem Kontakt interagiert hast oder wenn ein Beitrag ein gewisses Maß an Interaktion erreicht hat. +* Alle Beiträge dieses Kontakts anzeigen: Alle Beiträge dieses Kontakts werden auf dem Kanal "Für Dich" erscheinen +* Zeige nur einige Beiträge an: Wenn ein Kontakt viele Beiträge in einem kurzen Zeitraum erstellt, reduziert diese Einstellung die Anzahl der angezeigten Beiträge in jedem Kanal. +* Zeige keine Beiträge an: Beiträge von diesem Kontakt werden in keinem Kanal angezeigt. + +Voreingestellte Kanäle +--- + +* Für Dich: Beiträge von Kontakten mit denen du interagierst und die mit dir interagieren. Im Detail bestehend aus: + * Beiträge von Leuten, mit denen du überdurchschnittlich viel interagierst. + * Beiträge von Kontakten, denen du folgst und mit denen du überdurchschnittlich viel interagierst. + * Beiträge von Kontakten, bei denen du "Benachrichtigung bei neuen Beiträgen" aktiviert hast oder wo du die Kanalfrequenz entsprechend eingestellt hast. +* Entdecken: Beiträge von Kontakten denen du nicht folgst, aber denen zu folgen für dich interessant sein könnte. Im Detail bestehend aus: + * Beiträge von Leuten denen du nicht folgst, aber mit denen du überdurchschnittlich viel interagierst. + * Beiträge von Leuten denen du nicht folgst, aber die mit dir überdurchschnittlich viel interagieren. + * Beliebte Beiträge von Leuten denen du nicht folgst, aber mit denen du interagiert hast oder die mit dir interagiert haben. +* Angesagt: Beiträge mit überdurchschnittlich hoher Anzahl von Interaktionen. +* Sprache: Beiträge in deiner Sprache. +* Folgende: Beiträge von Leuten die dir folgen, aber denen du nicht folgst. +* Geteilt von teilenden: Beiträge von Kontakten denen die Leute folgen, denen du folgst. +* Ruhige teilende: Beiträge von Konten denen du folgst, aber die nicht sehr oft posten. +* Bilder: Beiträge mit Bildern. +* Audio: Beiträge mit Audio. +* Videos: Beiträge mit Videos. + +Vom Benutzer eingestellte Kanäle +--- + +In den Einstellungen, unter "Kanäle", kannst du deine eigenen Kanäle erstellen. + +Jeder Kanal wird durch diese Werte definiert: + +* Bezeichnung: Dieses Feld ist notwendig und wird für die Kanalbezeichnung verwendet. +* Beschreibung: Eine kurze Beschreibung des Inhalts. Dies kann helfen den Überblick zu behalten, wenn du viele Kanäle hast. +* Zugriffsschlüssel: Wenn du auf diesen Kanal über einen Zugriffsschlüssel zugreifen willst, kannst du ihn hier festlegen. Achte darauf, dass du nicht einen bereits verwendeten Schlüssel benutzt. +* Circle/Kanal: Dies definiert die Datenquelle für diesen Kanal. Voreingestellt ist die Globale Gemeinschaft. Es gibt ein paar vorgegebene Werte, wie die Konten denen du folgst, oder die Kontakte, die dir folgen. Außerdem können alle deine Circles ausgewählt werden. +* Tags einschließen: Durch Kommata getrennte Liste von Tags. Ein Beitrag wird verwendet, wenn er eines der aufgeführten Tags enthält. +* Tags ausschließen: Durch Kommata getrennte Liste von Tags. Wenn ein Beitrag eines dieser Tags enthält, wird er nicht Teil dieses Kanals sein. +* Volltextsuche: Dies kann genutzt werden um Inhalte, basierend auf dem Inhalt und ein paar zusätzlichen Schlüsselwörtern, ein- oder auszuschließen. Es nutzt die "boolean mode"-Operatoren von MariaDB: https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode +* Bilder, Videos, Audio: Wenn ausgewählt, wirst du Inhalte mit dem gewählten Medientyp sehen. Diese Optionen können kombiniert werden. Wenn keines dieser Felder ausgewählt wurde, wirst du alle Inhalte, mit oder ohne angefügten Medien, sehen. + +Zusätzliche Schlüsselwörter für die Volltextsuche +--- + +Zusätzlich zu der Suche nach Inhalten, gibt es Schlüsselwörter, die in der Volltextsuche genutzt werden können. +Alternativen werden durch "|" dargestellt. + +* from - Verwende "from:nickname" oder "from:nickname@domain.tld" um nach Beiträgen von einem bestimmten Autor zu suchen. +* to - Verwende "to:nickname" oder "to:nickname@domain.tld" um nach Beiträgen mit dem gegebenen Empfänger zu suchen. +* group - Verwende "group:nickname" oder "group:nickname@domain.tld" um nach Beiträgen aus der gegebenen Gruppe zu suchen. +* application | relay - Nutze "application:nickname" oder "application:nickname@domain.tld" um Beiträge zu finden, die von der gegebenen relay application geteilt wurden. +* server - Verwende "server:hostname" um Beiträge von einem bestimmten Server zu suchen. Im Falle eine Gruppen-Postings enthält der Suchtext beides, den Hostname des Gruppen-Servers und den Hostname des Autors. +* source - Der ActivityPub-Typ der Beitragsquelle. Nutze dies um beispielsweise Gruppenpostings oder Beiträge von Services (aka Bots) ein- oder auszuschließen. + * source:person - Der Beitrag wurde von einem regulären Nutzerkonto erstellt. + * source:organization - Der Beitrag wurde von einer Organisation erstellt. + * source:group - Dieser Beitrag wurde über eine Gruppe erstellt oder verteilt. + * source:service | source:news - Dieser Beitrag stammt aus einem 'service' Account. Dieser Quellen(source)-Typ wird oft genutzt um Bot Accounts zu markieren. + * source:application | source:relay - Dieser Beitrag wurde von einer Anwendung (application) erstellt. Dies wird im Fediverse höchstwahrscheinlich für die Beitragserstellung nicht genutzt. +* tag - Nutze "tag:tagname" um nach einem bestimmten tag (Schlagwort) zu suchen. +* media - Mit diesem Schlüsselwort kannst du nach angefügten Medien suchen. + * media:image | media:photo | media:picture - Dieser Beitrag enthält ein Bild + * media:video - Dieser Beitrag enthält ein Video + * media:audio - Dieser Beitrag enthält Audio + * media:card - Dieser Beitrag enthält eine Linkvorschau-'card' + * media:post - Dieser Beitrag verweist auf einen anderen Beitrag, was bedeutet, es ist ein zitierter Beitrag +* network | net - Verwende dies um Netzwerke in deinen Kanal einzuschließen oder von ihm auszuschließen. + * network:apub | network:activitypub - ActivityPub (verwendet von den Systemen im Fediverse) + * network:dfrn | network:friendica - altes Friendica-Protokoll. Heutzutage nutzt Friendica meist ActivityPub. + * network:dspr | network:diaspora - Das Diaspora-Protokoll wird hauptsächlich von Diaspora selbst genutzt. Ein paar andere Systeme unterstützen dieses Protokoll ebenfalls, wie Hubzilla, Socialhome or Ganggo. + * network:feed - RSS/Atom feeds + * network:mail - Mails die via IMAP importiert worden sind. + * network:dscs | network:discourse - Beiträge, die über den Discourse connector empfangen werden. + * network:tmbl | network:tumblr - Beiträge, die über den Tumblr connector empfangen werden. + * network:bsky | network:bluesky - Beiträge, die über den Bluesky connector empfangen werden. +* platform - Benutze dies, um Plattformen in deinen Kanal einzuschließen, oder von ihm auszuschließen, d.h. "+platform:friendica". Im Falle eines Gruppen-Postings enthält der Suchtext beides, die Plattform des Gruppen-Servers und die Plattform des Autors. +* visibility - Du hast die Wahl zwischen verschiedenen Sichtbarkeiten. Du kannst nur die ungelisteten oder privaten Beiträge sehen, zu denen du Zugang hast. + * visibility:public - (öffentlich) + * visibility:unlisted - (ungelistet) + * visibility:private - (privat) +* language | lang - Verwende "language:code" um nach Beiträgen in der gewünschten Sprache (im [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format) zu suchen. + +Denke daran, dass du diese Schlüsselwörter kombinieren kannst. +So kannst du zum Beispiel einen Kanal erstellen, mit allen Beiträgen, die über das Fediverse sprechen, aber nicht im Fediverse veröffentlich wurden, mit diesen Suchbegriffen: "fediverse -network:apub -network:dfrn". diff --git a/doc/de/Circles-and-Privacy.md b/doc/de/Circles-and-Privacy.md index aebaff8715..dcb5f3259f 100644 --- a/doc/de/Circles-and-Privacy.md +++ b/doc/de/Circles-and-Privacy.md @@ -97,13 +97,6 @@ Wir nehmen hingegen Privatsphäre ernst und agieren nicht wie andere Netzwerke, Dein Profil und deine "Wall" sollen vielleicht auch von Freunden anderer Netzwerke besucht werden können. Wenn du diese Seiten allerdings für Webbesucher sperrst, die Friendica nicht kennt, kann das auch Freunde anderer Netzwerke blockieren. -Das kann möglicherweise ungewollte Ergebnisse produzieren, wenn du lange Statusbeiträge z.B. für Twitter oder Facebook schreibst. -Wenn Friendica einen Beitrag an diese Netzwerke schickt und nur eine bestimmte Nachrichtenlänge erlaubt ist, dann verkürzen wir diesen und erstellen einen Link, der zum Originalbeitrag führt. -Der Originallink führt zurück zu deinem Friendica-Profil. -Da Friendica nicht bestätigen kann, um wen es sich handelt, kann es passieren, dass diese Leute den Beitrag nicht komplett lesen können. - -Für Leute, die davon betroffen sind, schlagen wir vor, eine Zusammenfassung in Twitter-Länge zu erstellen mit mehr Details für Freunde, die den ganzen Beitrag sehen können. - Dein Profil oder deine gesamte Friendica-Seite zu blockieren, hat außerdem ernsthafte Einflüsse auf deine Kommunikation mit GNU Social-Nutzern. Diese Netzwerke kommunizieren mit anderen über öffentliche Protokolle, die nicht authentifiziert werden. Um deine Beiträge zu sehen, müssen diese Netzwerke deine Beiträge als "unbekannte Webbesucher" ansehen. diff --git a/doc/de/Connectors.md b/doc/de/Connectors.md index 7247124ef3..fe2d739fd5 100644 --- a/doc/de/Connectors.md +++ b/doc/de/Connectors.md @@ -4,15 +4,10 @@ Konnektoren (Connectors) * [Zur Startseite der Hilfe](help) Konnektoren erlauben es Dir, Dich mit anderen sozialen Netzwerken zu verbinden. -Konnektoren werden nur bei bestehenden Twitter und GNU Social-Accounts benötigt. +Mit diesen Konnektoren kannst Du z.B. zu Bluesky, Tumblr oder Twitter posten. +Für Bluesky und Tumblr gibt es eine bidirektionale Verbindung, d.h. du kannst Friendica nutzen, um deine Timeline von diesen Diensten zu lesen. Außerdem gibt es einen Konnektor, um Deinen Email-Posteingang zu nutzen. -Wenn Du keinen eigenen Knoten betreibst und wissen willst, ob der server Deiner Wahl diese Konnektoren installiert hat, kannst Du Dich darüber auf der Seite '<domain_des_friendica-servers>/friendica' informieren. - -Sind die Netzwerk-Konnektoren auf Deinem System installiert sind, kannst Du mit den folgenden Links die Einstellungsseiten besuchen und für Deinen Account konfigurieren: - -* [Twitter](/settings/connectors) -* [GNU Social](/settings/connectors) -* [Email](/settings/connectors) +Wenn Du keinen eigenen Knoten betreibst und wissen willst, ob der Server Deiner Wahl diese Konnektoren installiert hat, kannst Du Dich darüber auf der Seite '<domain_des_friendica-servers>/friendica' informieren. Anleitung, um sich mit Personen in bestimmten Netzwerken zu verbinden ========================================================== @@ -27,24 +22,6 @@ Ebenso kannst Du deren Identitäts-Adresse in der "Verbinden"-Box auf Deiner ["K Füge die Diaspora-Identitäts-Adresse (z.B. name@diasporapod.com)auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein. - -**GNU Social** - -Dieses Netzwerk wird als "federated social web" bzw. "OStatus"-Kontakte bezeichnet. - -Bitte beachte, dass es **keine** Einstellungen zur Privatssphäre im OStatus-Netzwerk gibt. -**Jede** Nachricht, die an eines dieser OStatus-Mitglieder verschickt wird, ist für jeden auf der Welt sichtbar; alle Privatssphäreneinstellungen verlieren ihre Wirkung. -Diese Nachrichten erscheinen ebenfalls in öffentlichen Suchergebnissen. - -Da die OStatus-Kommunikation keine Authentifizierung benutzt, können OStatus-Nutzer *keine* Nachrichten empfangen, wenn Du in Deinen Privatssphäreneinstellungen "Profil und Nachrichten vor Unbekannten verbergen" wählst. - -Um Dich mit einem OStatus-Mitglied zu verbinden, trage deren Profil-URL oder Identitäts-Adresse auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein. - -Der GNU Social-Konnektor kann genutzt werden, wenn Du Beiträge schreiben willst, die auf einer OStatus-Seite über einen existierenden OStatus-Account erscheinen sollen. - -Das ist nicht notwendig, wenn Du OStatus-Mitgliedern von Friendica aus folgst und diese Dir auch folgen, indem sie auf Deiner Kontaktseite ihre eigene Identitäts-Adresse eingeben. - - **Blogger, Wordpress, RSS feeds, andere Webseiten** Trage die URL auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein. @@ -53,14 +30,6 @@ Du hast keine Möglichkeit, diesen Kontakten zu antworten. Das erlaubt Dir, Dich mit Millionen von Seiten im Internet zu _verbinden_. Alles, was dafür nötig ist, ist dass die Seite einen Feed im RSS- oder Atom Syndication-Format nutzt und welches einen Autoren und ein Bild zur Seite liefert. - -**Twitter** - -Um einem Twitter-Nutzer zu folgen, trage die URL der Hauptseite des Twitter-Accounts auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein. -Um zu antworten, musst Du den Twitter-Konnektor installieren und über Deinen eigenen Status-Editor antworten. -Beginne Deine Nachricht mit @twitternutzer, ersetze das aber durch den richtigen Twitter-Namen. - - **Email** Konfiguriere den Email-Konnektor auf Deiner [Einstellungsseite](settings). diff --git a/doc/de/Export-Import-Contacts.md b/doc/de/Export-Import-Contacts.md index 6d814edcb4..b58752c9be 100644 --- a/doc/de/Export-Import-Contacts.md +++ b/doc/de/Export-Import-Contacts.md @@ -11,8 +11,7 @@ Um die Liste der Kontakte *denen du folgst* zu exportieren, geht die [Einstellun ## Import der gefolgten Kontakte -Um die Kontakt CSV Datei zu importieren, gehe in die [Einstellungen](settings). -Am Ende der Einstellungen zum Nutzerkonto findest du den Abschnitt "Kontakte Importieren". +Um die Kontakt CSV Datei zu importieren, gehe zu [Kontakte Importieren](settings/importcontacts). Hier kannst du die CSV Datei auswählen und hoch laden. ### Unterstütztes Datei Format diff --git a/doc/de/Home.md b/doc/de/Home.md index 6cd6e5ea87..6ea7937ea3 100644 --- a/doc/de/Home.md +++ b/doc/de/Home.md @@ -1,7 +1,7 @@ -Friendica - Dokumentation und Ressourcen -===================================== +Hilfe +===== -**Inhalte** +**Dokumentation für Benutzer** * Allgemeine Funktionen - Erste Schritte * [Account - Basics](help/Account-Basics) @@ -17,7 +17,7 @@ Friendica - Dokumentation und Ressourcen * [Circles und Privatsphäre](help/Circles-and-Privacy) * [Tags und Erwähnungen](help/Tags-and-Mentions) * [Community-Gruppen](help/Groups) - * [Channels](help/Channels) + * [Kanäle (Channels)](help/Channels) * [Chats](help/Chats) * Weiterführende Informationen * [Account umziehen](help/Move-Account) @@ -32,7 +32,7 @@ Friendica - Dokumentation und Ressourcen * [Update](help/Update) (EN) * [Konfigurationen & Admin-Panel](help/Settings) * [Addons](help/Addons) -* [Konnektoren (Connectors) installieren (Twitter/GNU Social)](help/Installing-Connectors) +* [Konnektoren (Connectors) installieren](help/Installing-Connectors) * [Installation eines ejabberd Servers (XMPP-Chat) mit synchronisierten Anmeldedaten](help/install-ejabberd) (EN) * [Betreibe deine Seite mit einem SSL-Zertifikat](help/SSL) * [Konfigurationswerte, die nur in der config/local.config.php gesetzt werden können](help/Config) (EN) @@ -60,18 +60,17 @@ Friendica - Dokumentation und Ressourcen * [Translation of Friendica](help/translations) (EN) * [Run tests](help/Tests) (EN) -**Externe Ressourcen** +**Links** -* [Haupt-Webseite](https://friendi.ca) -* Support Kanäle - * Friendica Support Gruppe: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers) - * [Mailing Listen Archiv](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca) zum Abonnieren der Liste eine E-Mail an ``support-request(at)friendi.ca?subject=subscribe`` senden - * Chats der Friendica Community (die IRC, Matrix und XMPP Räume sind mit einer Brücke verbunden) Logs dieser öffentlichen Chaträume können [hier aus dem IRC](https://gnusociarg.nsupdate.info/2021/%23frie) und [hier aus der Matrix](https://view.matrix.org/alias/%23friendi.ca:matrix.org/) gefunden werden. - * XMPP/Jabber MUC: support(at)forum.friendi.ca - * IRC: #friendica auf [libera.chat](https://web.libera.chat/?channels=#friendica) - * Matrix: [#friendi.ca](https://matrix.to/#/#friendi.ca:matrix.org) oder [#friendica-en](https://matrix.to/#/#friendica-en:matrix.org) auf matrix.org +* Website: [https://friendi.ca](https://friendi.ca) +* Help Group: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers) +* XMPP: [support@forum.friendi.ca](xmpp:support@forum.friendi.ca?join) +* IRC: [https://web.libera.chat/?channels=#friendica](https://web.libera.chat/?channels=#friendica) +* Matrix: [https://matrix.to/#/#friendi.ca:matrix.org](https://matrix.to/#/#friendi.ca:matrix.org) +* Mailing List: [https://mailman.friendi.ca/mailman/listinfo/support-friendi.ca](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca) -**Über diese Seite** +**Über** -* [Seite/Friendica-Version](friendica) -* [Mitwirkenden bei Friendica](credits) +* [Server Information](friendica) +* [Nutzungsbedingungen](tos) +* [Mitwirkende](credits) diff --git a/doc/de/Install.md b/doc/de/Install.md index 0122988c59..205127e34e 100644 --- a/doc/de/Install.md +++ b/doc/de/Install.md @@ -27,7 +27,7 @@ Requirements * Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale .htaccess-Datei nutzen kannst * PHP 7.4+ * PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei - * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar and OpenSSL-Erweiterung + * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar, IDN und OpenSSL-Erweiterung * Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert) * Einen E-Mail Server, so dass PHP `mail()` funktioniert. Wenn kein eigener E-Mail Server zur Verfügung steht, kann alternativ das [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) Addon mit einem externen SMTP Account verwendet werden. @@ -45,6 +45,10 @@ Falls du an automatischen Möglichkeiten interesse hast, wirf doch einen Blick a * das [Docker image für Friendica](https://github.com/friendica/docker) oder * die [Installation von Friendica auf YunoHost](https://github.com/YunoHost-Apps/friendica_ynh). +* [Tutorial: Creating a Friendica Server with Ubuntu 22.04](https://nequalsonelifestyle.com/2022/07/30/creating-friendica-server-ubuntu/) + * [Setting Up Friendica Daemon as a Systemd Service Tutorial](https://nequalsonelifestyle.com/2022/08/04/setting-up-friendica-daemon-systemd-service/) +* [Setting up Friendica on Unraid](https://www.jenovarain.com/2023/03/setting-up-friendica-on-unraid/) (NAS) +* [Installing Friendica with Elastio](https://elest.io/open-source/friendica) ### Friendica @@ -55,7 +59,7 @@ Der Linux-Code, mit dem man die Dateien direkt in ein Verzeichnis wie "meinewebs git clone https://github.com/friendica/friendica.git -b stable mywebsite cd mywebsite - bin/composer.phar install + bin/composer.phar run install:prod Stelle sicher, dass der Ordner *view/smarty3* existiert and von dem Webserver-Benutzer beschreibbar ist @@ -81,7 +85,7 @@ Wenn du die Entwickler Version von Friendica verwenden möchtest kannst du auf d Dies tust du mit den folgenden Befehlen git checkout develop - bin/composer.phar install + bin/composer.phar run install:prod cd addon git checkout develop @@ -206,13 +210,13 @@ Gehe in den Friendica-Hauptordner und führe den Kommandozeilen Befehl aus: Erstelle einen Cron job oder einen regelmäßigen Task, um den Poller alle 5-10 Minuten im Hintergrund ablaufen zu lassen. Beispiel: - cd /base/directory; /path/to/php bin/worker.php + cd /base/directory; /path/to/php bin/console.php worker Ändere "/base/directory" und "/path/to/php" auf deine Systemvorgaben. Wenn du einen Linux-Server nutzt, benutze den Befehl "crontab -e" und ergänze eine Zeile wie die Folgende; angepasst an dein System -`*/10 * * * * cd /home/myname/mywebsite; /usr/bin/php bin/worker.php` +`*/10 * * * * cd /home/myname/mywebsite; /usr/bin/php bin/console.php worker` Du kannst den PHP-Pfad finden, indem du den Befehl „which php“ ausführst. Wenn du Schwierigkeiten mit diesem Schritt hast, kannst du deinen Hosting-Anbieter kontaktieren. diff --git a/doc/de/Installing-Connectors.md b/doc/de/Installing-Connectors.md index 9025d3d73b..7d378e2374 100644 --- a/doc/de/Installing-Connectors.md +++ b/doc/de/Installing-Connectors.md @@ -1,85 +1,33 @@ -Konnektoren installieren (Twitter/GNU Social) +Konnektoren installieren ================================================== * [Zur Startseite der Hilfe](help) -Friendica nutzt Erweiterung, um die Verbindung zu anderen Netzwerken wie Twitter oder App.net zu gewährleisten. - -Es gibt außerdem ein Erweiterung, um über einen bestehenden GNU Social-Account diesen Service zu nutzen. -Du brauchst dieses Erweiterung aber nicht, um mit GNU Social-Mitgliedern von Friendica aus zu kommunizieren - es sei denn, du wünschst es, über einen existierenden Account einen Beitrag zu schreiben. - -Alle drei Erweiterung benötigen einen Account im gewünschten Netzwerk. -Zusätzlich musst du (bzw. der Administrator der Seite) einen API-Schlüssel holen, um einen authentifizierten Zugriff zu deinem Friendica-Server herstellen zu lassen. +Friendica verwendet Konnektoren, um sich mit einigen Netzwerken zu verbinden, wie Tumblr oder Bluesky. +Alle diese Konnektoren erfordern einen Account im Zielnetzwerk. +Außerdem musst du (oder die Server-Administration) in der Regel einen API-Schlüssel erhalten, um die Verbindung zu ermöglichen. **Seitenkonfiguration** -Erweiterung müssen vom Administrator installiert werden, bevor sie genutzt werden können. -Dieses kann über das Administrationsmenü erstellt werden. +Konnektoren müssen von der Server-Administration installiert werden, bevor sie verwendet werden können. +Dies geschieht über die Server-Verwaltung. -Jeder der Konnektoren benötigt zudem einen API-Schlüssel vom Service, der verbunden werden soll. -Einige Erweiterung erlaube es, diese Informationen auf den Administrationsseiten einzustellen, wohingegen andere eine direkte Bearbeitung der Konfigurationsdatei "config/local.config.php" erfordern. -Der Weg, um diese Schlüssel zu erhalten, variiert stark, jedoch brauchen fast alle einen bestehenden Account im gewünschten Service. -Einmal installiert, können diese Schlüssel von allen Seitennutzern genutzt werden. +Einige der Konnektoren erfordern auch einen „API-Schlüssel“ des Dienstes, mit dem du dich verbinden möchtest. +Für Tumblr findet man diese Informationen auf den Seiten der Server-Verwaltung, während für Twitter (X) jede Person einen eigenen API-Schlüssel erstellen muss. +Andere Konnektoren, wie Bluesky, benötigen überhaupt keinen API-Schlüssel. -Im Folgenden findest du die Einstellungen für die verschiedenen Services (viele dieser Informationen kommen direkt aus den Quelldateien der Erweiterung): +Weitere Informationen zu den spezifischen Anforderungen findest du auf der Einstellungsseite des jeweiligen Addons, entweder auf der Verwaltungsseite oder auf der Benutzerseite. +Bluesky Jetstream +--- -**Twitter Erweiterung für Friendica** +Um die Konnektivität mit Bluesky weiter zu verbessern, kann die „Jetstream“-Konnektivität aktiviert werden. +Jetstream ist ein Dienst, der sich mit dem Bluesky-Firehose verbindet. +Mit Jetstream kommen die Nachrichten in Echtzeit an und müssen nicht erst abgefragt werden. +Es ermöglicht auch die Echtzeitverarbeitung von Blöcken oder Tracking-Aktivitäten, die über die Bluesky-Website oder -Anwendung durchgeführt werden. -* Author: Tobias Diekershoff -* tobias.diekershoff@gmx.net +Um die Jetstream-Verarbeitung zu aktivieren, führe `bin/console.php daemon' über die Befehlszeile aus. +Du musst vorher die Prozess-ID-Datei in local.config.php im Abschnitt „jetstream“ mit dem Schlüssel „pidfile“ definieren. -* License:3-clause BSD license - -Konfiguration: -Um dieses Erweiterung zu nutzen, benötigst du einen OAuth Consumer-Schlüsselpaar (Schlüssel und Geheimnis), das du auf der Seite [https://twitter.com/apps](https://twitter.com/apps) erhalten kannst - -Registriere deine Friendica-Seite als "Client"-Anwendung mit "Read&Write"-Zugriff. Wir benötigen "Twitter als Login" nicht. Sobald du deine Anwendung installiert hast, erhältst du das Schlüsselpaar für deine Seite. - -Trage dieses Schlüsselpaar in deine globale "config/local.config.php"-Datei ein. - -``` -[twitter] -consumerkey = your consumer_key here -consumersecret = your consumer_secret here -``` - -Anschließend kann der Nutzer deiner Seite die Twitter-Einstellungen selbst eintragen: "Einstellungen -> Connector Einstellungen". - - -**GNU Social Erweiterung für Friendica** - -* Author: Tobias Diekershoff -* tobias.diekershoff@gmx.net - -* License:3-clause BSD license - -Konfiguration - -Wenn das Addon aktiv ist, muss der Nutzer die folgenden Einstellungen vornehmen, um sich mit dem GNU Social-Account seiner Wahl zu verbinden. - -* Die Basis-URL des GNU Social-API; für quitter.se ist es https://quitter.se/api/ -* OAuth Consumer key & Geheimnis - -Um das OAuth-Schlüsselpaar zu erhalten, muss der Nutzer - -(a) seinen Friendica-Admin fragen, ob bereits ein Schlüsselpaar existiert oder -(b) einen Friendica-Server als Anwendung auf dem GNU Social-Server anmelden. - -Dies kann über Einstellungen --> Connections --> "Register an OAuth client application" -> "Register a new application" auf dem GNU Social-Server durchgeführt werden. - -Während der Registrierung des OAuth-Clients ist Folgendes zu beachten: - -* Der Anwendungsname muss auf der GNU Social-Seite einzigartig sein, daher empfehlen wir einen Namen wie "friendica-nnnn", ersetze dabei "nnnn" mit einer frei gewählten Nummer oder deinem Webseitennamen. -* es gibt keine Callback-URL -* Registriere einen Desktop-Client -* stelle Lese- und Schreibrechte ein -* die Quell-URL sollte die URL deines Friendica-Servers sein - -Sobald die benötigten Daten gespeichert sind, musst du deinen Friendica-Account mit GNU Social verbinden. -Das kannst du über Einstellungen --> Connector-Einstellungen durchführen. -Folge dem "Einloggen mit GNU Social"-Button, erlaube den Zugriff und kopiere den Sicherheitscode in die entsprechende Box. -Friendica wird dann versuchen, die abschließende OAuth-Einstellungen über die API zu beziehen. - -Wenn es geklappt hat, kannst du in den Einstellungen festlegen, ob deine öffentlichen Nachrichten automatisch in deinem GNU Social-Account erscheinen soll (achte hierbei auf das kleine Schloss-Symbol im Status-Editor) +Um die verarbeiteten Nachrichten und die Drift (die Zeitdifferenz zwischen dem Datum der Nachricht und dem Datum, an dem das System diese Nachricht verarbeitet hat) zu verfolgen, wurden dem Statistik-Endpunkt einige Felder hinzugefügt. diff --git a/doc/de/Quick-Start-makingnewfriends.md b/doc/de/Quick-Start-makingnewfriends.md index b7381cddf9..701fba505f 100644 --- a/doc/de/Quick-Start-makingnewfriends.md +++ b/doc/de/Quick-Start-makingnewfriends.md @@ -3,26 +3,26 @@ Neue Freunde finden * [Zur Startseite der Hilfe](help) -Hier siehst Du die Kontaktvorschläge. -Wenn Du Dich mal verirrt hast, kannst Du diesen Link klicken und wieder hierher kommen. +Hier siehst Du die Kontaktvorschläge. +Wenn Du Dich mal verirrt hast, kannst Du diesen Link klicken und wieder hierher kommen. -Diese Seite funktioniert in etwa wie die Seite für Kontaktvorschläge in Facebook. -Jeder auf dieser Liste hat zugestimmt, als Kontaktvorschlag zu erscheinen. +Diese Seite funktioniert in etwa wie die Seite für Kontaktvorschläge in Facebook. +Jeder auf dieser Liste hat zugestimmt, als Kontaktvorschlag zu erscheinen. Das bedeutet, das sie Anfragen meist nicht ablehnen, da sie neue Leute kennenlernen wollen. -Siehst Du jemanden, der Dir interessant erscheint? -Klicke auf den "Verbinden"-Knopf beim Foto. -Als nächstes kommst Du zur Seite "Freundschafts-/Kontaktanfrage". -Fülle das Formular wie vorgegeben aus und trage optional eine kleine Notiz ein. -Nun musst Du nur noch auf die Bestätigung warten. +Siehst Du jemanden, der Dir interessant erscheint? +Klicke auf den "Verbinden"-Knopf beim Foto. +Als nächstes kommst Du zur Seite "Freundschafts-/Kontaktanfrage". +Fülle das Formular wie vorgegeben aus und trage optional eine kleine Notiz ein. +Nun musst Du nur noch auf die Bestätigung warten. Beachte dabei, dass es sich um reale Personen handelt und es somit etwas dauern kann. -Jetzt, nachdem Du jemanden hinzugefügt hast, weißt Du vielleicht nicht mehr, wie Du zurückkommst. +Jetzt, nachdem Du jemanden hinzugefügt hast, weißt Du vielleicht nicht mehr, wie Du zurückkommst. Klicke einfach auf den Link oben auf dieser Seite und Du gelangst zur Seite mit den Kontaktvorschlägen zurück, um weitere Personen hinzuzufügen. -Du willst nicht einfach Personen hinzufügen, die du nicht kennst? +Du willst nicht einfach Personen hinzufügen, die du nicht kennst? Kein Problem - an dieser Stelle kommen wir zu den Gruppen und Seiten. - + diff --git a/doc/de/Settings.md b/doc/de/Settings.md index 34b349e885..cee965ccaf 100644 --- a/doc/de/Settings.md +++ b/doc/de/Settings.md @@ -410,7 +410,7 @@ Wir raten allerdings dringen davon ab, da es die Interoperabilität mit anderen Mal angenommen, du hast ein Unterverzeichnis tests und willst Friendica in ein weiteres Unterverzeichnis installieren, dann lautet die Konfiguration hierfür: 'system' => [ - 'urlpath' => 'tests/friendica', + 'url' => 'https://example.com/tests/friendica', ], ## Weitere Ausnahmen diff --git a/doc/stats.md b/doc/stats.md new file mode 100644 index 0000000000..1b6a2dfd2a --- /dev/null +++ b/doc/stats.md @@ -0,0 +1,35 @@ +Monitoring +=========== + +* [Home](help) + +## Endpoints + +Currently, there are two endpoints for statistics available + +- `/stats` Returns some basic statistics of the current node +- `/stats/caching` Returns statistics of cache or lock instances, which are used for the currend node + +### `/stats` + +The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments. + +### `/stats/caching` + +The statistics contain data about the opcache, the used caching (like memory usage, hits/misses, entries, ...) and the used lock (including the cache data) + +## Configuration + +Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key + +## 3rd Party monitoring tools + +### Zabbix + +To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix. + +### Prometheus + +To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter). + +You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation diff --git a/doc/themes.md b/doc/themes.md index 8d85dce2a9..5804868836 100644 --- a/doc/themes.md +++ b/doc/themes.md @@ -2,153 +2,10 @@ * [Home](help) -To change the look of friendica you have to touch the themes. -The current default theme is [Vier](https://github.com/friendica/friendica/tree/stable/view/theme/vier) but there are numerous others. -Have a look at [github.com/bkil/friendica-themes](https://github.com/bkil/friendica-themes) for an overview of the existing themes. -In case none of them suits your needs, there are several ways to change a theme. +The default Theme in Friendica is called [frio](https://github.com/friendica/friendica/tree/stable/view/theme/frio). -So, how to work on the UI of friendica. +Open `Settings > Display > Custom Theme Settings` adjust the Theme to your liking. Select your preferred Appearance - light, dark or black - and your favorite Accent color. Click `Submit` to save your changes. -You can either directly edit an existing theme. -But you might loose your changes when the theme is updated by the friendica team. +The `Custom` Appearance allows to tweak the themes CSS and set colors for UI elements. So called `schemestrings` can be shared between users. -If you are almost happy with an existing theme, the easiest way to cover your needs is to create a new theme, inheriting most of the properties of the parent theme and change just minor stuff. -The below for a more detailed description of theme heritage. - -Some themes also allow users to select *variants* of the theme. -Those theme variants most often contain an additional [CSS](https://en.wikipedia.org/wiki/CSS) file to override some styling of the default theme values. -From the themes in the main repository *vier* and *vier* are using this methods for variations. -Quattro is using a slightly different approach. - -Third you can start your theme from scratch. -Which is the most complex way to change friendicas look. -But it leaves you the most freedom. -So below for a *detailed* description and the meaning of some special files. - -### Styling - -If you want to change the styling of a theme, have a look at the themes CSS file. -In most cases, you can found these in - - /view/theme/**your-theme-name**/style.css - -sometimes, there is also a file called style.php in the theme directory. -This is only needed if the theme allows the user to change certain things of the theme dynamically. -Say the font size or set a background image. - -### Templates - -If you want to change the structure of the theme, you need to change the templates used by the theme. -Friendica themes are using [SMARTY3](http://www.smarty.net/) for templating. -The default template can be found in - - /view/templates - -if you want to override any template within your theme create your version of the template in - - /view/theme/**your-theme-name**/templates - -any template that exists there will be used instead of the default one. - -### JavaScript - -The same rule applies to the JavaScript files found in - - /js - -they will be overwritten by files in - - /view/theme/**your-theme-name**/js. - -## Creating a Theme from Scratch - -Keep patient. -Basically what you have to do is identify which template you have to change so it looks more like what you want. -Adopt the CSS of the theme accordingly. -And iterate the process until you have the theme the way you want it. - -*Use the source Luke.* and don't hesitate to ask in @[developers](https://forum.friendi.ca/profile/developers) or @[helpers](https://forum.friendi.ca/profile/helpers). - -## Special Files - -### unsupported - -If a file with this name (which might be empty) exists in the theme directory, the theme is marked as *unsupported*. -An unsupported theme may not be selected by a user in the settings. -Users who are already using it wont notice anything. - -### README(.md) - -The contents of this file, with or without the .md which indicates [Markdown](https://daringfireball.net/projects/markdown/) syntax, will be displayed at most repository hosting services and in the theme page within the admin panel of friendica. - -This file should contain information you want to let others know about your theme. - -### screenshot.[png|jpg] - -If you want to have a preview image of your theme displayed in the settings you should take a screenshot and save it with this name. -Supported formats are PNG and JPEG. - -### theme.php - -This is the main definition file of the theme. -In the header of that file, some meta information is stored. -For example, have a look at the theme.php of the *vier* theme: - - - * Author: Ike - * Author: Beanow - * Maintainer: Ike - * Description: "Vier" is a very compact and modern theme. It uses the font awesome font library: http://fortawesome.github.com/Font-Awesome/ - */ - -You see the definition of the theme's name, it's version and the initial author of the theme. -These three pieces of information should be listed. -If the original author is no longer working on the theme, but a maintainer has taken over, the maintainer should be listed as well. -The information from the theme header will be displayed in the admin panel. - -The first thing in file is to import the `App` class from `\Friendica\` namespace. - - use Friendica\App; - -This will make our job a little easier, as we don't have to specify the full name every time we need to use the `App` class. - -The next crucial part of the theme.php file is a definition of an init function. -The name of the function is _init. -So in the case of vier it is - - function vier_init(App $a) { - $a->theme_info = array(); - $a->set_template_engine('smarty3'); - } - -Here we have set the basic theme information, in this case they are empty. -But the array needs to be set. -And we have set the template engine that should be used by friendica for this theme. -At the moment you should use the *smarty3* engine. -There once was a friendica specific templating engine as well but that is not used anymore. -If you like to use another templating engine, please implement it. - -If you want to add something to the HTML header of the theme, one way to do so is by adding it to the theme.php file. -To do so, add something alike - - DI::page()['htmlhead'] .= <<< EOT - /* stuff you want to add to the header */ - EOT; - -So you can access the properties of this friendica session from the theme.php file as well. - -### default.php - -This file covers the structure of the underlying HTML layout. -The default file is in - - /view/default.php - -if you want to change it, say adding a 4th column for banners of your favourite FLOSS projects, place a new default.php file in your theme directory. -As with the theme.php file, you can use the properties of the $a variable with holds the friendica application to decide what content is displayed. +In the `General Theme Settings` you can also find the [vier](https://github.com/friendica/friendica/tree/stable/view/theme/vier) Theme, which precedes frio and is no longer officially maintained. diff --git a/docblox.dist.xml.license b/docblox.dist.xml.license new file mode 100644 index 0000000000..985c307f25 --- /dev/null +++ b/docblox.dist.xml.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/appnet.png b/images/appnet.png deleted file mode 100644 index d92199704c..0000000000 Binary files a/images/appnet.png and /dev/null differ diff --git a/images/blogger.png.license b/images/blogger.png.license new file mode 100644 index 0000000000..cbf6bac69e --- /dev/null +++ b/images/blogger.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2017 blogger.com + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/bluesky.jpg b/images/bluesky.jpg index 35020ac770..65c7593514 100644 Binary files a/images/bluesky.jpg and b/images/bluesky.jpg differ diff --git a/images/bluesky.jpg.license b/images/bluesky.jpg.license new file mode 100644 index 0000000000..6e3f8b787a --- /dev/null +++ b/images/bluesky.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Bluesky + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/default/diaspora.png.license b/images/default/diaspora.png.license new file mode 100644 index 0000000000..b3127d9150 --- /dev/null +++ b/images/default/diaspora.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 Diaspora Inc., 2010 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/images/default/gotosocial.svg.license b/images/default/gotosocial.svg.license new file mode 100644 index 0000000000..a8d83b0afc --- /dev/null +++ b/images/default/gotosocial.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 Anna Abramek + +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/default/hometown.png.license b/images/default/hometown.png.license new file mode 100644 index 0000000000..b99ea1b91c --- /dev/null +++ b/images/default/hometown.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019-2024 hometown project + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/default/mastodon.png.license b/images/default/mastodon.png.license new file mode 100644 index 0000000000..4e63c065f3 --- /dev/null +++ b/images/default/mastodon.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2016-2024 Eugen Rochko & other Mastodon contributors + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/default/peertube-account.png.license b/images/default/peertube-account.png.license new file mode 100644 index 0000000000..1400a4dc48 --- /dev/null +++ b/images/default/peertube-account.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2013-2023 Cole Bemis, Feather Icons + +SPDX-License-Identifier: MIT diff --git a/images/default/peertube-channel.png.license b/images/default/peertube-channel.png.license new file mode 100644 index 0000000000..1400a4dc48 --- /dev/null +++ b/images/default/peertube-channel.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2013-2023 Cole Bemis, Feather Icons + +SPDX-License-Identifier: MIT diff --git a/images/default/plume.png.license b/images/default/plume.png.license new file mode 100644 index 0000000000..c3aa27dbf0 --- /dev/null +++ b/images/default/plume.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 Plume project + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/diaspora-banner.jpg.license b/images/diaspora-banner.jpg.license new file mode 100644 index 0000000000..a6ce50e403 --- /dev/null +++ b/images/diaspora-banner.jpg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 Diaspora Inc., 2010 + +SPDX-License-Identifier: CC-BY-3.0 diff --git a/images/diaspora-logo.png.license b/images/diaspora-logo.png.license new file mode 100644 index 0000000000..b3127d9150 --- /dev/null +++ b/images/diaspora-logo.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 Diaspora Inc., 2010 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/images/diaspora.png.license b/images/diaspora.png.license new file mode 100644 index 0000000000..51679d3af7 --- /dev/null +++ b/images/diaspora.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2011 Diaspora Inc., 2010 + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/discourse.png.license b/images/discourse.png.license new file mode 100644 index 0000000000..d86793d836 --- /dev/null +++ b/images/discourse.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2014 Civilized Discourse Construction Kit, Inc. + +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/facebook.png.license b/images/facebook.png.license new file mode 100644 index 0000000000..065f4a8f96 --- /dev/null +++ b/images/facebook.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2013 Facebook Inc + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/friendica-404_svg_flexy-o-hare.png.license b/images/friendica-404_svg_flexy-o-hare.png.license new file mode 100644 index 0000000000..80b62c4fa3 --- /dev/null +++ b/images/friendica-404_svg_flexy-o-hare.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: lostinlight for the Friendica Project + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/friendica-404_svg_hare-bottom-light-inside.png.license b/images/friendica-404_svg_hare-bottom-light-inside.png.license new file mode 100644 index 0000000000..80b62c4fa3 --- /dev/null +++ b/images/friendica-404_svg_hare-bottom-light-inside.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: lostinlight for the Friendica Project + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/gnusocial.png.license b/images/gnusocial.png.license new file mode 100644 index 0000000000..1d0b7027ba --- /dev/null +++ b/images/gnusocial.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2015 Jonas Laugs, with lettering by Steven DuBois + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/googleplus.png.license b/images/googleplus.png.license new file mode 100644 index 0000000000..1f833eb333 --- /dev/null +++ b/images/googleplus.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2012 Google + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/libertree.png.license b/images/libertree.png.license new file mode 100644 index 0000000000..c425a2884c --- /dev/null +++ b/images/libertree.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2012 the libertree project + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/platforms/black/aardwolf.svg b/images/platforms/black/aardwolf.svg new file mode 100644 index 0000000000..74558d599d --- /dev/null +++ b/images/platforms/black/aardwolf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/aardwolf.svg.license b/images/platforms/black/aardwolf.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/aardwolf.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/activitypods.svg b/images/platforms/black/activitypods.svg new file mode 100644 index 0000000000..494f938ba7 --- /dev/null +++ b/images/platforms/black/activitypods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/activitypods.svg.license b/images/platforms/black/activitypods.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/activitypods.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/activitypub.svg b/images/platforms/black/activitypub.svg new file mode 100644 index 0000000000..ab181102a2 --- /dev/null +++ b/images/platforms/black/activitypub.svg @@ -0,0 +1,3 @@ + + + diff --git a/images/platforms/black/activitypub.svg.license b/images/platforms/black/activitypub.svg.license new file mode 100644 index 0000000000..33acc29726 --- /dev/null +++ b/images/platforms/black/activitypub.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Matthias Pfefferle https://github.com/pfefferle/openwebicons?ref=svgrepo.com +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/black/akkoma.svg b/images/platforms/black/akkoma.svg new file mode 100644 index 0000000000..db6fbd5372 --- /dev/null +++ b/images/platforms/black/akkoma.svg @@ -0,0 +1,7 @@ + + + Akkoma + + + + \ No newline at end of file diff --git a/images/platforms/black/akkoma.svg.license b/images/platforms/black/akkoma.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/akkoma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/anfora.svg b/images/platforms/black/anfora.svg new file mode 100644 index 0000000000..953d931d5f --- /dev/null +++ b/images/platforms/black/anfora.svg @@ -0,0 +1,11 @@ + + + Anfora + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/anfora.svg.license b/images/platforms/black/anfora.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/anfora.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/awakari.svg b/images/platforms/black/awakari.svg new file mode 100644 index 0000000000..7d65e5a68a --- /dev/null +++ b/images/platforms/black/awakari.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/awakari.svg.license b/images/platforms/black/awakari.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/awakari.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/azorius.svg b/images/platforms/black/azorius.svg new file mode 100644 index 0000000000..3dcec30fce --- /dev/null +++ b/images/platforms/black/azorius.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/azorius.svg.license b/images/platforms/black/azorius.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/azorius.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/bluesky.svg b/images/platforms/black/bluesky.svg new file mode 100644 index 0000000000..6e7f1bdb96 --- /dev/null +++ b/images/platforms/black/bluesky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/bluesky.svg.license b/images/platforms/black/bluesky.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/bluesky.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/bonfire.svg b/images/platforms/black/bonfire.svg new file mode 100644 index 0000000000..459892408e --- /dev/null +++ b/images/platforms/black/bonfire.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/bonfire.svg.license b/images/platforms/black/bonfire.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/bonfire.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/bookwyrm.svg b/images/platforms/black/bookwyrm.svg new file mode 100644 index 0000000000..df54fc74c5 --- /dev/null +++ b/images/platforms/black/bookwyrm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/bookwyrm.svg.license b/images/platforms/black/bookwyrm.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/bookwyrm.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/bridgy_fed.svg b/images/platforms/black/bridgy_fed.svg new file mode 100644 index 0000000000..1e202aedce --- /dev/null +++ b/images/platforms/black/bridgy_fed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/bridgy_fed.svg.license b/images/platforms/black/bridgy_fed.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/bridgy_fed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/brighteon_social.svg b/images/platforms/black/brighteon_social.svg new file mode 100644 index 0000000000..67b9abbfd5 --- /dev/null +++ b/images/platforms/black/brighteon_social.svg @@ -0,0 +1,12 @@ + + + Brighteon Social + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/brighteon_social.svg.license b/images/platforms/black/brighteon_social.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/brighteon_social.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/brutalinks.svg b/images/platforms/black/brutalinks.svg new file mode 100644 index 0000000000..9540a38b07 --- /dev/null +++ b/images/platforms/black/brutalinks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/brutalinks.svg.license b/images/platforms/black/brutalinks.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/brutalinks.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/calckey.svg b/images/platforms/black/calckey.svg new file mode 100644 index 0000000000..dd22d04466 --- /dev/null +++ b/images/platforms/black/calckey.svg @@ -0,0 +1,11 @@ + + + Calckey + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/calckey.svg.license b/images/platforms/black/calckey.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/calckey.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/castopod.svg b/images/platforms/black/castopod.svg new file mode 100644 index 0000000000..2db17e7a02 --- /dev/null +++ b/images/platforms/black/castopod.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/castopod.svg.license b/images/platforms/black/castopod.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/castopod.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/catodon.svg b/images/platforms/black/catodon.svg new file mode 100644 index 0000000000..dad44fcc30 --- /dev/null +++ b/images/platforms/black/catodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/catodon.svg.license b/images/platforms/black/catodon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/catodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/chatter_net.svg b/images/platforms/black/chatter_net.svg new file mode 100644 index 0000000000..39312ca2ea --- /dev/null +++ b/images/platforms/black/chatter_net.svg @@ -0,0 +1,7 @@ + + + Chatter Net + + + + \ No newline at end of file diff --git a/images/platforms/black/chatter_net.svg.license b/images/platforms/black/chatter_net.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/chatter_net.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/chuckya.svg b/images/platforms/black/chuckya.svg new file mode 100644 index 0000000000..a4b60c4efa --- /dev/null +++ b/images/platforms/black/chuckya.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/chuckya.svg.license b/images/platforms/black/chuckya.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/chuckya.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/clubsall.svg b/images/platforms/black/clubsall.svg new file mode 100644 index 0000000000..29db863218 --- /dev/null +++ b/images/platforms/black/clubsall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/clubsall.svg.license b/images/platforms/black/clubsall.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/clubsall.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/communecter.svg b/images/platforms/black/communecter.svg new file mode 100644 index 0000000000..46505c9d84 --- /dev/null +++ b/images/platforms/black/communecter.svg @@ -0,0 +1,12 @@ + + + COmmunecter + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/communecter.svg.license b/images/platforms/black/communecter.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/communecter.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/decodon.svg b/images/platforms/black/decodon.svg new file mode 100644 index 0000000000..f2a47a722d --- /dev/null +++ b/images/platforms/black/decodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/decodon.svg.license b/images/platforms/black/decodon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/decodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/diaspora.svg b/images/platforms/black/diaspora.svg new file mode 100644 index 0000000000..3087c4e6aa --- /dev/null +++ b/images/platforms/black/diaspora.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/diaspora.svg.license b/images/platforms/black/diaspora.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/diaspora.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/discourse.svg b/images/platforms/black/discourse.svg new file mode 100644 index 0000000000..0359b3b004 --- /dev/null +++ b/images/platforms/black/discourse.svg @@ -0,0 +1,10 @@ + + + Discourse + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/discourse.svg.license b/images/platforms/black/discourse.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/discourse.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/dolphin.svg b/images/platforms/black/dolphin.svg new file mode 100644 index 0000000000..2d581150e1 --- /dev/null +++ b/images/platforms/black/dolphin.svg @@ -0,0 +1,14 @@ + + + Dolphin + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/dolphin.svg.license b/images/platforms/black/dolphin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/dolphin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/drupal.svg b/images/platforms/black/drupal.svg new file mode 100644 index 0000000000..c9ab526143 --- /dev/null +++ b/images/platforms/black/drupal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/drupal.svg.license b/images/platforms/black/drupal.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/drupal.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/email.svg b/images/platforms/black/email.svg new file mode 100644 index 0000000000..04c39cbfd9 --- /dev/null +++ b/images/platforms/black/email.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/platforms/black/email.svg.license b/images/platforms/black/email.svg.license new file mode 100644 index 0000000000..d51dd6d76e --- /dev/null +++ b/images/platforms/black/email.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Instructure Ui https://github.com/instructure/instructure-ui?ref=svgrepo.com +SPDX-License-Identifier: MIT diff --git a/images/platforms/black/emissary.svg b/images/platforms/black/emissary.svg new file mode 100644 index 0000000000..b73b64f324 --- /dev/null +++ b/images/platforms/black/emissary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/emissary.svg.license b/images/platforms/black/emissary.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/emissary.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/epicyon.svg b/images/platforms/black/epicyon.svg new file mode 100644 index 0000000000..ab399d6b86 --- /dev/null +++ b/images/platforms/black/epicyon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/epicyon.svg.license b/images/platforms/black/epicyon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/epicyon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/f2ap.svg b/images/platforms/black/f2ap.svg new file mode 100644 index 0000000000..5c6c37db22 --- /dev/null +++ b/images/platforms/black/f2ap.svg @@ -0,0 +1,32 @@ + + + f2ap + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/f2ap.svg.license b/images/platforms/black/f2ap.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/f2ap.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/fedibird.svg b/images/platforms/black/fedibird.svg new file mode 100644 index 0000000000..8849889a5e --- /dev/null +++ b/images/platforms/black/fedibird.svg @@ -0,0 +1,12 @@ + + + Fedibird + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/fedibird.svg.license b/images/platforms/black/fedibird.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/fedibird.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/fedify.svg b/images/platforms/black/fedify.svg new file mode 100644 index 0000000000..ee1b9b20a0 --- /dev/null +++ b/images/platforms/black/fedify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/fedify.svg.license b/images/platforms/black/fedify.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/fedify.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/firefish.svg b/images/platforms/black/firefish.svg new file mode 100644 index 0000000000..b3dd710e4f --- /dev/null +++ b/images/platforms/black/firefish.svg @@ -0,0 +1,36 @@ + +Firefish + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/platforms/black/firefish.svg.license b/images/platforms/black/firefish.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/firefish.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/flipboard.svg b/images/platforms/black/flipboard.svg new file mode 100644 index 0000000000..85494db64f --- /dev/null +++ b/images/platforms/black/flipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/flipboard.svg.license b/images/platforms/black/flipboard.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/flipboard.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/flohmarkt.svg b/images/platforms/black/flohmarkt.svg new file mode 100644 index 0000000000..0df94e3288 --- /dev/null +++ b/images/platforms/black/flohmarkt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/flohmarkt.svg.license b/images/platforms/black/flohmarkt.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/flohmarkt.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/forgefriends.svg b/images/platforms/black/forgefriends.svg new file mode 100644 index 0000000000..fd6ddcfca6 --- /dev/null +++ b/images/platforms/black/forgefriends.svg @@ -0,0 +1,13 @@ + + + forgefriends + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/forgefriends.svg.license b/images/platforms/black/forgefriends.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/forgefriends.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/forgejo.svg b/images/platforms/black/forgejo.svg new file mode 100644 index 0000000000..b78d3b9c86 --- /dev/null +++ b/images/platforms/black/forgejo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/forgejo.svg.license b/images/platforms/black/forgejo.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/forgejo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/forte.svg b/images/platforms/black/forte.svg new file mode 100644 index 0000000000..c5d53c8763 --- /dev/null +++ b/images/platforms/black/forte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/forte.svg.license b/images/platforms/black/forte.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/forte.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/foundkey.svg b/images/platforms/black/foundkey.svg new file mode 100644 index 0000000000..071215d3df --- /dev/null +++ b/images/platforms/black/foundkey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/foundkey.svg.license b/images/platforms/black/foundkey.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/foundkey.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/friendica.svg b/images/platforms/black/friendica.svg new file mode 100644 index 0000000000..fbfc7e1221 --- /dev/null +++ b/images/platforms/black/friendica.svg @@ -0,0 +1,7 @@ + + + Friendica + + + + \ No newline at end of file diff --git a/images/platforms/black/friendica.svg.license b/images/platforms/black/friendica.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/friendica.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/funkwhale.svg b/images/platforms/black/funkwhale.svg new file mode 100644 index 0000000000..11318d562e --- /dev/null +++ b/images/platforms/black/funkwhale.svg @@ -0,0 +1,14 @@ + + + Funkwhale + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/funkwhale.svg.license b/images/platforms/black/funkwhale.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/funkwhale.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/gancio.svg b/images/platforms/black/gancio.svg new file mode 100644 index 0000000000..41c28673ff --- /dev/null +++ b/images/platforms/black/gancio.svg @@ -0,0 +1,12 @@ + + + Gancio + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/gancio.svg.license b/images/platforms/black/gancio.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/gancio.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/gath.io.svg b/images/platforms/black/gath.io.svg new file mode 100644 index 0000000000..d2e89cd6c1 --- /dev/null +++ b/images/platforms/black/gath.io.svg @@ -0,0 +1,11 @@ + + + Gath.io + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/gath.io.svg.license b/images/platforms/black/gath.io.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/gath.io.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/ghost.svg b/images/platforms/black/ghost.svg new file mode 100644 index 0000000000..b7ca833be3 --- /dev/null +++ b/images/platforms/black/ghost.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/ghost.svg.license b/images/platforms/black/ghost.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/ghost.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/gitlab.svg b/images/platforms/black/gitlab.svg new file mode 100644 index 0000000000..23a97f4814 --- /dev/null +++ b/images/platforms/black/gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/gitlab.svg.license b/images/platforms/black/gitlab.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/gitlab.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/glitch-soc.svg b/images/platforms/black/glitch-soc.svg new file mode 100644 index 0000000000..b097a6608a --- /dev/null +++ b/images/platforms/black/glitch-soc.svg @@ -0,0 +1,7 @@ + + + Glitch-soc Alt 1 + + + + \ No newline at end of file diff --git a/images/platforms/black/glitch-soc.svg.license b/images/platforms/black/glitch-soc.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/glitch-soc.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/glitchsoc.svg b/images/platforms/black/glitchsoc.svg new file mode 100644 index 0000000000..b097a6608a --- /dev/null +++ b/images/platforms/black/glitchsoc.svg @@ -0,0 +1,7 @@ + + + Glitch-soc Alt 1 + + + + \ No newline at end of file diff --git a/images/platforms/black/glitchsoc.svg.license b/images/platforms/black/glitchsoc.svg.license new file mode 100644 index 0000000000..a53eead85a --- /dev/null +++ b/images/platforms/black/glitchsoc.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Copyright (C) 2016-2024 Eugen Rochko & other Mastodon contributors +SPDX-License-Identifier: AGPL-3.0-only diff --git a/images/platforms/black/gnu_social.svg b/images/platforms/black/gnu_social.svg new file mode 100644 index 0000000000..51790be388 --- /dev/null +++ b/images/platforms/black/gnu_social.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/gnu_social.svg.license b/images/platforms/black/gnu_social.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/gnu_social.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/gnusocial.svg b/images/platforms/black/gnusocial.svg new file mode 100644 index 0000000000..51790be388 --- /dev/null +++ b/images/platforms/black/gnusocial.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/gnusocial.svg.license b/images/platforms/black/gnusocial.svg.license new file mode 100644 index 0000000000..33acc29726 --- /dev/null +++ b/images/platforms/black/gnusocial.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Matthias Pfefferle https://github.com/pfefferle/openwebicons?ref=svgrepo.com +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/black/go-fed.svg b/images/platforms/black/go-fed.svg new file mode 100644 index 0000000000..71caf52743 --- /dev/null +++ b/images/platforms/black/go-fed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/go-fed.svg.license b/images/platforms/black/go-fed.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/go-fed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/goblin.svg b/images/platforms/black/goblin.svg new file mode 100644 index 0000000000..d4024eab0a --- /dev/null +++ b/images/platforms/black/goblin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/goblin.svg.license b/images/platforms/black/goblin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/goblin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/gotosocial.svg b/images/platforms/black/gotosocial.svg new file mode 100644 index 0000000000..128032e499 --- /dev/null +++ b/images/platforms/black/gotosocial.svg @@ -0,0 +1,9 @@ + + + GoToSocial + + + + + + \ No newline at end of file diff --git a/images/platforms/black/gotosocial.svg.license b/images/platforms/black/gotosocial.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/gotosocial.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/greatape.svg b/images/platforms/black/greatape.svg new file mode 100644 index 0000000000..ecb21e81b4 --- /dev/null +++ b/images/platforms/black/greatape.svg @@ -0,0 +1,7 @@ + + + Greatape + + + + \ No newline at end of file diff --git a/images/platforms/black/greatape.svg.license b/images/platforms/black/greatape.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/greatape.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/guppe.svg b/images/platforms/black/guppe.svg new file mode 100644 index 0000000000..f1e39eb5dd --- /dev/null +++ b/images/platforms/black/guppe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/guppe.svg.license b/images/platforms/black/guppe.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/guppe.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/hollo.svg b/images/platforms/black/hollo.svg new file mode 100644 index 0000000000..c1caf81cef --- /dev/null +++ b/images/platforms/black/hollo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/hollo.svg.license b/images/platforms/black/hollo.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/hollo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/hometown.svg b/images/platforms/black/hometown.svg new file mode 100644 index 0000000000..33cc6fbe79 --- /dev/null +++ b/images/platforms/black/hometown.svg @@ -0,0 +1,17 @@ + + + Hometown + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/hometown.svg.license b/images/platforms/black/hometown.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/hometown.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/honk.svg b/images/platforms/black/honk.svg new file mode 100644 index 0000000000..6e12a2b4ab --- /dev/null +++ b/images/platforms/black/honk.svg @@ -0,0 +1,9 @@ + + + Honk + + + + + + \ No newline at end of file diff --git a/images/platforms/black/honk.svg.license b/images/platforms/black/honk.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/honk.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/hubzilla.svg b/images/platforms/black/hubzilla.svg new file mode 100644 index 0000000000..60aaa1e532 --- /dev/null +++ b/images/platforms/black/hubzilla.svg @@ -0,0 +1,9 @@ + + + Hubzilla + + + + + + \ No newline at end of file diff --git a/images/platforms/black/hubzilla.svg.license b/images/platforms/black/hubzilla.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/hubzilla.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/iceshrimp.svg b/images/platforms/black/iceshrimp.svg new file mode 100644 index 0000000000..b0eaa569ac --- /dev/null +++ b/images/platforms/black/iceshrimp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/iceshrimp.svg.license b/images/platforms/black/iceshrimp.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/iceshrimp.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/juick.svg b/images/platforms/black/juick.svg new file mode 100644 index 0000000000..1cddc5135c --- /dev/null +++ b/images/platforms/black/juick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/juick.svg.license b/images/platforms/black/juick.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/juick.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/kazarma.svg b/images/platforms/black/kazarma.svg new file mode 100644 index 0000000000..d7a9f0c550 --- /dev/null +++ b/images/platforms/black/kazarma.svg @@ -0,0 +1,7 @@ + + + Kazarma + + + + \ No newline at end of file diff --git a/images/platforms/black/kazarma.svg.license b/images/platforms/black/kazarma.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/kazarma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/kbin.svg b/images/platforms/black/kbin.svg new file mode 100644 index 0000000000..aa5e1ae3f1 --- /dev/null +++ b/images/platforms/black/kbin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/kbin.svg.license b/images/platforms/black/kbin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/kbin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/kepi.svg b/images/platforms/black/kepi.svg new file mode 100644 index 0000000000..96b6871e34 --- /dev/null +++ b/images/platforms/black/kepi.svg @@ -0,0 +1,12 @@ + + + kepi + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/kepi.svg.license b/images/platforms/black/kepi.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/kepi.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/kitsune.svg b/images/platforms/black/kitsune.svg new file mode 100644 index 0000000000..a58f0d4203 --- /dev/null +++ b/images/platforms/black/kitsune.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/kitsune.svg.license b/images/platforms/black/kitsune.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/kitsune.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/kmyblue.svg b/images/platforms/black/kmyblue.svg new file mode 100644 index 0000000000..2c77208f3e --- /dev/null +++ b/images/platforms/black/kmyblue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/kmyblue.svg.license b/images/platforms/black/kmyblue.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/kmyblue.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/kookie.svg b/images/platforms/black/kookie.svg new file mode 100644 index 0000000000..e435201b06 --- /dev/null +++ b/images/platforms/black/kookie.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/kookie.svg.license b/images/platforms/black/kookie.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/kookie.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/ktistec.svg b/images/platforms/black/ktistec.svg new file mode 100644 index 0000000000..39b71d11b6 --- /dev/null +++ b/images/platforms/black/ktistec.svg @@ -0,0 +1,9 @@ + + + Ktistec + + + + + + \ No newline at end of file diff --git a/images/platforms/black/ktistec.svg.license b/images/platforms/black/ktistec.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/ktistec.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/lemmy.svg b/images/platforms/black/lemmy.svg new file mode 100644 index 0000000000..f031bb90b5 --- /dev/null +++ b/images/platforms/black/lemmy.svg @@ -0,0 +1,7 @@ + + + Lemmy + + + + \ No newline at end of file diff --git a/images/platforms/black/lemmy.svg.license b/images/platforms/black/lemmy.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/lemmy.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/loops.svg b/images/platforms/black/loops.svg new file mode 100644 index 0000000000..9ec17d9983 --- /dev/null +++ b/images/platforms/black/loops.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/loops.svg.license b/images/platforms/black/loops.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/loops.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/mastodon.svg b/images/platforms/black/mastodon.svg new file mode 100644 index 0000000000..f3e0e962ca --- /dev/null +++ b/images/platforms/black/mastodon.svg @@ -0,0 +1,9 @@ + + + Mastodon + + + + + + \ No newline at end of file diff --git a/images/platforms/black/mastodon.svg.license b/images/platforms/black/mastodon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/mastodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/mbin.svg b/images/platforms/black/mbin.svg new file mode 100644 index 0000000000..70b51b4f7d --- /dev/null +++ b/images/platforms/black/mbin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/mbin.svg.license b/images/platforms/black/mbin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/mbin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/micro.blog.svg b/images/platforms/black/micro.blog.svg new file mode 100644 index 0000000000..df4c8c06cf --- /dev/null +++ b/images/platforms/black/micro.blog.svg @@ -0,0 +1,9 @@ + + + Micro.blog + + + + + + \ No newline at end of file diff --git a/images/platforms/black/micro.blog.svg.license b/images/platforms/black/micro.blog.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/micro.blog.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/minds.svg b/images/platforms/black/minds.svg new file mode 100644 index 0000000000..8fd98b7b3d --- /dev/null +++ b/images/platforms/black/minds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/minds.svg.license b/images/platforms/black/minds.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/minds.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/misskey.svg b/images/platforms/black/misskey.svg new file mode 100644 index 0000000000..ab1c3204a0 --- /dev/null +++ b/images/platforms/black/misskey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/misskey.svg.license b/images/platforms/black/misskey.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/misskey.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/mistpark.svg b/images/platforms/black/mistpark.svg new file mode 100644 index 0000000000..994ede5fe6 --- /dev/null +++ b/images/platforms/black/mistpark.svg @@ -0,0 +1,9 @@ + + + Mistpark + + + + + + \ No newline at end of file diff --git a/images/platforms/black/mistpark.svg.license b/images/platforms/black/mistpark.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/mistpark.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/mitra.svg b/images/platforms/black/mitra.svg new file mode 100644 index 0000000000..593c62b6b1 --- /dev/null +++ b/images/platforms/black/mitra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/mitra.svg.license b/images/platforms/black/mitra.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/mitra.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/mobilizon.svg b/images/platforms/black/mobilizon.svg new file mode 100644 index 0000000000..31ef831ec9 --- /dev/null +++ b/images/platforms/black/mobilizon.svg @@ -0,0 +1,9 @@ + + + Mobilizon + + + + + + \ No newline at end of file diff --git a/images/platforms/black/mobilizon.svg.license b/images/platforms/black/mobilizon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/mobilizon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/neodb.svg b/images/platforms/black/neodb.svg new file mode 100644 index 0000000000..adae2cf953 --- /dev/null +++ b/images/platforms/black/neodb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/neodb.svg.license b/images/platforms/black/neodb.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/neodb.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/newsmast.svg b/images/platforms/black/newsmast.svg new file mode 100644 index 0000000000..8331cb87e2 --- /dev/null +++ b/images/platforms/black/newsmast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/newsmast.svg.license b/images/platforms/black/newsmast.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/newsmast.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/nextcloud_social.svg b/images/platforms/black/nextcloud_social.svg new file mode 100644 index 0000000000..acd6ee0f9a --- /dev/null +++ b/images/platforms/black/nextcloud_social.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/nextcloud_social.svg.license b/images/platforms/black/nextcloud_social.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/nextcloud_social.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/nodebb.svg b/images/platforms/black/nodebb.svg new file mode 100644 index 0000000000..7798d3bd8a --- /dev/null +++ b/images/platforms/black/nodebb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/nodebb.svg.license b/images/platforms/black/nodebb.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/nodebb.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/osada.svg b/images/platforms/black/osada.svg new file mode 100644 index 0000000000..56b4d774bb --- /dev/null +++ b/images/platforms/black/osada.svg @@ -0,0 +1,9 @@ + + + Osada + + + + + + \ No newline at end of file diff --git a/images/platforms/black/osada.svg.license b/images/platforms/black/osada.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/osada.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/owncast.svg b/images/platforms/black/owncast.svg new file mode 100644 index 0000000000..44b3f5b9b8 --- /dev/null +++ b/images/platforms/black/owncast.svg @@ -0,0 +1,13 @@ + + + Owncast + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/owncast.svg.license b/images/platforms/black/owncast.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/owncast.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/peertube.svg b/images/platforms/black/peertube.svg new file mode 100644 index 0000000000..554ca8d262 --- /dev/null +++ b/images/platforms/black/peertube.svg @@ -0,0 +1,7 @@ + + + PeerTube + + + + \ No newline at end of file diff --git a/images/platforms/black/peertube.svg.license b/images/platforms/black/peertube.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/peertube.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/piefed.svg b/images/platforms/black/piefed.svg new file mode 100644 index 0000000000..87386fd939 --- /dev/null +++ b/images/platforms/black/piefed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/piefed.svg.license b/images/platforms/black/piefed.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/piefed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/pinetta.svg b/images/platforms/black/pinetta.svg new file mode 100644 index 0000000000..4d8d49d2c6 --- /dev/null +++ b/images/platforms/black/pinetta.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/pinetta.svg.license b/images/platforms/black/pinetta.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/pinetta.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/pixelfed.svg b/images/platforms/black/pixelfed.svg new file mode 100644 index 0000000000..4f56fe12e4 --- /dev/null +++ b/images/platforms/black/pixelfed.svg @@ -0,0 +1,7 @@ + + + Pixelfed + + + + \ No newline at end of file diff --git a/images/platforms/black/pixelfed.svg.license b/images/platforms/black/pixelfed.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/pixelfed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/pleroma.svg b/images/platforms/black/pleroma.svg new file mode 100644 index 0000000000..af93e43942 --- /dev/null +++ b/images/platforms/black/pleroma.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/pleroma.svg.license b/images/platforms/black/pleroma.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/pleroma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/plume.svg b/images/platforms/black/plume.svg new file mode 100644 index 0000000000..d78dc18e70 --- /dev/null +++ b/images/platforms/black/plume.svg @@ -0,0 +1,7 @@ + + + Plume + + + + \ No newline at end of file diff --git a/images/platforms/black/plume.svg.license b/images/platforms/black/plume.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/plume.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/postmarks.svg b/images/platforms/black/postmarks.svg new file mode 100644 index 0000000000..19dda5af8f --- /dev/null +++ b/images/platforms/black/postmarks.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/postmarks.svg.license b/images/platforms/black/postmarks.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/postmarks.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/prismo.svg b/images/platforms/black/prismo.svg new file mode 100644 index 0000000000..67d71635c6 --- /dev/null +++ b/images/platforms/black/prismo.svg @@ -0,0 +1,9 @@ + + + Prismo + + + + + + \ No newline at end of file diff --git a/images/platforms/black/prismo.svg.license b/images/platforms/black/prismo.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/prismo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/pump-io.svg b/images/platforms/black/pump-io.svg new file mode 100644 index 0000000000..e4e4fc3533 --- /dev/null +++ b/images/platforms/black/pump-io.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/images/platforms/black/pump-io.svg.license b/images/platforms/black/pump-io.svg.license new file mode 100644 index 0000000000..b3db2b09ce --- /dev/null +++ b/images/platforms/black/pump-io.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: MathiasGebbe https://commons.wikimedia.org/wiki/File:Pump.io_Logo.svg +SPDX-License-Identifier: CC-BY-SA-3.0 diff --git a/images/platforms/black/rebased.svg b/images/platforms/black/rebased.svg new file mode 100644 index 0000000000..e74cac4873 --- /dev/null +++ b/images/platforms/black/rebased.svg @@ -0,0 +1,9 @@ + + + Rebased + + + + + + \ No newline at end of file diff --git a/images/platforms/black/rebased.svg.license b/images/platforms/black/rebased.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/rebased.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/redmatrix.svg b/images/platforms/black/redmatrix.svg new file mode 100644 index 0000000000..7aebdf7b52 --- /dev/null +++ b/images/platforms/black/redmatrix.svg @@ -0,0 +1,9 @@ + + + Redmatrix + + + + + + \ No newline at end of file diff --git a/images/platforms/black/redmatrix.svg.license b/images/platforms/black/redmatrix.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/redmatrix.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/reel2bits.svg b/images/platforms/black/reel2bits.svg new file mode 100644 index 0000000000..d5b9e48a75 --- /dev/null +++ b/images/platforms/black/reel2bits.svg @@ -0,0 +1,14 @@ + + + reel2bits + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/reel2bits.svg.license b/images/platforms/black/reel2bits.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/reel2bits.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/rss.svg b/images/platforms/black/rss.svg new file mode 100644 index 0000000000..9fe37b87a6 --- /dev/null +++ b/images/platforms/black/rss.svg @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/images/platforms/black/rss.svg.license b/images/platforms/black/rss.svg.license new file mode 100644 index 0000000000..33acc29726 --- /dev/null +++ b/images/platforms/black/rss.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Matthias Pfefferle https://github.com/pfefferle/openwebicons?ref=svgrepo.com +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/black/ruffy.svg b/images/platforms/black/ruffy.svg new file mode 100644 index 0000000000..d6af02252a --- /dev/null +++ b/images/platforms/black/ruffy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/ruffy.svg.license b/images/platforms/black/ruffy.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/ruffy.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/sakura.svg b/images/platforms/black/sakura.svg new file mode 100644 index 0000000000..1caeb82705 --- /dev/null +++ b/images/platforms/black/sakura.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/sakura.svg.license b/images/platforms/black/sakura.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/sakura.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/seppo.svg b/images/platforms/black/seppo.svg new file mode 100644 index 0000000000..f5ca69ba8f --- /dev/null +++ b/images/platforms/black/seppo.svg @@ -0,0 +1,7 @@ + + + Seppo! + + + + \ No newline at end of file diff --git a/images/platforms/black/seppo.svg.license b/images/platforms/black/seppo.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/seppo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/shadowfacts.svg b/images/platforms/black/shadowfacts.svg new file mode 100644 index 0000000000..89c2cad2b5 --- /dev/null +++ b/images/platforms/black/shadowfacts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/shadowfacts.svg.license b/images/platforms/black/shadowfacts.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/shadowfacts.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/sharky.svg b/images/platforms/black/sharky.svg new file mode 100644 index 0000000000..4fd2f3fd09 --- /dev/null +++ b/images/platforms/black/sharky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/sharky.svg.license b/images/platforms/black/sharky.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/sharky.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/shuttlecraft.svg b/images/platforms/black/shuttlecraft.svg new file mode 100644 index 0000000000..269386557c --- /dev/null +++ b/images/platforms/black/shuttlecraft.svg @@ -0,0 +1,7 @@ + + + Shuttlecraft + + + + \ No newline at end of file diff --git a/images/platforms/black/shuttlecraft.svg.license b/images/platforms/black/shuttlecraft.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/shuttlecraft.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/smilodon.svg b/images/platforms/black/smilodon.svg new file mode 100644 index 0000000000..e418615cc7 --- /dev/null +++ b/images/platforms/black/smilodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/smilodon.svg.license b/images/platforms/black/smilodon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/smilodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/smithereen.svg b/images/platforms/black/smithereen.svg new file mode 100644 index 0000000000..bda9644917 --- /dev/null +++ b/images/platforms/black/smithereen.svg @@ -0,0 +1,9 @@ + + + Smithereen + + + + + + \ No newline at end of file diff --git a/images/platforms/black/smithereen.svg.license b/images/platforms/black/smithereen.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/smithereen.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/snac.svg b/images/platforms/black/snac.svg new file mode 100644 index 0000000000..5d37b80941 --- /dev/null +++ b/images/platforms/black/snac.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/snac.svg.license b/images/platforms/black/snac.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/snac.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/soapbox.svg b/images/platforms/black/soapbox.svg new file mode 100644 index 0000000000..f70bcf2700 --- /dev/null +++ b/images/platforms/black/soapbox.svg @@ -0,0 +1,9 @@ + + + Soapbox + + + + + + \ No newline at end of file diff --git a/images/platforms/black/soapbox.svg.license b/images/platforms/black/soapbox.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/soapbox.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/socialhome.svg b/images/platforms/black/socialhome.svg new file mode 100644 index 0000000000..fcd8bb08c0 --- /dev/null +++ b/images/platforms/black/socialhome.svg @@ -0,0 +1,9 @@ + + + Socialhome + + + + + + \ No newline at end of file diff --git a/images/platforms/black/socialhome.svg.license b/images/platforms/black/socialhome.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/socialhome.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/streams.svg b/images/platforms/black/streams.svg new file mode 100644 index 0000000000..3c78ce0beb --- /dev/null +++ b/images/platforms/black/streams.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/streams.svg.license b/images/platforms/black/streams.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/streams.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/sublinks.svg b/images/platforms/black/sublinks.svg new file mode 100644 index 0000000000..6e771f63bc --- /dev/null +++ b/images/platforms/black/sublinks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/sublinks.svg.license b/images/platforms/black/sublinks.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/sublinks.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/sutty.svg b/images/platforms/black/sutty.svg new file mode 100644 index 0000000000..f7b9a9030c --- /dev/null +++ b/images/platforms/black/sutty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/sutty.svg.license b/images/platforms/black/sutty.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/sutty.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/swanye.svg.license b/images/platforms/black/swanye.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/swanye.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/takahe.svg b/images/platforms/black/takahe.svg new file mode 100644 index 0000000000..1faf05bfeb --- /dev/null +++ b/images/platforms/black/takahe.svg @@ -0,0 +1,9 @@ + + + Takahē + + + + + + \ No newline at end of file diff --git a/images/platforms/black/takahe.svg.license b/images/platforms/black/takahe.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/takahe.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/takesama.svg b/images/platforms/black/takesama.svg new file mode 100644 index 0000000000..fdf0f65984 --- /dev/null +++ b/images/platforms/black/takesama.svg @@ -0,0 +1,7 @@ + + + takesama + + + + \ No newline at end of file diff --git a/images/platforms/black/takesama.svg.license b/images/platforms/black/takesama.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/takesama.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/threads.svg b/images/platforms/black/threads.svg new file mode 100644 index 0000000000..0097a7aeba --- /dev/null +++ b/images/platforms/black/threads.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/threads.svg.license b/images/platforms/black/threads.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/threads.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/tumblr.svg b/images/platforms/black/tumblr.svg new file mode 100644 index 0000000000..2fe91e709e --- /dev/null +++ b/images/platforms/black/tumblr.svg @@ -0,0 +1,19 @@ + + + + + tumblr [#181] + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/tumblr.svg.license b/images/platforms/black/tumblr.svg.license new file mode 100644 index 0000000000..4c8c4b8e5d --- /dev/null +++ b/images/platforms/black/tumblr.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: bypeople https://www.svgrepo.com/svg/513007/tumblr-181 +SPDX-License-Identifier: CC-PDDC diff --git a/images/platforms/black/vernissage.svg b/images/platforms/black/vernissage.svg new file mode 100644 index 0000000000..9c576ac7cb --- /dev/null +++ b/images/platforms/black/vernissage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/vernissage.svg.license b/images/platforms/black/vernissage.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/vernissage.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/vervis.svg b/images/platforms/black/vervis.svg new file mode 100644 index 0000000000..7c6a37f507 --- /dev/null +++ b/images/platforms/black/vervis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/vervis.svg.license b/images/platforms/black/vervis.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/vervis.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/vidzy.svg b/images/platforms/black/vidzy.svg new file mode 100644 index 0000000000..953b9a5c3e --- /dev/null +++ b/images/platforms/black/vidzy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/vidzy.svg.license b/images/platforms/black/vidzy.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/vidzy.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/vocata.svg b/images/platforms/black/vocata.svg new file mode 100644 index 0000000000..4d4476a320 --- /dev/null +++ b/images/platforms/black/vocata.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/vocata.svg.license b/images/platforms/black/vocata.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/vocata.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/wafrn.svg b/images/platforms/black/wafrn.svg new file mode 100644 index 0000000000..8e9d27df10 --- /dev/null +++ b/images/platforms/black/wafrn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/wafrn.svg.license b/images/platforms/black/wafrn.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/wafrn.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/wildebeest.svg b/images/platforms/black/wildebeest.svg new file mode 100644 index 0000000000..91b37d0a32 --- /dev/null +++ b/images/platforms/black/wildebeest.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/wildebeest.svg.license b/images/platforms/black/wildebeest.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/wildebeest.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/wordpress.svg b/images/platforms/black/wordpress.svg new file mode 100644 index 0000000000..a5c5aa6249 --- /dev/null +++ b/images/platforms/black/wordpress.svg @@ -0,0 +1,9 @@ + + + Wordpress + + + + + + \ No newline at end of file diff --git a/images/platforms/black/wordpress.svg.license b/images/platforms/black/wordpress.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/wordpress.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/write.as.svg b/images/platforms/black/write.as.svg new file mode 100644 index 0000000000..4ea60554fd --- /dev/null +++ b/images/platforms/black/write.as.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/write.as.svg.license b/images/platforms/black/write.as.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/write.as.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/writefreely.svg b/images/platforms/black/writefreely.svg new file mode 100644 index 0000000000..60c8c0bb53 --- /dev/null +++ b/images/platforms/black/writefreely.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/writefreely.svg.license b/images/platforms/black/writefreely.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/writefreely.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/wxwclub.svg b/images/platforms/black/wxwclub.svg new file mode 100644 index 0000000000..1ec30e8d7a --- /dev/null +++ b/images/platforms/black/wxwclub.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/wxwclub.svg.license b/images/platforms/black/wxwclub.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/wxwclub.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/xwiki.svg b/images/platforms/black/xwiki.svg new file mode 100644 index 0000000000..3bd39a5f89 --- /dev/null +++ b/images/platforms/black/xwiki.svg @@ -0,0 +1,15 @@ + + + XWiki + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/black/xwiki.svg.license b/images/platforms/black/xwiki.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/xwiki.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/black/zap.svg b/images/platforms/black/zap.svg new file mode 100644 index 0000000000..062b00393e --- /dev/null +++ b/images/platforms/black/zap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/black/zap.svg.license b/images/platforms/black/zap.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/black/zap.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/aardwolf.svg b/images/platforms/color/aardwolf.svg new file mode 100644 index 0000000000..8e5fb499ed --- /dev/null +++ b/images/platforms/color/aardwolf.svg @@ -0,0 +1,42 @@ + + + g38804-2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/aardwolf.svg.license b/images/platforms/color/aardwolf.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/aardwolf.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/activitypods.svg b/images/platforms/color/activitypods.svg new file mode 100644 index 0000000000..1cac3f1640 --- /dev/null +++ b/images/platforms/color/activitypods.svg @@ -0,0 +1,15 @@ + + + + Layer 1 + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/activitypods.svg.license b/images/platforms/color/activitypods.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/activitypods.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/activitypub.svg b/images/platforms/color/activitypub.svg new file mode 100644 index 0000000000..f56d428fbc --- /dev/null +++ b/images/platforms/color/activitypub.svg @@ -0,0 +1,288 @@ + + + + + ActivityPub logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + ActivityPub logo + + 2017-04-15 + + + Robert Martinez + + + + + ActivityPub + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/platforms/color/activitypub.svg.license b/images/platforms/color/activitypub.svg.license new file mode 100644 index 0000000000..acef685eed --- /dev/null +++ b/images/platforms/color/activitypub.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: mray https://commons.wikimedia.org/wiki/File:ActivityPub-logo-symbol.svg +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/color/akkoma.svg b/images/platforms/color/akkoma.svg new file mode 100644 index 0000000000..96dc8d8ce8 --- /dev/null +++ b/images/platforms/color/akkoma.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/images/platforms/color/akkoma.svg.license b/images/platforms/color/akkoma.svg.license new file mode 100644 index 0000000000..f3816d7841 --- /dev/null +++ b/images/platforms/color/akkoma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: floatingghost https://commons.wikimedia.org/wiki/File:Akkoma_logo.svg +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/platforms/color/bluesky.svg b/images/platforms/color/bluesky.svg new file mode 100644 index 0000000000..b60d51c279 --- /dev/null +++ b/images/platforms/color/bluesky.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/platforms/color/bluesky.svg.license b/images/platforms/color/bluesky.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/bluesky.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/chuckya.svg b/images/platforms/color/chuckya.svg new file mode 100644 index 0000000000..3d633b8842 --- /dev/null +++ b/images/platforms/color/chuckya.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/chuckya.svg.license b/images/platforms/color/chuckya.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/chuckya.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/decodon.svg b/images/platforms/color/decodon.svg new file mode 100644 index 0000000000..0e648c759b --- /dev/null +++ b/images/platforms/color/decodon.svg @@ -0,0 +1,12 @@ + + + decodon_flower_logo + + \ No newline at end of file diff --git a/images/platforms/color/decodon.svg.license b/images/platforms/color/decodon.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/decodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/discourse.svg b/images/platforms/color/discourse.svg new file mode 100644 index 0000000000..4cbb8c87b3 --- /dev/null +++ b/images/platforms/color/discourse.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/discourse.svg.license b/images/platforms/color/discourse.svg.license new file mode 100644 index 0000000000..5c569fb582 --- /dev/null +++ b/images/platforms/color/discourse.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Discourse team https://commons.wikimedia.org/wiki/File:Discourse_icon.svg +SPDX-License-Identifier: GPL-2.0-only diff --git a/images/platforms/color/fedify.svg b/images/platforms/color/fedify.svg new file mode 100644 index 0000000000..b19fbb59b6 --- /dev/null +++ b/images/platforms/color/fedify.svg @@ -0,0 +1,175 @@ + + + + + Deno Avatar + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/platforms/color/fedify.svg.license b/images/platforms/color/fedify.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/fedify.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/firefish.svg b/images/platforms/color/firefish.svg new file mode 100644 index 0000000000..7b36f4d77e --- /dev/null +++ b/images/platforms/color/firefish.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/platforms/color/firefish.svg.license b/images/platforms/color/firefish.svg.license new file mode 100644 index 0000000000..93e1b24be1 --- /dev/null +++ b/images/platforms/color/firefish.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Kainoa Kanter https://commons.wikimedia.org/wiki/File:Firefish_animated.svg +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/platforms/color/flipboard.svg b/images/platforms/color/flipboard.svg new file mode 100644 index 0000000000..6f42ee490f --- /dev/null +++ b/images/platforms/color/flipboard.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/platforms/color/flipboard.svg.license b/images/platforms/color/flipboard.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/flipboard.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/mods/fpostit/friendica.svg b/images/platforms/color/friendica.svg similarity index 100% rename from mods/fpostit/friendica.svg rename to images/platforms/color/friendica.svg diff --git a/images/platforms/color/friendica.svg.license b/images/platforms/color/friendica.svg.license new file mode 100644 index 0000000000..7aaeaba96e --- /dev/null +++ b/images/platforms/color/friendica.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2010-2024 the Friendica project +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/color/gitlab.svg b/images/platforms/color/gitlab.svg new file mode 100644 index 0000000000..858feda2a0 --- /dev/null +++ b/images/platforms/color/gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/gitlab.svg.license b/images/platforms/color/gitlab.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/gitlab.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/gnusocial.svg b/images/platforms/color/gnusocial.svg new file mode 100644 index 0000000000..ba61f345c7 --- /dev/null +++ b/images/platforms/color/gnusocial.svg @@ -0,0 +1,99 @@ + + + + + GNU Social Image Logo + + + + + + image/svg+xml + + GNU Social Image Logo + + + + + + + + + + + + + diff --git a/images/platforms/color/gnusocial.svg.license b/images/platforms/color/gnusocial.svg.license new file mode 100644 index 0000000000..bdcd7aa795 --- /dev/null +++ b/images/platforms/color/gnusocial.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Jonas Laugs, with lettering by Steven DuBois https://commons.wikimedia.org/wiki/File:GNU_Social_Image_Logo.svg +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/color/kookie.svg b/images/platforms/color/kookie.svg new file mode 100644 index 0000000000..df374d1d33 --- /dev/null +++ b/images/platforms/color/kookie.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/kookie.svg.license b/images/platforms/color/kookie.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/kookie.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/loops.svg b/images/platforms/color/loops.svg new file mode 100644 index 0000000000..8ba6b2231c --- /dev/null +++ b/images/platforms/color/loops.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/images/platforms/color/loops.svg.license b/images/platforms/color/loops.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/loops.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/mastodon.svg b/images/platforms/color/mastodon.svg new file mode 100644 index 0000000000..a20e7981f6 --- /dev/null +++ b/images/platforms/color/mastodon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/mastodon.svg.license b/images/platforms/color/mastodon.svg.license new file mode 100644 index 0000000000..cd758551b8 --- /dev/null +++ b/images/platforms/color/mastodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Eugen Rochko & other Mastodon contributors https://commons.wikimedia.org/wiki/File:Mastodon_logotype_(simple)_new_hue.svg +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/platforms/color/mbin.svg b/images/platforms/color/mbin.svg new file mode 100644 index 0000000000..b48eccdd17 --- /dev/null +++ b/images/platforms/color/mbin.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/platforms/color/mbin.svg.license b/images/platforms/color/mbin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/mbin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/misskey.svg b/images/platforms/color/misskey.svg new file mode 100644 index 0000000000..15c0bcc9bd --- /dev/null +++ b/images/platforms/color/misskey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/misskey.svg.license b/images/platforms/color/misskey.svg.license new file mode 100644 index 0000000000..41182ca490 --- /dev/null +++ b/images/platforms/color/misskey.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: https://icon-sets.iconify.design/simple-icons/misskey/ +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/color/neodb.svg b/images/platforms/color/neodb.svg new file mode 100644 index 0000000000..cc10cff898 --- /dev/null +++ b/images/platforms/color/neodb.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/platforms/color/neodb.svg.license b/images/platforms/color/neodb.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/neodb.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/newsmast.svg b/images/platforms/color/newsmast.svg new file mode 100644 index 0000000000..875fef30d9 --- /dev/null +++ b/images/platforms/color/newsmast.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/newsmast.svg.license b/images/platforms/color/newsmast.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/newsmast.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/nodebb.svg b/images/platforms/color/nodebb.svg new file mode 100644 index 0000000000..6ef96a9261 --- /dev/null +++ b/images/platforms/color/nodebb.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/nodebb.svg.license b/images/platforms/color/nodebb.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/nodebb.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/peertube.svg b/images/platforms/color/peertube.svg new file mode 100644 index 0000000000..0e6e228e61 --- /dev/null +++ b/images/platforms/color/peertube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/peertube.svg.license b/images/platforms/color/peertube.svg.license new file mode 100644 index 0000000000..13cba9b2e3 --- /dev/null +++ b/images/platforms/color/peertube.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: PeerTube contributors https://commons.wikimedia.org/wiki/File:Logo_de_PeerTube.svg +SPDX-License-Identifier: CC-PDDC diff --git a/images/platforms/color/pixelfed.svg b/images/platforms/color/pixelfed.svg new file mode 100644 index 0000000000..dfaf03fbea --- /dev/null +++ b/images/platforms/color/pixelfed.svg @@ -0,0 +1,101 @@ + + + + icon/color/svg/pixelfed-icon-color + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/pixelfed.svg.license b/images/platforms/color/pixelfed.svg.license new file mode 100644 index 0000000000..a8129c266b --- /dev/null +++ b/images/platforms/color/pixelfed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Mark Wilson & other Pixelfed contributors https://commons.wikimedia.org/wiki/File:Pixelfed_logo_multicolor_(September_2018).svg +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/color/pleroma.svg b/images/platforms/color/pleroma.svg new file mode 100644 index 0000000000..2696052279 --- /dev/null +++ b/images/platforms/color/pleroma.svg @@ -0,0 +1,75 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/images/platforms/color/pleroma.svg.license b/images/platforms/color/pleroma.svg.license new file mode 100644 index 0000000000..a5c1aa75f2 --- /dev/null +++ b/images/platforms/color/pleroma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Henry Jameson https://commons.wikimedia.org/wiki/File:Smaller_Pleroma_logo.svg +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/images/platforms/color/rss.svg b/images/platforms/color/rss.svg new file mode 100644 index 0000000000..a7f9cf196c --- /dev/null +++ b/images/platforms/color/rss.svg @@ -0,0 +1,18 @@ + + + + RSS feed icon + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/rss.svg.license b/images/platforms/color/rss.svg.license new file mode 100644 index 0000000000..59f909d7b6 --- /dev/null +++ b/images/platforms/color/rss.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Jahoe https://commons.wikimedia.org/wiki/File:Generic_Feed-icon.svg +SPDX-License-Identifier: GPL-2.0-or-later diff --git a/images/platforms/color/sharky.svg b/images/platforms/color/sharky.svg new file mode 100644 index 0000000000..40a2fd9bed --- /dev/null +++ b/images/platforms/color/sharky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/sharky.svg.license b/images/platforms/color/sharky.svg.license new file mode 100644 index 0000000000..c0a1118a51 --- /dev/null +++ b/images/platforms/color/sharky.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: https://icon-sets.iconify.design/game-icons/shark-jaws/ +SPDX-License-Identifier: CC-BY-3.0 diff --git a/images/platforms/color/tumblr.svg b/images/platforms/color/tumblr.svg new file mode 100644 index 0000000000..d4f4d6d4df --- /dev/null +++ b/images/platforms/color/tumblr.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/images/platforms/color/tumblr.svg.license b/images/platforms/color/tumblr.svg.license new file mode 100644 index 0000000000..39a4f64c06 --- /dev/null +++ b/images/platforms/color/tumblr.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: ZyMOS https://commons.wikimedia.org/wiki/File:Tumblr.svg +SPDX-License-Identifier: CC-PDDC diff --git a/images/platforms/color/vervis.svg b/images/platforms/color/vervis.svg new file mode 100644 index 0000000000..571464e7a8 --- /dev/null +++ b/images/platforms/color/vervis.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/vervis.svg.license b/images/platforms/color/vervis.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/vervis.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/vocata.svg b/images/platforms/color/vocata.svg new file mode 100644 index 0000000000..a0ab84330f --- /dev/null +++ b/images/platforms/color/vocata.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/platforms/color/vocata.svg.license b/images/platforms/color/vocata.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/color/vocata.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/color/wordpress.svg b/images/platforms/color/wordpress.svg new file mode 100644 index 0000000000..7182535973 --- /dev/null +++ b/images/platforms/color/wordpress.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/color/wordpress.svg.license b/images/platforms/color/wordpress.svg.license new file mode 100644 index 0000000000..1fea8f7f81 --- /dev/null +++ b/images/platforms/color/wordpress.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: WordPress https://commons.wikimedia.org/wiki/File:WordPress_blue_logo.svg +SPDX-License-Identifier: GPL-2.0-or-later diff --git a/images/platforms/white/activitypub.svg b/images/platforms/white/activitypub.svg new file mode 100644 index 0000000000..f44e06683a --- /dev/null +++ b/images/platforms/white/activitypub.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/activitypub.svg.license b/images/platforms/white/activitypub.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/activitypub.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/akkoma.svg b/images/platforms/white/akkoma.svg new file mode 100644 index 0000000000..7bc4df5630 --- /dev/null +++ b/images/platforms/white/akkoma.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/akkoma.svg.license b/images/platforms/white/akkoma.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/akkoma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/andstatus.svg b/images/platforms/white/andstatus.svg new file mode 100644 index 0000000000..ab551a2275 --- /dev/null +++ b/images/platforms/white/andstatus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/andstatus.svg.license b/images/platforms/white/andstatus.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/andstatus.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/bluesky.svg b/images/platforms/white/bluesky.svg new file mode 100644 index 0000000000..88153df455 --- /dev/null +++ b/images/platforms/white/bluesky.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/bluesky.svg.license b/images/platforms/white/bluesky.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/bluesky.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/bonfire.svg b/images/platforms/white/bonfire.svg new file mode 100644 index 0000000000..9f2c438cfa --- /dev/null +++ b/images/platforms/white/bonfire.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/bonfire.svg.license b/images/platforms/white/bonfire.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/bonfire.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/bookwyrm.svg b/images/platforms/white/bookwyrm.svg new file mode 100644 index 0000000000..6afc7c37ca --- /dev/null +++ b/images/platforms/white/bookwyrm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/bookwyrm.svg.license b/images/platforms/white/bookwyrm.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/bookwyrm.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/bridgy_fed.svg b/images/platforms/white/bridgy_fed.svg new file mode 100644 index 0000000000..4326188486 --- /dev/null +++ b/images/platforms/white/bridgy_fed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/images/platforms/white/bridgy_fed.svg.license b/images/platforms/white/bridgy_fed.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/bridgy_fed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/calckey.svg b/images/platforms/white/calckey.svg new file mode 100644 index 0000000000..05de635f32 --- /dev/null +++ b/images/platforms/white/calckey.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/calckey.svg.license b/images/platforms/white/calckey.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/calckey.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/castopod.svg b/images/platforms/white/castopod.svg new file mode 100644 index 0000000000..a6c0c6df5e --- /dev/null +++ b/images/platforms/white/castopod.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/platforms/white/castopod.svg.license b/images/platforms/white/castopod.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/castopod.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/diaspora.svg b/images/platforms/white/diaspora.svg new file mode 100644 index 0000000000..54ecd5f069 --- /dev/null +++ b/images/platforms/white/diaspora.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/diaspora.svg.license b/images/platforms/white/diaspora.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/diaspora.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/discourse.svg b/images/platforms/white/discourse.svg new file mode 100644 index 0000000000..801b1345ee --- /dev/null +++ b/images/platforms/white/discourse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/discourse.svg.license b/images/platforms/white/discourse.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/discourse.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/dolphin.svg b/images/platforms/white/dolphin.svg new file mode 100644 index 0000000000..590bb6728d --- /dev/null +++ b/images/platforms/white/dolphin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/dolphin.svg.license b/images/platforms/white/dolphin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/dolphin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/drupal.svg b/images/platforms/white/drupal.svg new file mode 100644 index 0000000000..f4179023e7 --- /dev/null +++ b/images/platforms/white/drupal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/drupal.svg.license b/images/platforms/white/drupal.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/drupal.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/email.svg b/images/platforms/white/email.svg new file mode 100644 index 0000000000..cbc88ddb10 --- /dev/null +++ b/images/platforms/white/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/email.svg.license b/images/platforms/white/email.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/email.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/firefish.svg b/images/platforms/white/firefish.svg new file mode 100644 index 0000000000..bf20d55ebf --- /dev/null +++ b/images/platforms/white/firefish.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/firefish.svg.license b/images/platforms/white/firefish.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/firefish.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/flipboard.svg b/images/platforms/white/flipboard.svg new file mode 100644 index 0000000000..62d8873eb1 --- /dev/null +++ b/images/platforms/white/flipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/flipboard.svg.license b/images/platforms/white/flipboard.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/flipboard.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/flohmarkt.svg b/images/platforms/white/flohmarkt.svg new file mode 100644 index 0000000000..cbab50efe0 --- /dev/null +++ b/images/platforms/white/flohmarkt.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/flohmarkt.svg.license b/images/platforms/white/flohmarkt.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/flohmarkt.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/forgejo.svg b/images/platforms/white/forgejo.svg new file mode 100644 index 0000000000..fb9566de7e --- /dev/null +++ b/images/platforms/white/forgejo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/forgejo.svg.license b/images/platforms/white/forgejo.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/forgejo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/friendica.svg b/images/platforms/white/friendica.svg new file mode 100644 index 0000000000..c08f087165 --- /dev/null +++ b/images/platforms/white/friendica.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/friendica.svg.license b/images/platforms/white/friendica.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/friendica.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/funkwhale.svg b/images/platforms/white/funkwhale.svg new file mode 100644 index 0000000000..8afb6a3226 --- /dev/null +++ b/images/platforms/white/funkwhale.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/funkwhale.svg.license b/images/platforms/white/funkwhale.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/funkwhale.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/ghost.svg b/images/platforms/white/ghost.svg new file mode 100644 index 0000000000..cca71bc322 --- /dev/null +++ b/images/platforms/white/ghost.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/ghost.svg.license b/images/platforms/white/ghost.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/ghost.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/gitlab.svg b/images/platforms/white/gitlab.svg new file mode 100644 index 0000000000..ef428338de --- /dev/null +++ b/images/platforms/white/gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/gitlab.svg.license b/images/platforms/white/gitlab.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/gitlab.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/glitch-soc.svg b/images/platforms/white/glitch-soc.svg new file mode 100644 index 0000000000..54ad242c76 --- /dev/null +++ b/images/platforms/white/glitch-soc.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/images/platforms/white/glitch-soc.svg.license b/images/platforms/white/glitch-soc.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/glitch-soc.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/gnusocial.svg b/images/platforms/white/gnusocial.svg new file mode 100644 index 0000000000..179c6fc8ba --- /dev/null +++ b/images/platforms/white/gnusocial.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/gnusocial.svg.license b/images/platforms/white/gnusocial.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/gnusocial.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/gotosocial.svg b/images/platforms/white/gotosocial.svg new file mode 100644 index 0000000000..2e097a139f --- /dev/null +++ b/images/platforms/white/gotosocial.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/gotosocial.svg.license b/images/platforms/white/gotosocial.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/gotosocial.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/guppe.svg b/images/platforms/white/guppe.svg new file mode 100644 index 0000000000..25b62c1a56 --- /dev/null +++ b/images/platforms/white/guppe.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/guppe.svg.license b/images/platforms/white/guppe.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/guppe.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/hollo.svg b/images/platforms/white/hollo.svg new file mode 100644 index 0000000000..ad53ba31f8 --- /dev/null +++ b/images/platforms/white/hollo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/hollo.svg.license b/images/platforms/white/hollo.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/hollo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/hubzilla.svg b/images/platforms/white/hubzilla.svg new file mode 100644 index 0000000000..9c9168a4c1 --- /dev/null +++ b/images/platforms/white/hubzilla.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/hubzilla.svg.license b/images/platforms/white/hubzilla.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/hubzilla.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/iceshrimp.svg b/images/platforms/white/iceshrimp.svg new file mode 100644 index 0000000000..7b942fe652 --- /dev/null +++ b/images/platforms/white/iceshrimp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/iceshrimp.svg.license b/images/platforms/white/iceshrimp.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/iceshrimp.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/kbin.svg b/images/platforms/white/kbin.svg new file mode 100644 index 0000000000..7cc7811bde --- /dev/null +++ b/images/platforms/white/kbin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/kbin.svg.license b/images/platforms/white/kbin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/kbin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/lemmy.svg b/images/platforms/white/lemmy.svg new file mode 100644 index 0000000000..3bed2cc4b1 --- /dev/null +++ b/images/platforms/white/lemmy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/lemmy.svg.license b/images/platforms/white/lemmy.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/lemmy.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/loforo.svg b/images/platforms/white/loforo.svg new file mode 100644 index 0000000000..76724dd834 --- /dev/null +++ b/images/platforms/white/loforo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/loforo.svg.license b/images/platforms/white/loforo.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/loforo.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/loops.svg b/images/platforms/white/loops.svg new file mode 100644 index 0000000000..4ba2f33ba5 --- /dev/null +++ b/images/platforms/white/loops.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/loops.svg.license b/images/platforms/white/loops.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/loops.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/mastodon.svg b/images/platforms/white/mastodon.svg new file mode 100644 index 0000000000..2ac7d0b895 --- /dev/null +++ b/images/platforms/white/mastodon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/mastodon.svg.license b/images/platforms/white/mastodon.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/mastodon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/mbin.svg b/images/platforms/white/mbin.svg new file mode 100644 index 0000000000..28e9b49345 --- /dev/null +++ b/images/platforms/white/mbin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/mbin.svg.license b/images/platforms/white/mbin.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/mbin.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/microblog.svg b/images/platforms/white/microblog.svg new file mode 100644 index 0000000000..c77fd2ff6b --- /dev/null +++ b/images/platforms/white/microblog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/microblog.svg.license b/images/platforms/white/microblog.svg.license new file mode 100644 index 0000000000..07ac40ec9a --- /dev/null +++ b/images/platforms/white/microblog.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-4.0 diff --git a/images/platforms/white/minds.svg b/images/platforms/white/minds.svg new file mode 100644 index 0000000000..0ddee7bc03 --- /dev/null +++ b/images/platforms/white/minds.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/minds.svg.license b/images/platforms/white/minds.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/minds.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/misskey.svg b/images/platforms/white/misskey.svg new file mode 100644 index 0000000000..9dd605eb9a --- /dev/null +++ b/images/platforms/white/misskey.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/misskey.svg.license b/images/platforms/white/misskey.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/misskey.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/mobilizon.svg b/images/platforms/white/mobilizon.svg new file mode 100644 index 0000000000..2d2a6d9a17 --- /dev/null +++ b/images/platforms/white/mobilizon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/mobilizon.svg.license b/images/platforms/white/mobilizon.svg.license new file mode 100644 index 0000000000..53413ed82d --- /dev/null +++ b/images/platforms/white/mobilizon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/images/platforms/white/nextcloud.svg b/images/platforms/white/nextcloud.svg new file mode 100644 index 0000000000..6ef81a6795 --- /dev/null +++ b/images/platforms/white/nextcloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/nextcloud.svg.license b/images/platforms/white/nextcloud.svg.license new file mode 100644 index 0000000000..bf13a3d696 --- /dev/null +++ b/images/platforms/white/nextcloud.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: MIT diff --git a/images/platforms/white/owncast.svg b/images/platforms/white/owncast.svg new file mode 100644 index 0000000000..cf0ada166b --- /dev/null +++ b/images/platforms/white/owncast.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/images/platforms/white/owncast.svg.license b/images/platforms/white/owncast.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/owncast.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/peertube.svg b/images/platforms/white/peertube.svg new file mode 100644 index 0000000000..7a15c8e0d4 --- /dev/null +++ b/images/platforms/white/peertube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/peertube.svg.license b/images/platforms/white/peertube.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/peertube.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/phanpy.svg b/images/platforms/white/phanpy.svg new file mode 100644 index 0000000000..f9f8073e41 --- /dev/null +++ b/images/platforms/white/phanpy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/platforms/white/phanpy.svg.license b/images/platforms/white/phanpy.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/phanpy.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/pixelfed.svg b/images/platforms/white/pixelfed.svg new file mode 100644 index 0000000000..c872c65e89 --- /dev/null +++ b/images/platforms/white/pixelfed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/pixelfed.svg.license b/images/platforms/white/pixelfed.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/pixelfed.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/pleroma.svg b/images/platforms/white/pleroma.svg new file mode 100644 index 0000000000..e0b0585c7b --- /dev/null +++ b/images/platforms/white/pleroma.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/pleroma.svg.license b/images/platforms/white/pleroma.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/pleroma.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/plume.svg b/images/platforms/white/plume.svg new file mode 100644 index 0000000000..b4fbff3127 --- /dev/null +++ b/images/platforms/white/plume.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/plume.svg.license b/images/platforms/white/plume.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/plume.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/rss.svg b/images/platforms/white/rss.svg new file mode 100644 index 0000000000..1874fa72d3 --- /dev/null +++ b/images/platforms/white/rss.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/rss.svg.license b/images/platforms/white/rss.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/rss.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/shark.svg b/images/platforms/white/shark.svg new file mode 100644 index 0000000000..6b360796ab --- /dev/null +++ b/images/platforms/white/shark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/shark.svg.license b/images/platforms/white/shark.svg.license new file mode 100644 index 0000000000..d1c6a344b0 --- /dev/null +++ b/images/platforms/white/shark.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC-BY-3.0 diff --git a/images/platforms/white/soapbox.svg b/images/platforms/white/soapbox.svg new file mode 100644 index 0000000000..b852de2381 --- /dev/null +++ b/images/platforms/white/soapbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/soapbox.svg.license b/images/platforms/white/soapbox.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/soapbox.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/socialhome.svg b/images/platforms/white/socialhome.svg new file mode 100644 index 0000000000..513f2bb6d0 --- /dev/null +++ b/images/platforms/white/socialhome.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/images/platforms/white/socialhome.svg.license b/images/platforms/white/socialhome.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/socialhome.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/streams.svg b/images/platforms/white/streams.svg new file mode 100644 index 0000000000..3b7224134a --- /dev/null +++ b/images/platforms/white/streams.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/images/platforms/white/streams.svg.license b/images/platforms/white/streams.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/streams.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/takahe.svg b/images/platforms/white/takahe.svg new file mode 100644 index 0000000000..31146146ea --- /dev/null +++ b/images/platforms/white/takahe.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/takahe.svg.license b/images/platforms/white/takahe.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/takahe.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/threads.svg b/images/platforms/white/threads.svg new file mode 100644 index 0000000000..d178ba1dfb --- /dev/null +++ b/images/platforms/white/threads.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/threads.svg.license b/images/platforms/white/threads.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/threads.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/tumblr.svg b/images/platforms/white/tumblr.svg new file mode 100644 index 0000000000..d11cfd53ee --- /dev/null +++ b/images/platforms/white/tumblr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/tumblr.svg.license b/images/platforms/white/tumblr.svg.license new file mode 100644 index 0000000000..bf13a3d696 --- /dev/null +++ b/images/platforms/white/tumblr.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: MIT diff --git a/images/platforms/white/wordpress.svg b/images/platforms/white/wordpress.svg new file mode 100644 index 0000000000..39c01c6658 --- /dev/null +++ b/images/platforms/white/wordpress.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/wordpress.svg.license b/images/platforms/white/wordpress.svg.license new file mode 100644 index 0000000000..dfea79525f --- /dev/null +++ b/images/platforms/white/wordpress.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: Apache-2.0 diff --git a/images/platforms/white/write.as.svg b/images/platforms/white/write.as.svg new file mode 100644 index 0000000000..b910f3d480 --- /dev/null +++ b/images/platforms/white/write.as.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/platforms/white/write.as.svg.license b/images/platforms/white/write.as.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/write.as.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/writefreely.svg b/images/platforms/white/writefreely.svg new file mode 100644 index 0000000000..e6e7e76d16 --- /dev/null +++ b/images/platforms/white/writefreely.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/writefreely.svg.license b/images/platforms/white/writefreely.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/writefreely.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/platforms/white/xwiki.svg b/images/platforms/white/xwiki.svg new file mode 100644 index 0000000000..db64f83d2e --- /dev/null +++ b/images/platforms/white/xwiki.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/platforms/white/xwiki.svg.license b/images/platforms/white/xwiki.svg.license new file mode 100644 index 0000000000..330e2cb8af --- /dev/null +++ b/images/platforms/white/xwiki.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: iconify https://icon-sets.iconify.design +SPDX-License-Identifier: CC0-1.0 diff --git a/images/platforms/white/zap.svg b/images/platforms/white/zap.svg new file mode 100644 index 0000000000..6e1470d88b --- /dev/null +++ b/images/platforms/white/zap.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/platforms/white/zap.svg.license b/images/platforms/white/zap.svg.license new file mode 100644 index 0000000000..3ffce40c5f --- /dev/null +++ b/images/platforms/white/zap.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: wakest https://codeberg.org/FediverseIconography +SPDX-License-Identifier: GPL-3.0-only diff --git a/images/screenshots/friendica-2023-10-frio-desktop.png b/images/screenshots/friendica-2023-10-frio-desktop.png deleted file mode 100644 index e06f971365..0000000000 Binary files a/images/screenshots/friendica-2023-10-frio-desktop.png and /dev/null differ diff --git a/images/screenshots/friendica-2023-10-frio-mobile-options-dark-blue.png b/images/screenshots/friendica-2023-10-frio-mobile-options-dark-blue.png index e486cfeead..edbfe7564e 100644 Binary files a/images/screenshots/friendica-2023-10-frio-mobile-options-dark-blue.png and b/images/screenshots/friendica-2023-10-frio-mobile-options-dark-blue.png differ diff --git a/images/screenshots/friendica-2023-10-frio-mobile-options-light-blue.png b/images/screenshots/friendica-2023-10-frio-mobile-options-light-blue.png index 04c6c60682..e299313e83 100644 Binary files a/images/screenshots/friendica-2023-10-frio-mobile-options-light-blue.png and b/images/screenshots/friendica-2023-10-frio-mobile-options-light-blue.png differ diff --git a/images/screenshots/friendica-2023-10-frio-mobile-timeline-dark-blue.png b/images/screenshots/friendica-2023-10-frio-mobile-timeline-dark-blue.png index ebcbce3d6c..01ebc07076 100644 Binary files a/images/screenshots/friendica-2023-10-frio-mobile-timeline-dark-blue.png and b/images/screenshots/friendica-2023-10-frio-mobile-timeline-dark-blue.png differ diff --git a/images/screenshots/friendica-2023-12-frio-desktop.png b/images/screenshots/friendica-2023-12-frio-desktop.png new file mode 100644 index 0000000000..ac11015caa Binary files /dev/null and b/images/screenshots/friendica-2023-12-frio-desktop.png differ diff --git a/images/tumblr.png.license b/images/tumblr.png.license new file mode 100644 index 0000000000..8e741e2533 --- /dev/null +++ b/images/tumblr.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2014 Notrons (Wikimedia) + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/twitter.png.license b/images/twitter.png.license new file mode 100644 index 0000000000..88cdb0a15e --- /dev/null +++ b/images/twitter.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2013 Aimmp (Wikimedia) + +SPDX-License-Identifier: CC0-1.0 diff --git a/images/wordpress.png.license b/images/wordpress.png.license new file mode 100644 index 0000000000..d719437e22 --- /dev/null +++ b/images/wordpress.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2003 Wordpress + +SPDX-License-Identifier: GPL-2.0-or-later diff --git a/images/youtube_icon.gif.license b/images/youtube_icon.gif.license new file mode 100644 index 0000000000..36dc1373af --- /dev/null +++ b/images/youtube_icon.gif.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2011 ZyMOS (Wikimedia) + +SPDX-License-Identifier: CC0-1.0 diff --git a/index.php b/index.php index 87778308a5..c1265556f6 100644 --- a/index.php +++ b/index.php @@ -1,56 +1,22 @@ . - * - */ -use Dice\Dice; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later $start_time = microtime(true); if (!file_exists(__DIR__ . '/vendor/autoload.php')) { - die('Vendor path not found. Please execute "bin/composer.phar --no-dev install" on the command line in the web root.'); + die('Vendor path not found. Please execute "bin/composer.phar run install:prod" on the command line in the web root.'); } require __DIR__ . '/vendor/autoload.php'; -$dice = (new Dice())->addRules(include __DIR__ . '/static/dependencies.config.php'); -/** @var \Friendica\Core\Addon\Capability\ICanLoadAddons $addonLoader */ -$addonLoader = $dice->create(\Friendica\Core\Addon\Capability\ICanLoadAddons::class); -$dice = $dice->addRules($addonLoader->getActiveAddonConfig('dependencies')); -$dice = $dice->addRule(Friendica\App\Mode::class, ['call' => [['determineRunMode', [false, $_SERVER], Dice::CHAIN_CALL]]]); +$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); -\Friendica\DI::init($dice); +$container = \Friendica\Core\DiceContainer::fromBasePath(__DIR__); -\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); +$app = \Friendica\App::fromContainer($container); -$a = \Friendica\DI::app(); - -\Friendica\DI::mode()->setExecutor(\Friendica\App\Mode::INDEX); - -$a->runFrontend( - $dice->create(\Friendica\App\Router::class), - $dice->create(\Friendica\Core\PConfig\Capability\IManagePersonalConfigValues::class), - $dice->create(\Friendica\Security\Authentication::class), - $dice->create(\Friendica\App\Page::class), - $dice->create(\Friendica\Content\Nav::class), - $dice->create(Friendica\Module\Special\HTTPException::class), - new \Friendica\Util\HTTPInputData($_SERVER), - $start_time, - $_SERVER -); +$app->processRequest($request, $start_time); diff --git a/mod/item.php b/mod/item.php index e85c08e023..e416126a4a 100644 --- a/mod/item.php +++ b/mod/item.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * This is the POST destination for most all locally posted * text stuff. This function handles status, wall-to-wall status, @@ -28,16 +16,14 @@ * information. */ -use Friendica\App; use Friendica\Content\Conversation; use Friendica\Content\Text\BBCode; -use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\ItemURI; @@ -45,7 +31,8 @@ use Friendica\Model\Post; use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; -function item_post(App $a) { +function item_post() +{ $uid = DI::userSession()->getLocalUserId(); if (!$uid) { @@ -56,7 +43,11 @@ function item_post(App $a) { item_drop($uid, $_REQUEST['dropitems']); } - Hook::callAll('post_local_start', $_REQUEST); + $eventDispatcher = DI::eventDispatcher(); + + $_REQUEST = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL_START, $_REQUEST) + )->getArray(); $return_path = $_REQUEST['return'] ?? ''; $preview = intval($_REQUEST['preview'] ?? 0); @@ -68,7 +59,7 @@ function item_post(App $a) { */ if (!$preview && !empty($_REQUEST['post_id_random'])) { if (DI::session()->get('post-random') == $_REQUEST['post_id_random']) { - Logger::warning('duplicate post'); + DI::logger()->warning('duplicate post'); item_post_return(DI::baseUrl(), $return_path); } else { DI::session()->set('post-random', $_REQUEST['post_id_random']); @@ -144,10 +135,10 @@ function item_insert(int $uid, array $request, bool $preview, string $return_pat $post = DI::contentItem()->initializePost($post); $post['edit'] = null; - $post['post-type'] = $request['post_type'] ?? ''; - $post['wall'] = $request['wall'] ?? true; + $post['post-type'] = $request['post_type'] ?? ''; + $post['wall'] = $request['wall'] ?? true; $post['pubmail'] = $request['pubmail_enable'] ?? false; - $post['created'] = $request['created_at'] ?? DateTimeFormat::utcNow(); + $post['created'] = $request['created_at'] ?? DateTimeFormat::utcNow(); $post['edited'] = $post['changed'] = $post['commented'] = $post['created']; $post['app'] = ''; $post['inform'] = ''; @@ -177,18 +168,18 @@ function item_insert(int $uid, array $request, bool $preview, string $return_pat // This enables interaction like starring and saving into folders if ($toplevel_item['uid'] == 0) { $stored = Item::storeForUserByUriId($toplevel_item['uri-id'], $post['uid'], ['post-reason' => Item::PR_ACTIVITY]); - Logger::info('Public item stored for user', ['uri-id' => $toplevel_item['uri-id'], 'uid' => $post['uid'], 'stored' => $stored]); + DI::logger()->info('Public item stored for user', ['uri-id' => $toplevel_item['uri-id'], 'uid' => $post['uid'], 'stored' => $stored]); } - $post['parent'] = $toplevel_item['id']; - $post['gravity'] = Item::GRAVITY_COMMENT; - $post['thr-parent'] = $parent_item['uri']; - $post['wall'] = $toplevel_item['wall']; + $post['parent'] = $toplevel_item['id']; + $post['gravity'] = Item::GRAVITY_COMMENT; + $post['thr-parent'] = $parent_item['uri']; + $post['wall'] = $toplevel_item['wall']; } else { - $parent_item = []; - $post['parent'] = 0; - $post['gravity'] = Item::GRAVITY_PARENT; - $post['thr-parent'] = $post['uri']; + $parent_item = []; + $post['parent'] = 0; + $post['gravity'] = Item::GRAVITY_PARENT; + $post['thr-parent'] = $post['uri']; } $post = DI::contentItem()->getACL($post, $parent_item, $request); @@ -209,7 +200,7 @@ function item_insert(int $uid, array $request, bool $preview, string $return_pat $post = Post::selectFirst(Item::ITEM_FIELDLIST, ['id' => $post_id]); if (!$post) { - Logger::error('Item couldn\'t be fetched.', ['post_id' => $post_id]); + DI::logger()->error('Item couldn\'t be fetched.', ['post_id' => $post_id]); if ($return_path) { DI::baseUrl()->redirect($return_path); } @@ -225,7 +216,7 @@ function item_insert(int $uid, array $request, bool $preview, string $return_pat DI::contentItem()->copyPermissions($post['thr-parent-id'], $post['uri-id'], $post['parent-uri-id']); } - Logger::debug('post_complete'); + DI::logger()->debug('post_complete'); item_post_return(DI::baseUrl(), $return_path); // NOTREACHED @@ -233,13 +224,15 @@ function item_insert(int $uid, array $request, bool $preview, string $return_pat function item_process(array $post, array $request, bool $preview, string $return_path): array { - $post['self'] = true; - $post['api_source'] = false; - $post['attach'] = ''; - $post['title'] = trim($request['title'] ?? ''); - $post['body'] = $request['body'] ?? ''; - $post['location'] = trim($request['location'] ?? ''); - $post['coord'] = trim($request['coord'] ?? ''); + $post['self'] = true; + $post['api_source'] = false; + $post['attach'] = ''; + $post['title'] = trim($request['title'] ?? ''); + $post['content-warning'] = trim($request['summary'] ?? ''); + $post['sensitive'] = !empty($request['sensitive'] ?? false); + $post['body'] = $request['body'] ?? ''; + $post['location'] = trim($request['location'] ?? ''); + $post['coord'] = trim($request['coord'] ?? ''); $post = DI::contentItem()->addCategories($post, $request['category'] ?? ''); @@ -248,7 +241,7 @@ function item_process(array $post, array $request, bool $preview, string $return $post['body'] .= DI::contentItem()->storeAttachmentFromRequest($request); } - $post = DI::contentItem()->finalizePost($post); + $post = DI::contentItem()->finalizePost($post, $preview); if (!strlen($post['body'])) { if ($preview) { @@ -278,20 +271,32 @@ function item_process(array $post, array $request, bool $preview, string $return $post['quote-uri-id'] = Item::getQuoteUriId($post['body'], $post['uid']); $post['body'] = BBCode::removeSharedData(Item::setHashtags($post['body'])); $post['writable'] = true; + $post['sensitive'] = false; + $post['post-reason'] = Item::PR_LOCAL; $o = DI::conversation()->render([$post], Conversation::MODE_SEARCH, false, true); System::jsonExit(['preview' => $o]); } - Hook::callAll('post_local',$post); + $eventDispatcher = DI::eventDispatcher(); + + $hook_data = [ + 'item' => $post, + ]; + + $hook_data = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL, $hook_data) + )->getArray(); + + $post = $hook_data['item'] ?? $post; unset($post['edit']); unset($post['self']); unset($post['api_source']); if (!empty($request['scheduled_at'])) { - $scheduled_at = DateTimeFormat::convert($request['scheduled_at'], 'UTC', DI::app()->getTimeZone()); + $scheduled_at = DateTimeFormat::convert($request['scheduled_at'], 'UTC', DI::appHelper()->getTimeZone()); if ($scheduled_at > DateTimeFormat::utcNow()) { unset($post['created']); unset($post['edited']); @@ -305,7 +310,7 @@ function item_process(array $post, array $request, bool $preview, string $return } if (!empty($post['cancel'])) { - Logger::info('mod_item: post cancelled by addon.'); + DI::logger()->info('mod_item: post cancelled by addon.'); if ($return_path) { DI::baseUrl()->redirect($return_path); } @@ -332,12 +337,12 @@ function item_post_return($baseurl, $return_path) $json['reload'] = $baseurl . '/' . $_REQUEST['jsreload']; } - Logger::debug('post_json', ['json' => $json]); + DI::logger()->debug('post_json', ['json' => $json]); System::jsonExit($json); } -function item_content(App $a) +function item_content() { if (!DI::userSession()->isAuthenticated()) { throw new HTTPException\UnauthorizedException(); @@ -452,7 +457,7 @@ function drop_item(int $id, string $return = ''): string item_redirect_after_action($item, $return); //NOTREACHED } else { - Logger::warning('Permission denied.', ['local' => DI::userSession()->getLocalUserId(), 'uid' => $item['uid'], 'cid' => $contact_id]); + DI::logger()->warning('Permission denied.', ['local' => DI::userSession()->getLocalUserId(), 'uid' => $item['uid'], 'cid' => $contact_id]); DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.')); DI::baseUrl()->redirect('display/' . $item['guid']); //NOTREACHED diff --git a/mod/lostpass.php b/mod/lostpass.php index 5eb53ae2f3..d62fd5f5aa 100644 --- a/mod/lostpass.php +++ b/mod/lostpass.php @@ -1,25 +1,12 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * */ -use Friendica\App; use Friendica\Core\Renderer; use Friendica\Database\DBA; use Friendica\DI; @@ -27,7 +14,7 @@ use Friendica\Model\User; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; -function lostpass_post(App $a) +function lostpass_post() { $loginame = trim($_POST['login-name']); if (!$loginame) { @@ -90,7 +77,7 @@ function lostpass_post(App $a) DI::baseUrl()->redirect(); } -function lostpass_content(App $a) +function lostpass_content() { if (DI::args()->getArgc() > 1) { $pwdreset_token = DI::args()->getArgv()[1]; diff --git a/mod/message.php b/mod/message.php index d75fb240f0..f3a231601c 100644 --- a/mod/message.php +++ b/mod/message.php @@ -1,25 +1,13 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * */ -use Friendica\App; use Friendica\Content\Nav; use Friendica\Content\Pager; use Friendica\Content\Text\BBCode; @@ -34,7 +22,7 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; use Friendica\Util\Temporal; -function message_init(App $a) +function message_init() { $tabs = ''; @@ -49,7 +37,7 @@ function message_init(App $a) 'accesskey' => 'm', ]; - $tpl = Renderer::getMarkupTemplate('message_side.tpl'); + $tpl = Renderer::getMarkupTemplate('message_side.tpl'); DI::page()['aside'] = Renderer::replaceMacros($tpl, [ '$tabs' => $tabs, '$new' => $new, @@ -61,7 +49,7 @@ function message_init(App $a) ]); } -function message_post(App $a) +function message_post() { if (!DI::userSession()->getLocalUserId()) { DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.')); @@ -74,7 +62,7 @@ function message_post(App $a) $body = !empty($_REQUEST['body']) ? Strings::escapeHtml(trim($_REQUEST['body'])) : ''; $recipient = !empty($_REQUEST['recipient']) ? intval($_REQUEST['recipient']) : 0; - $ret = Mail::send($sender_id, $recipient, $body, $subject, $replyto); + $ret = Mail::send($sender_id, $recipient, $body, $subject, $replyto); $norecip = false; switch ($ret) { @@ -104,7 +92,7 @@ function message_post(App $a) } } -function message_content(App $a) +function message_content() { $o = ''; Nav::setSelected('messages'); @@ -114,7 +102,7 @@ function message_content(App $a) return Login::form(); } - $myprofile = DI::baseUrl() . '/profile/' . $a->getLoggedInUserNickname(); + $myprofile = DI::baseUrl() . '/profile/' . DI::userSession()->getLocalUserNickname(); $tpl = Renderer::getMarkupTemplate('mail_head.tpl'); if (DI::args()->getArgc() > 1 && DI::args()->getArgv()[1] == 'new') { @@ -144,7 +132,7 @@ function message_content(App $a) $cmd = DI::args()->getArgv()[1]; if ($cmd === 'drop') { $message = DBA::selectFirst('mail', ['convid'], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]); - if(!DBA::isResult($message)){ + if (!DBA::isResult($message)) { DI::sysmsg()->addNotice(DI::l10n()->t('Conversation not found.')); DI::baseUrl()->redirect('message'); } @@ -154,7 +142,7 @@ function message_content(App $a) } $conversation = DBA::selectFirst('mail', ['id'], ['convid' => $message['convid'], 'uid' => DI::userSession()->getLocalUserId()]); - if(!DBA::isResult($conversation)){ + if (!DBA::isResult($conversation)) { DI::baseUrl()->redirect('message'); } @@ -177,8 +165,8 @@ function message_content(App $a) $tpl = Renderer::getMarkupTemplate('msg-header.tpl'); DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ - '$nickname' => $a->getLoggedInUserNickname(), - '$linkurl' => DI::l10n()->t('Please enter a link URL:') + '$nickname' => DI::userSession()->getLocalUserNickname(), + '$linkurl' => DI::l10n()->t('Please enter a link URL:') ]); $recipientId = DI::args()->getArgv()[2] ?? null; @@ -191,7 +179,7 @@ function message_content(App $a) '$to' => DI::l10n()->t('To:'), '$subject' => DI::l10n()->t('Subject:'), '$subjtxt' => $_REQUEST['subject'] ?? '', - '$text' => $_REQUEST['body'] ?? '', + '$text' => $_REQUEST['body'] ?? '', '$readonly' => '', '$yourmessage' => DI::l10n()->t('Your message:'), '$select' => $select, @@ -220,7 +208,7 @@ function message_content(App $a) $r = get_messages(DI::userSession()->getLocalUserId(), $pager->getStart(), $pager->getItemsPerPage()); if (!DBA::isResult($r)) { - DI::sysmsg()->addNotice(DI::l10n()->t('No messages.')); + $o .= DI::l10n()->t('You have no messages.'); return $o; } @@ -235,7 +223,8 @@ function message_content(App $a) $o .= $header; - $message = DBA::fetchFirst(" + $message = DBA::fetchFirst( + " SELECT `mail`.*, `contact`.`name`, `contact`.`url`, `contact`.`thumb` FROM `mail` LEFT JOIN `contact` ON `mail`.`contact-id` = `contact`.`id` @@ -254,11 +243,12 @@ function message_content(App $a) if ($message['convid']) { $sql_extra = "AND (`mail`.`parent-uri` = ? OR `mail`.`convid` = ?)"; - $params[] = $message['convid']; + $params[] = $message['convid']; } else { $sql_extra = "AND `mail`.`parent-uri` = ?"; } - $messages_stmt = DBA::p(" + $messages_stmt = DBA::p( + " SELECT `mail`.*, `contact`.`name`, `contact`.`url`, `contact`.`thumb` FROM `mail` LEFT JOIN `contact` ON `mail`.`contact-id` = `contact`.`id` @@ -282,12 +272,12 @@ function message_content(App $a) $tpl = Renderer::getMarkupTemplate('msg-header.tpl'); DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ - '$nickname' => $a->getLoggedInUserNickname(), - '$linkurl' => DI::l10n()->t('Please enter a link URL:') + '$nickname' => DI::userSession()->getLocalUserNickname(), + '$linkurl' => DI::l10n()->t('Please enter a link URL:') ]); - $mails = []; - $seen = 0; + $mails = []; + $seen = 0; $unknown = false; foreach ($messages as $message) { @@ -297,18 +287,18 @@ function message_content(App $a) if ($message['from-url'] == $myprofile) { $from_url = $myprofile; - $sparkle = ''; + $sparkle = ''; } else { $from_url = Contact::magicLink($message['from-url']); - $sparkle = ' sparkle'; + $sparkle = ' sparkle'; } $from_name_e = $message['from-name']; - $subject_e = $message['title']; - $body_e = BBCode::convertForUriId($message['uri-id'], $message['body']); - $to_name_e = $message['name']; + $subject_e = $message['title']; + $body_e = BBCode::convertForUriId($message['uri-id'], $message['body']); + $to_name_e = $message['name']; - $contact = Contact::getByURL($message['from-url'], false, ['thumb', 'addr', 'id', 'avatar', 'url']); + $contact = Contact::getByURL($message['from-url'], false, ['thumb', 'addr', 'id', 'avatar', 'url']); $from_photo = Contact::getThumb($contact); $mails[] = [ @@ -333,7 +323,7 @@ function message_content(App $a) $parent = ''; $tpl = Renderer::getMarkupTemplate('mail_display.tpl'); - $o = Renderer::replaceMacros($tpl, [ + $o = Renderer::replaceMacros($tpl, [ '$thread_id' => DI::args()->getArgv()[1], '$thread_subject' => $message['title'], '$thread_seen' => $seen, @@ -342,19 +332,19 @@ function message_content(App $a) '$unknown_text' => DI::l10n()->t("No secure communications available. You may be able to respond from the sender's profile page."), '$mails' => $mails, // reply - '$header' => DI::l10n()->t('Send Reply'), - '$to' => DI::l10n()->t('To:'), - '$subject' => DI::l10n()->t('Subject:'), - '$subjtxt' => $message['title'], - '$readonly' => ' readonly="readonly" style="background: #BBBBBB;" ', - '$yourmessage' => DI::l10n()->t('Your message:'), - '$text' => '', - '$select' => $select, - '$parent' => $parent, - '$upload' => DI::l10n()->t('Upload photo'), - '$insert' => DI::l10n()->t('Insert web link'), - '$submit' => DI::l10n()->t('Submit'), - '$wait' => DI::l10n()->t('Please wait') + '$header' => DI::l10n()->t('Send Reply'), + '$to' => DI::l10n()->t('To:'), + '$subject' => DI::l10n()->t('Subject:'), + '$subjtxt' => $message['title'], + '$readonly' => ' readonly="readonly" style="background: #BBBBBB;" ', + '$yourmessage' => DI::l10n()->t('Your message:'), + '$text' => '', + '$select' => $select, + '$parent' => $parent, + '$upload' => DI::l10n()->t('Upload photo'), + '$insert' => DI::l10n()->t('Insert web link'), + '$submit' => DI::l10n()->t('Submit'), + '$wait' => DI::l10n()->t('Please wait') ]); return $o; @@ -409,18 +399,15 @@ function get_messages(int $uid, int $start, int $limit): array LEFT JOIN `contact` c ON m.`contact-id` = c.`id` WHERE m.`uid` = ? ORDER BY m2.`mailcreated` DESC - LIMIT ?, ?' - , $uid, $uid, $start, $limit)); + LIMIT ?, ?', $uid, $uid, $start, $limit)); } function render_messages(array $msg, string $t): string { - $a = DI::app(); - - $tpl = Renderer::getMarkupTemplate($t); + $tpl = Renderer::getMarkupTemplate($t); $rslt = ''; - $myprofile = DI::baseUrl() . '/profile/' . $a->getLoggedInUserNickname(); + $myprofile = DI::baseUrl() . '/profile/' . DI::userSession()->getLocalUserNickname(); foreach ($msg as $rr) { if ($rr['unknown']) { @@ -431,7 +418,7 @@ function render_messages(array $msg, string $t): string $participants = DI::l10n()->t("%s and You", $rr['from-name']); } - $body_e = $rr['body']; + $body_e = $rr['body']; $to_name_e = $rr['name']; if (is_null($rr['url'])) { @@ -439,7 +426,7 @@ function render_messages(array $msg, string $t): string continue; } - $contact = Contact::getByURL($rr['url'], false, ['thumb', 'addr', 'id', 'avatar', 'url']); + $contact = Contact::getByURL($rr['url'], false, ['thumb', 'addr', 'id', 'avatar', 'url']); $from_photo = Contact::getThumb($contact); $rslt .= Renderer::replaceMacros($tpl, [ diff --git a/mod/notes.php b/mod/notes.php index 554429d0ec..478e4c4aca 100644 --- a/mod/notes.php +++ b/mod/notes.php @@ -1,25 +1,12 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * */ -use Friendica\App; use Friendica\Content\Conversation; use Friendica\Content\Nav; use Friendica\Content\Pager; @@ -29,7 +16,7 @@ use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Module\BaseProfile; -function notes_init(App $a) +function notes_init() { if (! DI::userSession()->getLocalUserId()) { return; @@ -39,14 +26,16 @@ function notes_init(App $a) } -function notes_content(App $a, bool $update = false) +function notes_content(bool $update = false) { + $contactId = DI::appHelper()->getContactId(); + if (!DI::userSession()->getLocalUserId()) { DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.')); return; } - $o = BaseProfile::getTabsHTML('notes', true, $a->getLoggedInUserNickname(), false); + $o = BaseProfile::getTabsHTML('notes', true, DI::userSession()->getLocalUserNickname(), false); if (!$update) { $o .= '

                              ' . DI::l10n()->t('Personal Notes') . '

                              '; @@ -58,11 +47,11 @@ function notes_content(App $a, bool $update = false) 'acl_data' => '', ]; - $o .= DI::conversation()->statusEditor($x, $a->getContactId()); + $o .= DI::conversation()->statusEditor($x, $contactId); } $condition = ['uid' => DI::userSession()->getLocalUserId(), 'post-type' => Item::PT_PERSONAL_NOTE, 'gravity' => Item::GRAVITY_PARENT, - 'contact-id'=> $a->getContactId()]; + 'contact-id'=> $contactId]; if (DI::mode()->isMobile()) { $itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network', diff --git a/mod/photos.php b/mod/photos.php index 53388512fa..57076fc933 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -1,38 +1,22 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * */ -use Friendica\App; use Friendica\Content\Nav; use Friendica\Content\Pager; use Friendica\Content\Text\BBCode; -use Friendica\Content\Widget; use Friendica\Core\ACL; -use Friendica\Core\Addon; -use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Renderer; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\Photo; @@ -42,7 +26,6 @@ use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Module\BaseProfile; use Friendica\Network\HTTPException; -use Friendica\Network\Probe; use Friendica\Protocol\Activity; use Friendica\Security\Security; use Friendica\Util\Crypto; @@ -53,7 +36,7 @@ use Friendica\Util\Strings; use Friendica\Util\Temporal; use Friendica\Util\XML; -function photos_init(App $a) +function photos_init() { if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) { return; @@ -62,13 +45,11 @@ function photos_init(App $a) Nav::setSelected('home'); if (DI::args()->getArgc() > 1) { - $owner = Profile::load(DI::app(), DI::args()->getArgv()[1], false); + $owner = Profile::load(DI::appHelper(), DI::args()->getArgv()[1], false); if (!isset($owner['account_removed']) || $owner['account_removed']) { throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); } - $is_owner = (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $owner['uid'])); - $albums = Photo::getAlbums($owner['uid']); $albums_visible = ((intval($owner['hidewall']) && !DI::userSession()->isAuthenticated()) ? false : true); @@ -125,27 +106,25 @@ function photos_init(App $a) return; } -function photos_post(App $a) +function photos_post() { $user = User::getByNickname(DI::args()->getArgv()[1]); if (!DBA::isResult($user)) { throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.')); } - $phototypes = Images::supportedTypes(); - - $can_post = false; - $visitor = 0; + $can_post = false; + $visitor = 0; $page_owner_uid = intval($user['uid']); - $community_page = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY; + $community_page = in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN]); if (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $page_owner_uid)) { $can_post = true; } elseif ($community_page && !empty(DI::userSession()->getRemoteContactID($page_owner_uid))) { $contact_id = DI::userSession()->getRemoteContactID($page_owner_uid); - $can_post = true; - $visitor = $contact_id; + $can_post = true; + $visitor = $contact_id; } if (!$can_post) { @@ -161,7 +140,7 @@ function photos_post(App $a) System::exit(); } - $aclFormatter = DI::aclFormatter(); + $aclFormatter = DI::aclFormatter(); $str_contact_allow = isset($_REQUEST['contact_allow']) ? $aclFormatter->toString($_REQUEST['contact_allow']) : $owner_record['allow_cid'] ?? ''; $str_circle_allow = isset($_REQUEST['circle_allow']) ? $aclFormatter->toString($_REQUEST['circle_allow']) : $owner_record['allow_gid'] ?? ''; $str_contact_deny = isset($_REQUEST['contact_deny']) ? $aclFormatter->toString($_REQUEST['contact_deny']) : $owner_record['deny_cid'] ?? ''; @@ -171,7 +150,7 @@ function photos_post(App $a) if ($visibility === 'public') { // The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected $str_contact_allow = $str_circle_allow = $str_contact_deny = $str_circle_deny = ''; - } else if ($visibility === 'custom') { + } elseif ($visibility === 'custom') { // Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL // case that would make it public. So we always append the author's contact id to the allowed contacts. // See https://github.com/friendica/friendica/issues/9672 @@ -202,7 +181,7 @@ function photos_post(App $a) // Update the photo albums cache Photo::clearAlbumCache($page_owner_uid); - DI::baseUrl()->redirect('photos/' . $a->getLoggedInUserNickname() . '/album/' . bin2hex($newalbum)); + DI::baseUrl()->redirect('photos/' . DI::userSession()->getLocalUserNickname() . '/album/' . bin2hex($newalbum)); return; // NOTREACHED } @@ -215,14 +194,14 @@ function photos_post(App $a) // get the list of photos we are about to delete if ($visitor) { $r = DBA::toArray(DBA::p( - "SELECT distinct(`resource-id`) as `rid` FROM `photo` WHERE `contact-id` = ? AND `uid` = ? AND `album` = ?", + "SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `contact-id` = ? AND `uid` = ? AND `album` = ?", $visitor, $page_owner_uid, $album )); } else { $r = DBA::toArray(DBA::p( - "SELECT distinct(`resource-id`) as `rid` FROM `photo` WHERE `uid` = ? AND `album` = ?", + "SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `uid` = ? AND `album` = ?", DI::userSession()->getLocalUserId(), $album )); @@ -296,7 +275,7 @@ function photos_post(App $a) } if (!empty($_POST['rotate']) && (intval($_POST['rotate']) == 1 || intval($_POST['rotate']) == 2)) { - Logger::debug('rotate'); + DI::logger()->debug('rotate'); $photo = Photo::getPhotoForUser($page_owner_uid, $resource_id); @@ -337,7 +316,7 @@ function photos_post(App $a) if (DBA::isResult($photos)) { $photo = $photos[0]; - $ext = $phototypes[$photo['type']]; + $ext = Images::getExtensionByMimeType($photo['type']); Photo::update( ['desc' => $desc, 'album' => $albname, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny], ['resource-id' => $resource_id, 'uid' => $page_owner_uid] @@ -352,9 +331,9 @@ function photos_post(App $a) if (DBA::isResult($photos) && !$item_id) { // Create item container $title = ''; - $uri = Item::newURI(); + $uri = Item::newURI(); - $arr = []; + $arr = []; $arr['guid'] = System::createUUID(); $arr['uid'] = $page_owner_uid; $arr['uri'] = $uri; @@ -376,7 +355,7 @@ function photos_post(App $a) $arr['visible'] = 0; $arr['origin'] = 1; - $arr['body'] = Images::getBBCodeByResource($photo['resource-id'], $user['nickname'], $photo['scale'], $ext); + $arr['body'] = Images::getBBCodeByResource($photo['resource-id'], $user['nickname'], $photo['scale'], $ext); $item_id = Item::insert($arr); } @@ -390,7 +369,7 @@ function photos_post(App $a) } if (strlen($rawtags)) { - $inform = ''; + $inform = ''; // if the new tag doesn't have a namespace specifier (@foo or #foo) give it a hashtag $x = substr($rawtags, 0, 1); @@ -399,44 +378,20 @@ function photos_post(App $a) } $taginfo = []; - $tags = BBCode::getTags($rawtags); + $tags = BBCode::getTags($rawtags); if (count($tags)) { foreach ($tags as $tag) { if (strpos($tag, '@') === 0) { $profile = ''; - $contact = null; - $name = substr($tag, 1); - - if ((strpos($name, '@')) || (strpos($name, 'http://'))) { + $name = substr($tag, 1); + $contact = Contact::getByURL($name); + if (empty($contact)) { $newname = $name; - $links = @Probe::lrdd($name); - - if (count($links)) { - foreach ($links as $link) { - if ($link['@attributes']['rel'] === 'http://webfinger.net/rel/profile-page') { - $profile = $link['@attributes']['href']; - } - - if ($link['@attributes']['rel'] === 'salmon') { - $salmon = '$url:' . str_replace(',', '%sc', $link['@attributes']['href']); - - if (strlen($inform)) { - $inform .= ','; - } - - $inform .= $salmon; - } - } - } - - $taginfo[] = [$newname, $profile, $salmon]; - } else { - $newname = $name; - $tagcid = 0; - if (strrpos($newname, '+')) { $tagcid = intval(substr($newname, strrpos($newname, '+') + 1)); + } else { + $tagcid = 0; } if ($tagcid) { @@ -456,17 +411,17 @@ function photos_post(App $a) ); } } + } - if (DBA::isResult($contact)) { - $newname = $contact['name']; - $profile = $contact['url']; + if (DBA::isResult($contact)) { + $newname = $contact['name']; + $profile = $contact['url']; - $notify = 'cid:' . $contact['id']; - if (strlen($inform)) { - $inform .= ','; - } - $inform .= $notify; + $notify = 'cid:' . $contact['id']; + if (strlen($inform)) { + $inform .= ','; } + $inform .= $notify; } if ($profile) { @@ -497,7 +452,7 @@ function photos_post(App $a) } $newinform .= $inform; - $fields = ['inform' => $newinform, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()]; + $fields = ['inform' => $newinform, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()]; $condition = ['id' => $item_id]; Item::update($fields, $condition); @@ -561,7 +516,7 @@ function photos_post(App $a) } } -function photos_content(App $a) +function photos_content() { // URLs: // photos/name/upload @@ -590,15 +545,13 @@ function photos_content(App $a) $profile = Profile::getByUID($user['uid']); - $phototypes = Images::supportedTypes(); - $_SESSION['photo_return'] = DI::args()->getCommand(); // Parse arguments $datum = null; if (DI::args()->getArgc() > 3) { $datatype = DI::args()->getArgv()[2]; - $datum = DI::args()->getArgv()[3]; + $datum = DI::args()->getArgv()[3]; } elseif ((DI::args()->getArgc() > 2) && (DI::args()->getArgv()[2] === 'upload')) { $datatype = 'upload'; } else { @@ -622,18 +575,18 @@ function photos_content(App $a) $owner_uid = $user['uid']; - $community_page = (($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY) ? true : false); + $community_page = in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN]); if (DI::userSession()->getLocalUserId() && (DI::userSession()->getLocalUserId() == $owner_uid)) { $can_post = true; } elseif ($community_page && !empty(DI::userSession()->getRemoteContactID($owner_uid))) { $contact_id = DI::userSession()->getRemoteContactID($owner_uid); - $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => $owner_uid, 'blocked' => false, 'pending' => false]); + $contact = DBA::selectFirst('contact', [], ['id' => $contact_id, 'uid' => $owner_uid, 'blocked' => false, 'pending' => false]); if (DBA::isResult($contact)) { - $can_post = true; + $can_post = true; $remote_contact = true; - $visitor = $contact_id; + $visitor = $contact_id; } } @@ -676,31 +629,31 @@ function photos_content(App $a) $selname = (!is_null($datum) && Strings::isHex($datum)) ? hex2bin($datum) : ''; - $albumselect = ''; + $albumselect = ['' => '']; - $albumselect .= ''; - $albums = Photo::getAlbums($owner_uid); - if (!empty($albums)) { - foreach ($albums as $album) { - if ($album['album'] === '') { - continue; - } - $selected = (($selname === $album['album']) ? ' selected="selected" ' : ''); - $albumselect .= ''; + foreach (Photo::getAlbums($owner_uid) as $album) { + if ($album['album'] === '') { + continue; } + + $albumselect[$album['album']] = $album['album']; } $uploader = ''; $ret = [ - 'post_url' => 'profile/' . $user['nickname'] . '/photos', - 'addon_text' => $uploader, + 'post_url' => 'profile/' . $user['nickname'] . '/photos', + 'addon_text' => $uploader, 'default_upload' => true ]; - Hook::callAll('photo_upload_form', $ret); + $eventDispatcher = DI::eventDispatcher(); - $default_upload_box = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_box.tpl'), []); + $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::PHOTO_UPLOAD_FORM, $ret) + ); + + $default_upload_box = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_box.tpl'), []); $default_upload_submit = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_submit.tpl'), [ '$submit' => DI::l10n()->t('Submit'), ]); @@ -722,24 +675,25 @@ function photos_content(App $a) $tpl = Renderer::getMarkupTemplate('photos_upload.tpl'); - $aclselect_e = ($visitor ? '' : ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId())); + $aclselect_e = ($visitor ? '' : ACL::getFullSelectorHTML(DI::page(), DI::userSession()->getLocalUserId())); $o .= Renderer::replaceMacros($tpl, [ - '$pagename' => DI::l10n()->t('Upload Photos'), - '$sessid' => session_id(), - '$usage' => $usage_message, - '$nickname' => $user['nickname'], - '$newalbum' => DI::l10n()->t('New album name: '), - '$existalbumtext' => DI::l10n()->t('or select existing album:'), - '$nosharetext' => DI::l10n()->t('Do not show a status post for this upload'), - '$albumselect' => $albumselect, - '$permissions' => DI::l10n()->t('Permissions'), - '$aclselect' => $aclselect_e, - '$lockstate' => ACL::getLockstateForUserId($a->getLoggedInUserId()) ? 'lock' : 'unlock', - '$alt_uploader' => $ret['addon_text'], - '$default_upload_box' => ($ret['default_upload'] ? $default_upload_box : ''), + '$pagename' => DI::l10n()->t('Upload Photos'), + '$sessid' => session_id(), + '$usage' => $usage_message, + '$nickname' => $user['nickname'], + '$newalbum' => DI::l10n()->t('New album name: '), + '$existalbumtext' => DI::l10n()->t('or select existing album:'), + '$nosharetext' => DI::l10n()->t('Do not show a status post for this upload'), + '$albumselect' => $albumselect, + '$selname' => $selname, + '$permissions' => DI::l10n()->t('Permissions'), + '$aclselect' => $aclselect_e, + '$lockstate' => ACL::getLockstateForUserId(DI::userSession()->getLocalUserId()) ? 'lock' : 'unlock', + '$alt_uploader' => $ret['addon_text'], + '$default_upload_box' => ($ret['default_upload'] ? $default_upload_box : ''), '$default_upload_submit' => ($ret['default_upload'] ? $default_upload_submit : ''), - '$uploadurl' => $ret['post_url'], + '$uploadurl' => $ret['post_url'], // ACL permissions box '$return_path' => DI::args()->getQueryString(), @@ -761,8 +715,8 @@ function photos_content(App $a) } $total = 0; - $r = DBA::toArray(DBA::p( - "SELECT `resource-id`, max(`scale`) AS `scale` FROM `photo` WHERE `uid` = ? AND `album` = ? + $r = DBA::toArray(DBA::p( + "SELECT `resource-id`, MAX(`scale`) AS `scale` FROM `photo` WHERE `uid` = ? AND `album` = ? AND `scale` <= 4 $sql_extra GROUP BY `resource-id`", $owner_uid, $album @@ -782,9 +736,9 @@ function photos_content(App $a) } $r = DBA::toArray(DBA::p( - "SELECT `resource-id`, ANY_VALUE(`id`) AS `id`, ANY_VALUE(`filename`) AS `filename`, - ANY_VALUE(`type`) AS `type`, max(`scale`) AS `scale`, ANY_VALUE(`desc`) as `desc`, - ANY_VALUE(`created`) as `created` + "SELECT `resource-id`, MIN(`id`) AS `id`, MIN(`filename`) AS `filename`, + MIN(`type`) AS `type`, MAX(`scale`) AS `scale`, MIN(`desc`) AS `desc`, + MIN(`created`) AS `created` FROM `photo` WHERE `uid` = ? AND `album` = ? AND `scale` <= 4 $sql_extra GROUP BY `resource-id` ORDER BY `created` $order LIMIT ? , ?", intval($owner_uid), @@ -797,7 +751,7 @@ function photos_content(App $a) $drop_url = DI::args()->getQueryString(); return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ - '$l10n' => [ + '$l10n' => [ 'message' => DI::l10n()->t('Do you really want to delete this photo album and all its photos?'), 'confirm' => DI::l10n()->t('Delete Album'), 'cancel' => DI::l10n()->t('Cancel'), @@ -817,11 +771,11 @@ function photos_content(App $a) $album_e = $album; $o .= Renderer::replaceMacros($edit_tpl, [ - '$nametext' => DI::l10n()->t('New album name: '), - '$nickname' => $user['nickname'], - '$album' => $album_e, - '$hexalbum' => bin2hex($album), - '$submit' => DI::l10n()->t('Submit'), + '$nametext' => DI::l10n()->t('New album name: '), + '$nickname' => $user['nickname'], + '$album' => $album_e, + '$hexalbum' => bin2hex($album), + '$submit' => DI::l10n()->t('Submit'), '$dropsubmit' => DI::l10n()->t('Delete Album') ]); } @@ -831,7 +785,7 @@ function photos_content(App $a) } if ($order_field === 'created') { - $order = [DI::l10n()->t('Show Newest First'), 'photos/' . $user['nickname'] . '/album/' . bin2hex($album), 'oldest']; + $order = [DI::l10n()->t('Show Newest First'), 'photos/' . $user['nickname'] . '/album/' . bin2hex($album), 'oldest']; } else { $order = [DI::l10n()->t('Show Oldest First'), 'photos/' . $user['nickname'] . '/album/' . bin2hex($album) . '?order=created', 'newest']; } @@ -844,10 +798,10 @@ function photos_content(App $a) foreach ($r as $rr) { $twist = !$twist; - $ext = $phototypes[$rr['type']]; + $ext = Images::getExtensionByMimeType($rr['type']); $imgalt_e = $rr['filename']; - $desc_e = $rr['desc']; + $desc_e = $rr['desc']; $photos[] = [ 'id' => $rr['id'], @@ -855,7 +809,7 @@ function photos_content(App $a) 'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id'] . ($order_field === 'created' ? '?order=created' : ''), 'title' => DI::l10n()->t('View Photo'), - 'src' => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . '.' . $ext, + 'src' => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . $ext, 'alt' => $imgalt_e, 'desc' => $desc_e, 'ext' => $ext, @@ -866,13 +820,13 @@ function photos_content(App $a) $tpl = Renderer::getMarkupTemplate('photo_album.tpl'); $o .= Renderer::replaceMacros($tpl, [ - '$photos' => $photos, - '$album' => $album, + '$photos' => $photos, + '$album' => $album, '$can_post' => $can_post, - '$upload' => [DI::l10n()->t('Upload New Photos'), 'photos/' . $user['nickname'] . '/upload/' . bin2hex($album)], - '$order' => $order, - '$edit' => $edit, - '$drop' => $drop, + '$upload' => [DI::l10n()->t('Upload New Photos'), 'photos/' . $user['nickname'] . '/upload/' . bin2hex($album)], + '$order' => $order, + '$edit' => $edit, + '$drop' => $drop, '$paginate' => $pager->renderFull($total), ]); @@ -897,7 +851,7 @@ function photos_content(App $a) $drop_url = DI::args()->getQueryString(); return Renderer::replaceMacros(Renderer::getMarkupTemplate('confirm.tpl'), [ - '$l10n' => [ + '$l10n' => [ 'message' => DI::l10n()->t('Do you really want to delete this photo?'), 'confirm' => DI::l10n()->t('Delete Photo'), 'cancel' => DI::l10n()->t('Cancel'), @@ -999,8 +953,8 @@ function photos_content(App $a) if ($cmd === 'edit') { $tools['view'] = ['photos/' . $user['nickname'] . '/image/' . $datum, DI::l10n()->t('View photo')]; } else { - $tools['edit'] = ['photos/' . $user['nickname'] . '/image/' . $datum . '/edit', DI::l10n()->t('Edit photo')]; - $tools['delete'] = ['photos/' . $user['nickname'] . '/image/' . $datum . '/drop', DI::l10n()->t('Delete photo')]; + $tools['edit'] = ['photos/' . $user['nickname'] . '/image/' . $datum . '/edit', DI::l10n()->t('Edit photo')]; + $tools['delete'] = ['photos/' . $user['nickname'] . '/image/' . $datum . '/drop', DI::l10n()->t('Delete photo')]; $tools['profile'] = ['settings/profile/photo/crop/' . $ph[0]['resource-id'], DI::l10n()->t('Use as profile photo')]; } @@ -1013,18 +967,18 @@ function photos_content(App $a) } $photo = [ - 'href' => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . '.' . $phototypes[$hires['type']], + 'href' => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . Images::getExtensionByMimeType($hires['type']), 'title' => DI::l10n()->t('View Full Size'), - 'src' => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . '.' . $phototypes[$lores['type']] . '?_u=' . DateTimeFormat::utcNow('ymdhis'), + 'src' => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . Images::getExtensionByMimeType($lores['type']) . '?_u=' . DateTimeFormat::utcNow('ymdhis'), 'height' => $hires['height'], 'width' => $hires['width'], 'album' => $hires['album'], 'filename' => $hires['filename'], ]; - $map = null; + $map = null; $link_item = []; - $total = 0; + $total = 0; // Do we have an item for this photo? @@ -1038,12 +992,12 @@ function photos_content(App $a) if (!empty($link_item['parent']) && !empty($link_item['uid'])) { $condition = ["`parent` = ? AND `gravity` = ?", $link_item['parent'], Item::GRAVITY_COMMENT]; - $total = Post::count($condition); + $total = Post::count($condition); $pager = new Pager(DI::l10n(), DI::args()->getQueryString()); $params = ['order' => ['id'], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]]; - $items = Post::toArray(Post::selectForUser($link_item['uid'], Item::ITEM_FIELDLIST, $condition, $params)); + $items = Post::toArray(Post::selectForUser($link_item['uid'], array_merge(Item::ITEM_FIELDLIST, ['author-alias']), $condition, $params)); if (DI::userSession()->getLocalUserId() == $link_item['uid']) { Item::update(['unseen' => false], ['parent' => $link_item['parent']]); @@ -1068,54 +1022,55 @@ function photos_content(App $a) } } $tags = ['title' => DI::l10n()->t('Tags: '), 'tags' => $tag_arr]; - if ($cmd === 'edit') { + if ($cmd === 'edit' && !empty($tag_arr)) { $tags['removeanyurl'] = 'post/' . $link_item['id'] . '/tag/remove?return=' . urlencode(DI::args()->getCommand()); - $tags['removetitle'] = DI::l10n()->t('[Select tags to remove]'); + $tags['removetitle'] = DI::l10n()->t('[Select tags to remove]'); } } - $edit = Null; + $edit = null; if ($cmd === 'edit' && $can_post) { $edit_tpl = Renderer::getMarkupTemplate('photo_edit.tpl'); - $album_e = $ph[0]['album']; - $caption_e = $ph[0]['desc']; - $aclselect_e = ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId(), false, ACL::getDefaultUserPermissions($ph[0])); + $album_e = $ph[0]['album']; + $caption_e = $ph[0]['desc']; + $aclselect_e = ACL::getFullSelectorHTML(DI::page(), DI::userSession()->getLocalUserId(), false, ACL::getDefaultUserPermissions($ph[0])); $edit = Renderer::replaceMacros($edit_tpl, [ - '$id' => $ph[0]['id'], - '$album' => ['albname', DI::l10n()->t('New album name'), $album_e, ''], - '$caption' => ['desc', DI::l10n()->t('Caption'), $caption_e, ''], - '$tags' => ['newtag', DI::l10n()->t('Add a Tag'), "", DI::l10n()->t('Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping')], + '$id' => $ph[0]['id'], + '$album' => ['albname', DI::l10n()->t('New album name'), $album_e, ''], + '$caption' => ['desc', DI::l10n()->t('Caption'), $caption_e, ''], + '$tags' => ['newtag', DI::l10n()->t('Add a Tag'), "", DI::l10n()->t('Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping')], '$rotate_none' => ['rotate', DI::l10n()->t('Do not rotate'), 0, '', true], - '$rotate_cw' => ['rotate', DI::l10n()->t("Rotate CW \x28right\x29"), 1, ''], - '$rotate_ccw' => ['rotate', DI::l10n()->t("Rotate CCW \x28left\x29"), 2, ''], + '$rotate_cw' => ['rotate', DI::l10n()->t("Rotate CW \x28right\x29"), 1, ''], + '$rotate_ccw' => ['rotate', DI::l10n()->t("Rotate CCW \x28left\x29"), 2, ''], - '$nickname' => $user['nickname'], + '$nickname' => $user['nickname'], '$resource_id' => $ph[0]['resource-id'], '$permissions' => DI::l10n()->t('Permissions'), - '$aclselect' => $aclselect_e, + '$aclselect' => $aclselect_e, '$item_id' => $link_item['id'] ?? 0, - '$submit' => DI::l10n()->t('Submit'), - '$delete' => DI::l10n()->t('Delete Photo'), + '$submit' => DI::l10n()->t('Submit'), + '$delete' => DI::l10n()->t('Delete Photo'), // ACL permissions box '$return_path' => DI::args()->getQueryString(), ]); } - $like = ''; - $dislike = ''; + $like = ''; + $dislike = ''; $likebuttons = ''; - $comments = ''; - $paginate = ''; + $comments = ''; + $paginate = ''; if (!empty($link_item['id']) && !empty($link_item['uri'])) { - $cmnt_tpl = Renderer::getMarkupTemplate('comment_item.tpl'); - $tpl = Renderer::getMarkupTemplate('photo_item.tpl'); + $cmnt_tpl = Renderer::getMarkupTemplate('comment_item.tpl'); + $tpl = Renderer::getMarkupTemplate('photo_item.tpl'); $return_path = DI::args()->getCommand(); + $addonHelper = DI::addonHelper(); if (!DBA::isResult($items)) { if (($can_post || Security::canWriteToUserWall($owner_uid))) { @@ -1124,26 +1079,26 @@ function photos_content(App $a) * This should be better if done by a hook */ $qcomment = null; - if (Addon::isEnabled('qcomment')) { - $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); + if ($addonHelper->isAddonEnabled('qcomment')) { + $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); $qcomment = $words ? explode("\n", $words) : []; } $comments .= Renderer::replaceMacros($cmnt_tpl, [ '$return_path' => '', - '$jsreload' => $return_path, - '$id' => $link_item['id'], - '$parent' => $link_item['id'], - '$profile_uid' => $owner_uid, - '$mylink' => $contact['url'], - '$mytitle' => DI::l10n()->t('This is you'), - '$myphoto' => $contact['thumb'], - '$comment' => DI::l10n()->t('Comment'), - '$submit' => DI::l10n()->t('Submit'), - '$preview' => DI::l10n()->t('Preview'), - '$loading' => DI::l10n()->t('Loading...'), - '$qcomment' => $qcomment, - '$rand_num' => Crypto::randomDigits(12), + '$jsreload' => $return_path, + '$id' => $link_item['id'], + '$parent' => $link_item['id'], + '$profile_uid' => $owner_uid, + '$mylink' => $contact['url'], + '$mytitle' => DI::l10n()->t('This is you'), + '$myphoto' => $contact['thumb'], + '$comment' => DI::l10n()->t('Comment'), + '$submit' => DI::l10n()->t('Submit'), + '$preview' => DI::l10n()->t('Preview'), + '$loading' => DI::l10n()->t('Loading...'), + '$qcomment' => $qcomment, + '$rand_num' => Crypto::randomDigits(12), ]); } } @@ -1167,11 +1122,11 @@ function photos_content(App $a) } if (!empty($conv_responses['like'][$link_item['uri']])) { - $like = DI::conversation()->formatActivity($conv_responses['like'][$link_item['uri']]['links'], 'like', $link_item['id']); + $like = DI::conversation()->formatActivity($conv_responses['like'][$link_item['uri']]['links'], 'like', $link_item['id'], '', []); } if (!empty($conv_responses['dislike'][$link_item['uri']])) { - $dislike = DI::conversation()->formatActivity($conv_responses['dislike'][$link_item['uri']]['links'], 'dislike', $link_item['id']); + $dislike = DI::conversation()->formatActivity($conv_responses['dislike'][$link_item['uri']]['links'], 'dislike', $link_item['id'], '', []); } if (($can_post || Security::canWriteToUserWall($owner_uid))) { @@ -1180,30 +1135,30 @@ function photos_content(App $a) * This should be better if done by a hook */ $qcomment = null; - if (Addon::isEnabled('qcomment')) { - $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); + if ($addonHelper->isAddonEnabled('qcomment')) { + $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); $qcomment = $words ? explode("\n", $words) : []; } $comments .= Renderer::replaceMacros($cmnt_tpl, [ '$return_path' => '', - '$jsreload' => $return_path, - '$id' => $link_item['id'], - '$parent' => $link_item['id'], - '$profile_uid' => $owner_uid, - '$mylink' => $contact['url'], - '$mytitle' => DI::l10n()->t('This is you'), - '$myphoto' => $contact['thumb'], - '$comment' => DI::l10n()->t('Comment'), - '$submit' => DI::l10n()->t('Submit'), - '$preview' => DI::l10n()->t('Preview'), - '$qcomment' => $qcomment, - '$rand_num' => Crypto::randomDigits(12), + '$jsreload' => $return_path, + '$id' => $link_item['id'], + '$parent' => $link_item['id'], + '$profile_uid' => $owner_uid, + '$mylink' => $contact['url'], + '$mytitle' => DI::l10n()->t('This is you'), + '$myphoto' => $contact['thumb'], + '$comment' => DI::l10n()->t('Comment'), + '$submit' => DI::l10n()->t('Submit'), + '$preview' => DI::l10n()->t('Preview'), + '$qcomment' => $qcomment, + '$rand_num' => Crypto::randomDigits(12), ]); } foreach ($items as $item) { - $comment = ''; + $comment = ''; $template = $tpl; $activity = DI::activity(); @@ -1230,7 +1185,7 @@ function photos_content(App $a) } $dropping = (($item['contact-id'] == $contact_id) || ($item['uid'] == DI::userSession()->getLocalUserId())); - $drop = [ + $drop = [ 'dropping' => $dropping, 'pagedrop' => false, 'select' => DI::l10n()->t('Select'), @@ -1238,7 +1193,7 @@ function photos_content(App $a) ]; $title_e = $item['title']; - $body_e = BBCode::convertForUriId($item['uri-id'], $item['body']); + $body_e = BBCode::convertForUriId($item['uri-id'], $item['body']); $comments .= Renderer::replaceMacros($template, [ '$id' => $item['id'], @@ -1260,25 +1215,25 @@ function photos_content(App $a) * This should be better if done by a hook */ $qcomment = null; - if (Addon::isEnabled('qcomment')) { - $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); + if ($addonHelper->isAddonEnabled('qcomment')) { + $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); $qcomment = $words ? explode("\n", $words) : []; } $comments .= Renderer::replaceMacros($cmnt_tpl, [ '$return_path' => '', - '$jsreload' => $return_path, - '$id' => $item['id'], - '$parent' => $item['parent'], - '$profile_uid' => $owner_uid, - '$mylink' => $contact['url'], - '$mytitle' => DI::l10n()->t('This is you'), - '$myphoto' => $contact['thumb'], - '$comment' => DI::l10n()->t('Comment'), - '$submit' => DI::l10n()->t('Submit'), - '$preview' => DI::l10n()->t('Preview'), - '$qcomment' => $qcomment, - '$rand_num' => Crypto::randomDigits(12), + '$jsreload' => $return_path, + '$id' => $item['id'], + '$parent' => $item['parent'], + '$profile_uid' => $owner_uid, + '$mylink' => $contact['url'], + '$mytitle' => DI::l10n()->t('This is you'), + '$myphoto' => $contact['thumb'], + '$comment' => DI::l10n()->t('Comment'), + '$submit' => DI::l10n()->t('Submit'), + '$preview' => DI::l10n()->t('Preview'), + '$qcomment' => $qcomment, + '$rand_num' => Crypto::randomDigits(12), ]); } } @@ -1292,17 +1247,17 @@ function photos_content(App $a) } if ($cmd === 'view' && ($can_post || Security::canWriteToUserWall($owner_uid))) { - $like_tpl = Renderer::getMarkupTemplate('like_noshare.tpl'); + $like_tpl = Renderer::getMarkupTemplate('like_noshare.tpl'); $likebuttons = Renderer::replaceMacros($like_tpl, [ - '$id' => $link_item['id'], - '$like' => DI::l10n()->t('Like'), - '$like_title' => DI::l10n()->t('I like this (toggle)'), - '$dislike' => DI::l10n()->t('Dislike'), - '$wait' => DI::l10n()->t('Please wait'), + '$id' => $link_item['id'], + '$like' => DI::l10n()->t('Like'), + '$like_title' => DI::l10n()->t('I like this (toggle)'), + '$dislike' => DI::l10n()->t('Dislike'), + '$wait' => DI::l10n()->t('Please wait'), '$dislike_title' => DI::l10n()->t('I don\'t like this (toggle)'), - '$hide_dislike' => DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'hide_dislike'), - '$responses' => $responses, - '$return_path' => DI::args()->getQueryString(), + '$hide_dislike' => DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'hide_dislike'), + '$responses' => $responses, + '$return_path' => DI::args()->getQueryString(), ]); } @@ -1311,22 +1266,22 @@ function photos_content(App $a) $photo_tpl = Renderer::getMarkupTemplate('photo_view.tpl'); $o .= Renderer::replaceMacros($photo_tpl, [ - '$id' => $ph[0]['id'], - '$album' => [$album_link, $ph[0]['album']], - '$tools' => $tools, - '$photo' => $photo, - '$prevlink' => $prevlink, - '$nextlink' => $nextlink, - '$desc' => $ph[0]['desc'], - '$tags' => $tags, - '$edit' => $edit, - '$map' => $map, - '$map_text' => DI::l10n()->t('Map'), + '$id' => $ph[0]['id'], + '$album' => [$album_link, $ph[0]['album']], + '$tools' => $tools, + '$photo' => $photo, + '$prevlink' => $prevlink, + '$nextlink' => $nextlink, + '$desc' => $ph[0]['desc'], + '$tags' => $tags, + '$edit' => $edit, + '$map' => $map, + '$map_text' => DI::l10n()->t('Map'), '$likebuttons' => $likebuttons, - '$like' => $like, - '$dislike' => $dislike, - '$comments' => $comments, - '$paginate' => $paginate, + '$like' => $like, + '$dislike' => $dislike, + '$comments' => $comments, + '$paginate' => $paginate, ]); DI::page()['htmlhead'] .= "\n" . '' . "\n"; diff --git a/mod/update_contact.php b/mod/update_contact.php index b97e9ec7be..adfbc513d7 100644 --- a/mod/update_contact.php +++ b/mod/update_contact.php @@ -1,34 +1,21 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * See update_profile.php for documentation * */ -use Friendica\App; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Post; use Friendica\Model\Contact; -function update_contact_content(App $a) +function update_contact_content() { if (!empty(DI::args()->get(1)) && !empty($_GET['force'])) { $contact = DBA::selectFirst('account-user-view', ['pid', 'deleted'], ['id' => DI::args()->get(1)]); diff --git a/mod/update_notes.php b/mod/update_notes.php index f650fbb1d9..06bbac6930 100644 --- a/mod/update_notes.php +++ b/mod/update_notes.php @@ -1,32 +1,18 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * AJAX synchronisation of notes page */ -use Friendica\App; use Friendica\Core\System; -use Friendica\DI; require_once 'mod/notes.php'; -function update_notes_content(App $a) +function update_notes_content() { $profile_uid = intval($_GET['p']); @@ -40,7 +26,7 @@ function update_notes_content(App $a) * */ - $text = notes_content($a, $profile_uid); + $text = notes_content($profile_uid); System::htmlUpdateExit($text); } diff --git a/mods/fpostit/README b/mods/fpostit/README deleted file mode 100644 index 39b7c57613..0000000000 --- a/mods/fpostit/README +++ /dev/null @@ -1,8 +0,0 @@ -fpostit - -original author: Devlon Duthied - -see his blog posting: -http://blog.duthied.com/2011/09/13/node-agnostic-friendika-bookmarklet/ - -original published at github https://github.com/duthied/Friendika-Bookmarklet diff --git a/mods/fpostit/fpostit.js b/mods/fpostit/fpostit.js deleted file mode 100644 index a6c75aba8f..0000000000 --- a/mods/fpostit/fpostit.js +++ /dev/null @@ -1,11 +0,0 @@ -javascript: (function() { - the_url = 'http://testbubble.com/fpostit.php?url=' + encodeURIComponent(window.location.href) + '&title=' + encodeURIComponent(document.title) + '&text=' + encodeURIComponent('' (window.getSelection ? window.getSelection() : document.getSelection ? document.getSelection() : document.selection.createRange().text)); - a_funct = function() { - if (!window.open(the_url, 'fpostit', 'location=yes,links=no,scrollbars=no,toolbar=no,width=600,height=300')) location.href = the_url; - }; - if (/Firefox/.test(navigator.userAgent)) { - setTimeout(a_funct, 0) - } else { - a_funct() - } -})() \ No newline at end of file diff --git a/mods/fpostit/fpostit.php b/mods/fpostit/fpostit.php deleted file mode 100644 index 5024d73302..0000000000 --- a/mods/fpostit/fpostit.php +++ /dev/null @@ -1,148 +0,0 @@ -. - * - */ - -if (($_POST["friendica_acct_name"] != '') && ($_POST["friendica_password"] != '')) { - setcookie("username", $_POST["friendica_acct_name"], time()+60*60*24*300); - setcookie("password", $_POST["friendica_password"], time()+60*60*24*300); -} - -?> - - - - - - - $content]; - - // echo "posting to: $url
                              "; - - $c = curl_init(); - curl_setopt($c, CURLOPT_URL, $url); - curl_setopt($c, CURLOPT_USERPWD, "$username:$password"); - curl_setopt($c, CURLOPT_POSTFIELDS, $data); - curl_setopt($c, CURLOPT_RETURNTRANSFER, true); - curl_setopt($c, CURLOPT_FOLLOWLOCATION, true); - $c_result = curl_exec($c); - if(curl_errno($c)){ - $error = curl_error($c); - showForm($error, $content); - } - - curl_close($c); - if (!isset($error)) { - echo ''; - } - - } else { - $error = "Missing account name and/or password...try again please"; - showForm($error, $content); - } - -} else { - showForm(null, $content); -} - -function showForm($error, $content) { - $username_cookie = $_COOKIE['username']; - $password_cookie = $_COOKIE['password']; - - echo << -

                              - Friendica Bookmarklet

                              - - -
                              -
                              - Enter the email address of the Friendica Account that you want to cross-post to:(example: user@friendica.org)

                              - Account ID:
                              - Password:
                              -
                              -   $error -
                              -

                              -
                              -EOF; - -} -?> - - - \ No newline at end of file diff --git a/mods/home.html b/mods/home.html index b881cf8ff2..7521f4e3e1 100644 --- a/mods/home.html +++ b/mods/home.html @@ -12,7 +12,7 @@

                              What other networks does it interact with?

                              -

                              Every network that speaks either the DFRN2, ActivityPub, OStatus or diaspora* protocol. Currently this list includes: diaspora*, friendica, funkwhale, ganggo, GNU social, Hubzilla, Mastodon, NextClout social, pixelfed, Pleroma, postActivi, Osada, Socialhome and many more.

                              +

                              Every network that speaks either the DFRN2, ActivityPub or diaspora* protocol. Currently this list includes: diaspora*, friendica, funkwhale, ganggo, GNU social, Hubzilla, Mastodon, NextClout social, pixelfed, Pleroma, Socialhome and many more.

                              Learn more at fediverse.party

                              diff --git a/mods/license/license_php.template b/mods/license/license_php.template deleted file mode 100644 index f77cf62e7a..0000000000 --- a/mods/license/license_php.template +++ /dev/null @@ -1,19 +0,0 @@ -. - * diff --git a/mods/local.config.ci.php b/mods/local.config.ci.php index 2da6b2a1ab..fbb3bb2d02 100644 --- a/mods/local.config.ci.php +++ b/mods/local.config.ci.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * */ diff --git a/mods/sample-systemd.service b/mods/sample-systemd.service index aae2e5825c..ac37359989 100644 --- a/mods/sample-systemd.service +++ b/mods/sample-systemd.service @@ -6,4 +6,4 @@ Description=Friendica Worker User=http #Adapt the path in the following line to your system, use 'which php' to find php path, #provide the absolute path for worker.php -ExecStart=/usr/bin/php /www/path/bin/worker.php & +ExecStart=/usr/bin/php /www/path/bin/console.php worker & diff --git a/ruleset.xml.license b/ruleset.xml.license new file mode 100644 index 0000000000..985c307f25 --- /dev/null +++ b/ruleset.xml.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010-2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/security.txt b/security.txt index d3adcec347..e36afeaa87 100644 --- a/security.txt +++ b/security.txt @@ -1,6 +1,6 @@ Contact: mailto:info@friendi.ca -Expires: 2024-10-30T23:59:59Z +Expires: 2025-06-30T23:59:59Z Preferred-Languages: en diff --git a/security.txt.license b/security.txt.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/security.txt.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/src/App.php b/src/App.php index a01a6bc6c8..ffcbfd1544 100644 --- a/src/App.php +++ b/src/App.php @@ -1,53 +1,56 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; -use Exception; +use Dice\Dice; use Friendica\App\Arguments; use Friendica\App\BaseURL; +use Friendica\App\Mode; +use Friendica\App\Page; +use Friendica\App\Request; +use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; +use Friendica\Capabilities\ICanHandleRequests; use Friendica\Content\Nav; +use Friendica\Core\Addon\AddonHelper; +use Friendica\Core\Addon\Capability\ICanLoadAddons; use Friendica\Core\Config\Factory\Config; +use Friendica\Core\Container; +use Friendica\Core\Hooks\HookEventBridge; +use Friendica\Core\Logger\LoggerManager; +use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\DiceContainer; +use Friendica\Core\L10n; +use Friendica\Core\Logger\Capability\LogChannel; +use Friendica\Core\Logger\Handler\ErrorHandler; +use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; +use Friendica\Core\System; +use Friendica\Core\Update; use Friendica\Database\Definition\DbaDefinition; use Friendica\Database\Definition\ViewDefinition; +use Friendica\Event\ConfigLoadedEvent; +use Friendica\Event\Event; use Friendica\Module\Maintenance; -use Friendica\Security\Authentication; -use Friendica\Core\Config\ValueObject\Cache; -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; -use Friendica\Core\L10n; -use Friendica\Core\System; -use Friendica\Core\Theme; -use Friendica\Database\Database; -use Friendica\Model\Contact; -use Friendica\Model\Profile; use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Network\HTTPException; +use Friendica\Protocol\ATProtocol\DID; +use Friendica\Security\Authentication; +use Friendica\Security\ExAuth; +use Friendica\Security\OpenWebAuth; +use Friendica\Util\BasePath; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPInputData; use Friendica\Util\HTTPSignature; use Friendica\Util\Profiler; -use Friendica\Util\Strings; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; /** @@ -59,27 +62,29 @@ use Psr\Log\LoggerInterface; * and anything else that might need to be passed around * before we spit the page out. * + * @final */ class App { const PLATFORM = 'Friendica'; - const CODENAME = 'Yellow Archangel'; - const VERSION = '2023.12'; - - // Allow themes to control internal parameters - // by changing App values in theme.php - private $theme_info = [ - 'videowidth' => 425, - 'videoheight' => 350, - ]; - - private $timezone = ''; - private $profile_owner = 0; - private $contact_id = 0; - private $queue = []; + const CODENAME = 'Interrupted Fern'; + const VERSION = '2025.02-dev'; /** - * @var App\Mode The Mode of the Application + * @internal + */ + public static function fromContainer(Container $container): self + { + return new self($container); + } + + /** + * @var Container + */ + private $container; + + /** + * @var Mode The Mode of the Application */ private $mode; @@ -88,10 +93,11 @@ class App */ private $baseURL; - /** @var string The name of the current theme */ - private $currentTheme; - /** @var string The name of the current mobile theme */ - private $currentMobileTheme; + /** @var string */ + private $requestId; + + /** @var Authentication */ + private $auth; /** * @var IManageConfigValues The config @@ -108,11 +114,6 @@ class App */ private $profiler; - /** - * @var Database The Friendica database connection - */ - private $database; - /** * @var L10n The translator */ @@ -123,224 +124,239 @@ class App */ private $args; - /** - * @var IManagePersonalConfigValues - */ - private $pConfig; - /** * @var IHandleUserSessions */ private $session; /** - * @deprecated 2022.03 - * @see IHandleUserSessions::isAuthenticated() + * @var AppHelper $appHelper */ - public function isLoggedIn(): bool + private $appHelper; + + private function __construct(Container $container) { - return $this->session->isAuthenticated(); + $this->container = $container; } /** - * @deprecated 2022.03 - * @see IHandleUserSessions::isSiteAdmin() + * @internal */ - public function isSiteAdmin(): bool + public function processRequest(ServerRequestInterface $request, float $start_time): void { - return $this->session->isSiteAdmin(); + $this->container->addRule(Mode::class, [ + 'call' => [ + ['determineRunMode', [false, $request->getServerParams()], Dice::CHAIN_CALL], + ], + ]); + + $this->setupContainerForAddons(); + + $this->setupLogChannel(LogChannel::APP); + + $this->setupLegacyServiceLocator(); + + $this->registerErrorHandler(); + + $this->registerEventDispatcher(); + + $this->requestId = $this->container->create(Request::class)->getRequestId(); + $this->auth = $this->container->create(Authentication::class); + $this->config = $this->container->create(IManageConfigValues::class); + $this->mode = $this->container->create(Mode::class); + $this->baseURL = $this->container->create(BaseURL::class); + $this->logger = $this->container->create(LoggerInterface::class); + $this->profiler = $this->container->create(Profiler::class); + $this->l10n = $this->container->create(L10n::class); + $this->args = $this->container->create(Arguments::class); + $this->session = $this->container->create(IHandleUserSessions::class); + $this->appHelper = $this->container->create(AppHelper::class); + + $addonHelper = $this->container->create(AddonHelper::class); + + $this->load( + $request->getServerParams(), + $this->container->create(DbaDefinition::class), + $this->container->create(ViewDefinition::class), + $this->mode, + $this->config, + $this->profiler, + $this->container->create(EventDispatcherInterface::class), + $this->appHelper, + $addonHelper, + ); + + $this->registerTemplateEngine(); + + $this->runFrontend( + $this->container->create(EventDispatcherInterface::class), + $this->container->create(IManagePersonalConfigValues::class), + $this->container->create(Page::class), + $this->container->create(Nav::class), + $addonHelper, + $this->container->create(ModuleHTTPException::class), + $start_time, + $request + ); } /** - * @deprecated 2022.03 - * @see IHandleUserSessions::getLocalUserId() + * @internal */ - public function getLoggedInUserId(): int + public function processConsole(array $serverParams): void { - return $this->session->getLocalUserId(); + $argv = $serverParams['argv'] ?? []; + + $this->setupContainerForAddons(); + + $this->setupLogChannel($this->determineLogChannel($argv)); + + $this->setupLegacyServiceLocator(); + + $this->registerErrorHandler(); + + $this->registerEventDispatcher(); + + $this->load( + $serverParams, + $this->container->create(DbaDefinition::class), + $this->container->create(ViewDefinition::class), + $this->container->create(Mode::class), + $this->container->create(IManageConfigValues::class), + $this->container->create(Profiler::class), + $this->container->create(EventDispatcherInterface::class), + $this->container->create(AppHelper::class), + $this->container->create(AddonHelper::class), + ); + + $this->registerTemplateEngine(); + + (\Friendica\Core\Console::create($this->container, $argv))->execute(); } /** - * @deprecated 2022.03 - * @see IHandleUserSessions::getLocalUserNickname() + * @internal */ - public function getLoggedInUserNickname(): string + public function processEjabberd(array $serverParams): void { - return $this->session->getLocalUserNickname(); + $this->setupContainerForAddons(); + + $this->setupLogChannel(LogChannel::AUTH_JABBERED); + + $this->setupLegacyServiceLocator(); + + $this->registerErrorHandler(); + + $this->registerEventDispatcher(); + + $this->load( + $serverParams, + $this->container->create(DbaDefinition::class), + $this->container->create(ViewDefinition::class), + $this->container->create(Mode::class), + $this->container->create(IManageConfigValues::class), + $this->container->create(Profiler::class), + $this->container->create(EventDispatcherInterface::class), + $this->container->create(AppHelper::class), + $this->container->create(AddonHelper::class), + ); + + /** @var BasePath */ + $basePath = $this->container->create(BasePath::class); + + // Check the database structure and possibly fixes it + Update::check($basePath->getPath(), true); + + $appMode = $this->container->create(Mode::class); + + if ($appMode->isNormal()) { + /** @var ExAuth $oAuth */ + $oAuth = $this->container->create(ExAuth::class); + $oAuth->readStdin(); + } } - /** - * Set the profile owner ID - * - * @param int $owner_id - * @return void - */ - public function setProfileOwner(int $owner_id) + private function setupContainerForAddons(): void { - $this->profile_owner = $owner_id; + /** @var ICanLoadAddons $addonLoader */ + $addonLoader = $this->container->create(ICanLoadAddons::class); + + foreach ($addonLoader->getActiveAddonConfig('dependencies') as $name => $rule) { + $this->container->addRule($name, $rule); + } } - /** - * Get the profile owner ID - * - * @return int - */ - public function getProfileOwner(): int + private function determineLogChannel(array $argv): string { - return $this->profile_owner; + $command = strtolower($argv[1] ?? ''); + + if ($command === 'daemon' || $command === 'jetstream') { + return LogChannel::DAEMON; + } + + if ($command === 'worker') { + return LogChannel::WORKER; + } + + // @TODO Add support for jetstream + + return LogChannel::CONSOLE; } - /** - * Set the contact ID - * - * @param int $contact_id - * @return void - */ - public function setContactId(int $contact_id) + private function setupLogChannel(string $logChannel): void { - $this->contact_id = $contact_id; + /** @var LoggerManager */ + $loggerManager = $this->container->create(LoggerManager::class); + $loggerManager->changeLogChannel($logChannel); } - /** - * Get the contact ID - * - * @return int - */ - public function getContactId(): int + private function setupLegacyServiceLocator(): void { - return $this->contact_id; + if ($this->container instanceof DiceContainer) { + DI::init($this->container->getDice()); + } } - /** - * Set the timezone - * - * @param string $timezone A valid time zone identifier, see https://www.php.net/manual/en/timezones.php - * @return void - */ - public function setTimeZone(string $timezone) + private function registerErrorHandler(): void { - $this->timezone = (new \DateTimeZone($timezone))->getName(); - DateTimeFormat::setLocalTimeZone($this->timezone); + ErrorHandler::register($this->container->create(LoggerInterface::class)); } - /** - * Get the timezone - * - * @return int - */ - public function getTimeZone(): string + private function registerEventDispatcher(): void { - return $this->timezone; + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = $this->container->create(EventDispatcherInterface::class); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } } - /** - * Set workerqueue information - * - * @param array $queue - * @return void - */ - public function setQueue(array $queue) + private function registerTemplateEngine(): void { - $this->queue = $queue; - } - - /** - * Fetch workerqueue information - * - * @return array Worker queue - */ - public function getQueue(): array - { - return $this->queue ?? []; - } - - /** - * Fetch a specific workerqueue field - * - * @param string $index Work queue record to fetch - * @return mixed Work queue item or NULL if not found - */ - public function getQueueValue(string $index) - { - return $this->queue[$index] ?? null; - } - - public function setThemeInfoValue(string $index, $value) - { - $this->theme_info[$index] = $value; - } - - public function getThemeInfo() - { - return $this->theme_info; - } - - public function getThemeInfoValue(string $index, $default = null) - { - return $this->theme_info[$index] ?? $default; - } - - /** - * Returns the current config cache of this node - * - * @return Cache - */ - public function getConfigCache() - { - return $this->config->getCache(); - } - - /** - * The basepath of this app - * - * @return string Base path from configuration - */ - public function getBasePath(): string - { - return $this->config->get('system', 'basepath'); - } - - /** - * @param Database $database The Friendica Database - * @param IManageConfigValues $config The Configuration - * @param App\Mode $mode The mode of this Friendica app - * @param BaseURL $baseURL The full base URL of this Friendica app - * @param LoggerInterface $logger The current app logger - * @param Profiler $profiler The profiler of this application - * @param L10n $l10n The translator instance - * @param App\Arguments $args The Friendica Arguments of the call - * @param IManagePersonalConfigValues $pConfig Personal configuration - * @param IHandleUserSessions $session The (User)Session handler - * @param DbaDefinition $dbaDefinition - * @param ViewDefinition $viewDefinition - */ - public function __construct(Database $database, IManageConfigValues $config, App\Mode $mode, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args, IManagePersonalConfigValues $pConfig, IHandleUserSessions $session, DbaDefinition $dbaDefinition, ViewDefinition $viewDefinition) - { - $this->database = $database; - $this->config = $config; - $this->mode = $mode; - $this->baseURL = $baseURL; - $this->profiler = $profiler; - $this->logger = $logger; - $this->l10n = $l10n; - $this->args = $args; - $this->pConfig = $pConfig; - $this->session = $session; - - $this->load($dbaDefinition, $viewDefinition); + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); } /** * Load the whole app instance */ - protected function load(DbaDefinition $dbaDefinition, ViewDefinition $viewDefinition) - { - if ($this->config->get('system', 'ini_max_execution_time') !== false) { - set_time_limit((int)$this->config->get('system', 'ini_max_execution_time')); + private function load( + array $serverParams, + DbaDefinition $dbaDefinition, + ViewDefinition $viewDefinition, + Mode $mode, + IManageConfigValues $config, + Profiler $profiler, + EventDispatcherInterface $eventDispatcher, + AppHelper $appHelper, + AddonHelper $addonHelper + ): void { + if ($config->get('system', 'ini_max_execution_time') !== false) { + set_time_limit((int) $config->get('system', 'ini_max_execution_time')); } - if ($this->config->get('system', 'ini_pcre_backtrack_limit') !== false) { - ini_set('pcre.backtrack_limit', (int)$this->config->get('system', 'ini_pcre_backtrack_limit')); + if ($config->get('system', 'ini_pcre_backtrack_limit') !== false) { + ini_set('pcre.backtrack_limit', (int) $config->get('system', 'ini_pcre_backtrack_limit')); } // Normally this constant is defined - but not if "pcntl" isn't installed @@ -351,27 +367,20 @@ class App // Ensure that all "strtotime" operations do run timezone independent date_default_timezone_set('UTC'); - set_include_path( - get_include_path() . PATH_SEPARATOR - . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR - . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR - . $this->getBasePath()); + $profiler->reset(); - $this->profiler->reset(); - - if ($this->mode->has(App\Mode::DBAVAILABLE)) { + if ($mode->has(Mode::DBAVAILABLE)) { Core\Hook::loadHooks(); - $loader = (new Config())->createConfigFileManager($this->getBasePath(), $_SERVER); - Core\Hook::callAll('load_config', $loader); + $loader = (new Config())->createConfigFileManager($appHelper->getBasePath(), $addonHelper->getAddonPath(), $serverParams); + + $eventDispatcher->dispatch(new ConfigLoadedEvent(ConfigLoadedEvent::CONFIG_LOADED, $loader)); // Hooks are now working, reload the whole definitions with hook enabled $dbaDefinition->load(true); $viewDefinition->load(true); } - $this->loadDefaultTimezone(); - // Register template engines - Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + $this->loadDefaultTimezone($config, $appHelper); } /** @@ -381,166 +390,16 @@ class App * * @global string $default_timezone */ - private function loadDefaultTimezone() + private function loadDefaultTimezone(IManageConfigValues $config, AppHelper $appHelper) { - if ($this->config->get('system', 'default_timezone')) { - $timezone = $this->config->get('system', 'default_timezone', 'UTC'); + if ($config->get('system', 'default_timezone')) { + $timezone = $config->get('system', 'default_timezone', 'UTC'); } else { global $default_timezone; $timezone = $default_timezone ?? '' ?: 'UTC'; } - $this->setTimeZone($timezone); - } - - /** - * Returns the current theme name. May be overridden by the mobile theme name. - * - * @return string Current theme name or empty string in installation phase - * @throws Exception - */ - public function getCurrentTheme(): string - { - if ($this->mode->isInstall()) { - return ''; - } - - // Specific mobile theme override - if (($this->mode->isMobile() || $this->mode->isTablet()) && $this->session->get('show-mobile', true)) { - $user_mobile_theme = $this->getCurrentMobileTheme(); - - // --- means same mobile theme as desktop - if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') { - return $user_mobile_theme; - } - } - - if (!$this->currentTheme) { - $this->computeCurrentTheme(); - } - - return $this->currentTheme; - } - - /** - * Returns the current mobile theme name. - * - * @return string Mobile theme name or empty string if installer - * @throws Exception - */ - public function getCurrentMobileTheme(): string - { - if ($this->mode->isInstall()) { - return ''; - } - - if (is_null($this->currentMobileTheme)) { - $this->computeCurrentMobileTheme(); - } - - return $this->currentMobileTheme; - } - - /** - * Setter for current theme name - * - * @param string $theme Name of current theme - */ - public function setCurrentTheme(string $theme) - { - $this->currentTheme = $theme; - } - - /** - * Setter for current mobile theme name - * - * @param string $theme Name of current mobile theme - */ - public function setCurrentMobileTheme(string $theme) - { - $this->currentMobileTheme = $theme; - } - - /** - * Computes the current theme name based on the node settings, the page owner settings and the user settings - * - * @throws Exception - */ - private function computeCurrentTheme() - { - $system_theme = $this->config->get('system', 'theme'); - if (!$system_theme) { - throw new Exception($this->l10n->t('No system theme config value set.')); - } - - // Sane default - $this->setCurrentTheme($system_theme); - - $page_theme = null; - // Find the theme that belongs to the user whose stuff we are looking at - if (!empty($this->profile_owner) && ($this->profile_owner != $this->session->getLocalUserId())) { - // Allow folks to override user themes and always use their own on their own site. - // This works only if the user is on the same server - $user = $this->database->selectFirst('user', ['theme'], ['uid' => $this->profile_owner]); - if ($this->database->isResult($user) && !$this->session->getLocalUserId()) { - $page_theme = $user['theme']; - } - } - - $theme_name = $page_theme ?: $this->session->get('theme', $system_theme); - - $theme_name = Strings::sanitizeFilePathItem($theme_name); - if ($theme_name - && in_array($theme_name, Theme::getAllowedList()) - && (file_exists('view/theme/' . $theme_name . '/style.css') - || file_exists('view/theme/' . $theme_name . '/style.php')) - ) { - $this->setCurrentTheme($theme_name); - } - } - - /** - * Computes the current mobile theme name based on the node settings, the page owner settings and the user settings - */ - private function computeCurrentMobileTheme() - { - $system_mobile_theme = $this->config->get('system', 'mobile-theme', ''); - - // Sane default - $this->setCurrentMobileTheme($system_mobile_theme); - - $page_mobile_theme = null; - // Find the theme that belongs to the user whose stuff we are looking at - if (!empty($this->profile_owner) && ($this->profile_owner != $this->session->getLocalUserId())) { - // Allow folks to override user themes and always use their own on their own site. - // This works only if the user is on the same server - if (!$this->session->getLocalUserId()) { - $page_mobile_theme = $this->pConfig->get($this->profile_owner, 'system', 'mobile-theme'); - } - } - - $mobile_theme_name = $page_mobile_theme ?: $this->session->get('mobile-theme', $system_mobile_theme); - - $mobile_theme_name = Strings::sanitizeFilePathItem($mobile_theme_name); - if ($mobile_theme_name == '---' - || - in_array($mobile_theme_name, Theme::getAllowedList()) - && (file_exists('view/theme/' . $mobile_theme_name . '/style.css') - || file_exists('view/theme/' . $mobile_theme_name . '/style.php')) - ) { - $this->setCurrentMobileTheme($mobile_theme_name); - } - } - - /** - * Provide a sane default if nothing is chosen or the specified theme does not exist. - * - * @return string Current theme's stylesheet path - * @throws Exception - */ - public function getCurrentThemeStylesheetPath(): string - { - return Core\Theme::getStylesheetPath($this->getCurrentTheme()); + $appHelper->setTimeZone($timezone); } /** @@ -551,23 +410,34 @@ class App * * This probably should change to limit the size of this monster method. * - * @param App\Router $router * @param IManagePersonalConfigValues $pconfig - * @param Authentication $auth The Authentication backend of the node - * @param App\Page $page The Friendica page printing container + * @param Page $page The Friendica page printing container * @param ModuleHTTPException $httpException The possible HTTP Exception container - * @param HTTPInputData $httpInput A library for processing PHP input streams * @param float $start_time The start time of the overall script execution - * @param array $server The $_SERVER array * * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public function runFrontend(App\Router $router, IManagePersonalConfigValues $pconfig, Authentication $auth, App\Page $page, Nav $nav, ModuleHTTPException $httpException, HTTPInputData $httpInput, float $start_time, array $server) - { - $requeststring = ($_SERVER['REQUEST_METHOD'] ?? '') . ' ' . ($_SERVER['REQUEST_URI'] ?? '') . ' ' . ($_SERVER['SERVER_PROTOCOL'] ?? ''); - $this->logger->debug('Request received', ['address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); + private function runFrontend( + EventDispatcherInterface $eventDispatcher, + IManagePersonalConfigValues $pconfig, + Page $page, + Nav $nav, + AddonHelper $addonHelper, + ModuleHTTPException $httpException, + float $start_time, + ServerRequestInterface $request + ) { + $this->mode->setExecutor(Mode::INDEX); + + $httpInput = new HTTPInputData($request->getServerParams()); + $serverVars = $request->getServerParams(); + $queryVars = $request->getQueryParams(); + + $requeststring = ($serverVars['REQUEST_METHOD'] ?? '') . ' ' . ($serverVars['REQUEST_URI'] ?? '') . ' ' . ($serverVars['SERVER_PROTOCOL'] ?? ''); + $this->logger->debug('Request received', ['address' => $serverVars['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $serverVars['HTTP_REFERER'] ?? '', 'user-agent' => $serverVars['HTTP_USER_AGENT'] ?? '']); $request_start = microtime(true); + $request = $_REQUEST; $this->profiler->set($start_time, 'start'); $this->profiler->set(microtime(true), 'classinit'); @@ -577,64 +447,58 @@ class App try { // Missing DB connection: ERROR - if ($this->mode->has(App\Mode::LOCALCONFIGPRESENT) && !$this->mode->has(App\Mode::DBAVAILABLE)) { + if ($this->mode->has(Mode::LOCALCONFIGPRESENT) && !$this->mode->has(Mode::DBAVAILABLE)) { throw new HTTPException\InternalServerErrorException($this->l10n->t('Apologies but the website is unavailable at the moment.')); } if (!$this->mode->isInstall()) { // Force SSL redirection if ($this->config->get('system', 'force_ssl') && - (empty($server['HTTPS']) || $server['HTTPS'] === 'off') && - (empty($server['HTTP_X_FORWARDED_PROTO']) || $server['HTTP_X_FORWARDED_PROTO'] === 'http') && - !empty($server['REQUEST_METHOD']) && - $server['REQUEST_METHOD'] === 'GET') { + (empty($serverVars['HTTPS']) || $serverVars['HTTPS'] === 'off') && + (empty($serverVars['HTTP_X_FORWARDED_PROTO']) || $serverVars['HTTP_X_FORWARDED_PROTO'] === 'http') && + !empty($serverVars['REQUEST_METHOD']) && + $serverVars['REQUEST_METHOD'] === 'GET') { System::externalRedirect($this->baseURL . '/' . $this->args->getQueryString()); } - Core\Hook::callAll('init_1'); + + $eventDispatcher->dispatch(new Event(Event::INIT)); } + DID::routeRequest($this->args->getCommand(), $serverVars); + if ($this->mode->isNormal() && !$this->mode->isBackend()) { - $requester = HTTPSignature::getSigner('', $_SERVER); + $requester = HTTPSignature::getSigner('', $serverVars); if (!empty($requester)) { - Profile::addVisitorCookieForHandle($requester); + OpenWebAuth::addVisitorCookieForHandle($requester); } } // ZRL - if (!empty($_GET['zrl']) && $this->mode->isNormal() && !$this->mode->isBackend() && !$this->session->getLocalUserId()) { + if (!empty($queryVars['zrl']) && $this->mode->isNormal() && !$this->mode->isBackend() && !$this->session->getLocalUserId()) { // Only continue when the given profile link seems valid. // Valid profile links contain a path with "/profile/" and no query parameters - if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == '') && - strpos(parse_url($_GET['zrl'], PHP_URL_PATH) ?? '', '/profile/') !== false) { - if ($this->session->get('visitor_home') != $_GET['zrl']) { - $this->session->set('my_url', $_GET['zrl']); - $this->session->set('authenticated', 0); - - $remote_contact = Contact::getByURL($_GET['zrl'], false, ['subscribe']); - if (!empty($remote_contact['subscribe'])) { - $_SESSION['remote_comment'] = $remote_contact['subscribe']; - } - } - - Model\Profile::zrlInit($this); + if ((parse_url($queryVars['zrl'], PHP_URL_QUERY) == '') && + strpos(parse_url($queryVars['zrl'], PHP_URL_PATH) ?? '', '/profile/') !== false) { + $this->auth->setUnauthenticatedVisitor($queryVars['zrl']); + OpenWebAuth::zrlInit(); } else { // Someone came with an invalid parameter, maybe as a DDoS attempt // We simply stop processing here - $this->logger->debug('Invalid ZRL parameter.', ['zrl' => $_GET['zrl']]); + $this->logger->debug('Invalid ZRL parameter.', ['zrl' => $queryVars['zrl']]); throw new HTTPException\ForbiddenException(); } } - if (!empty($_GET['owt']) && $this->mode->isNormal()) { - $token = $_GET['owt']; - Model\Profile::openWebAuthInit($token); + if (!empty($queryVars['owt']) && $this->mode->isNormal()) { + $token = $queryVars['owt']; + OpenWebAuth::init($token); } if (!$this->mode->isBackend()) { - $auth->withSession($this); + $this->auth->withSession(); } - if (empty($_SESSION['authenticated'])) { + if ($this->session->isUnauthenticated()) { header('X-Account-Management-Status: none'); } @@ -648,10 +512,15 @@ class App // but we need "view" module for stylesheet if ($this->mode->isInstall() && $moduleName !== 'install') { $this->baseURL->redirect('install'); - } else { - Core\Update::check($this->getBasePath(), false); - Core\Addon::loadAddons(); - Core\Hook::loadHooks(); + } + + Core\Update::check($this->appHelper->getBasePath(), false); + $addonHelper->loadAddons(); + Core\Hook::loadHooks(); + + // Compatibility with Hubzilla + if ($moduleName == 'rpost') { + $this->baseURL->redirect('compose'); } // Compatibility with the Android Diaspora client @@ -691,12 +560,12 @@ class App $page['page_title'] = $moduleName; // The "view" module is required to show the theme CSS - if (!$this->mode->isInstall() && !$this->mode->has(App\Mode::MAINTENANCEDISABLED) && $moduleName !== 'view') { - $module = $router->getModule(Maintenance::class); + if (!$this->mode->isInstall() && !$this->mode->has(Mode::MAINTENANCEDISABLED) && $moduleName !== 'view') { + $module = $this->createModuleInstance(Maintenance::class); } else { // determine the module class and save it to the module instance // @todo there's an implicit dependency due SESSION::start(), so it has to be called here (yet) - $module = $router->getModule(); + $module = $this->createModuleInstance(null); } // Display can change depending on the requested language, so it shouldn't be cached whole @@ -704,41 +573,85 @@ class App // Processes data from GET requests $httpinput = $httpInput->process(); - $input = array_merge($httpinput['variables'], $httpinput['files'], $request ?? $_REQUEST); + + if (!is_array($httpinput['variables'])) { + $httpinput['variables'] = []; + } + if (!is_array($httpinput['files'])) { + $httpinput['files'] = []; + } + + $input = array_merge($httpinput['variables'], $httpinput['files'], $request); // Let the module run its internal process (init, get, post, ...) $timestamp = microtime(true); - $response = $module->run($httpException, $input); + $response = $module->run($httpException, $input); $this->profiler->set(microtime(true) - $timestamp, 'content'); // Wrapping HTML responses in the theme template if ($response->getHeaderLine(ICanCreateResponses::X_HEADER) === ICanCreateResponses::TYPE_HTML) { - $response = $page->run($this, $this->baseURL, $this->args, $this->mode, $response, $this->l10n, $this->profiler, $this->config, $pconfig, $nav, $this->session->getLocalUserId()); + $response = $page->run($this->appHelper, $this->session, $this->baseURL, $this->args, $this->mode, $response, $this->l10n, $this->profiler, $this->config, $pconfig, $nav, $this->session->getLocalUserId()); } - $this->logger->debug('Request processed sucessfully', ['response' => $response->getStatusCode(), 'address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]); + $this->logger->debug('Request processed sucessfully', ['response' => $response->getStatusCode(), 'address' => $serverVars['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $serverVars['HTTP_REFERER'] ?? '', 'user-agent' => $serverVars['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]); + $this->logSlowCalls(microtime(true) - $request_start, $response->getStatusCode(), $requeststring, $serverVars['HTTP_USER_AGENT'] ?? ''); System::echoResponse($response); } catch (HTTPException $e) { - $this->logger->debug('Request processed with exception', ['response' => $e->getCode(), 'address' => $_SERVER['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]); + $this->logger->debug('Request processed with exception', ['response' => $e->getCode(), 'address' => $serverVars['REMOTE_ADDR'] ?? '', 'request' => $requeststring, 'referer' => $serverVars['HTTP_REFERER'] ?? '', 'user-agent' => $serverVars['HTTP_USER_AGENT'] ?? '', 'duration' => number_format(microtime(true) - $request_start, 3)]); + $this->logSlowCalls(microtime(true) - $request_start, $e->getCode(), $requeststring, $serverVars['HTTP_USER_AGENT'] ?? ''); $httpException->rawContent($e); } $page->logRuntime($this->config, 'runFrontend'); } - /** - * Automatically redirects to relative or absolute URL - * Should only be used if it isn't clear if the URL is either internal or external - * - * @param string $toUrl The target URL - * - * @throws HTTPException\InternalServerErrorException - */ - public function redirect(string $toUrl) + private function createModuleInstance(?string $moduleClass = null): ICanHandleRequests { - if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) { - Core\System::externalRedirect($toUrl); - } else { - $this->baseURL->redirect($toUrl); + /** @var Router $router */ + $router = $this->container->create(Router::class); + + $moduleClass = $moduleClass ?? $router->getModuleClass(); + $parameters = $router->getParameters(); + + $dice_profiler_threshold = $this->config->get('system', 'dice_profiler_threshold', 0); + + $stamp = microtime(true); + + /** @var ICanHandleRequests $module */ + $module = $this->container->create($moduleClass, $parameters); + + if ($dice_profiler_threshold > 0) { + $dur = floatval(microtime(true) - $stamp); + if ($dur >= $dice_profiler_threshold) { + $this->logger->notice('Dice module creation lasts too long.', ['duration' => round($dur, 3), 'module' => $moduleClass, 'parameters' => $parameters]); + } } + + return $module; + } + + /** + * Log slow page executions + * + * @param float $duration + * @param integer $code + * @param string $request + * @param string $agent + * @return void + */ + private function logSlowCalls(float $duration, int $code, string $request, string $agent) + { + $logfile = $this->config->get('system', 'page_execution_logfile'); + $loglimit = $this->config->get('system', 'page_execution_log_limit'); + if (empty($logfile) || empty($loglimit) || ($duration < $loglimit)) { + return; + } + + @file_put_contents( + $logfile, + DateTimeFormat::utcNow() . "\t" . round($duration, 3) . "\t" . + $this->requestId . "\t" . $code . "\t" . + $request . "\t" . $agent . "\n", + FILE_APPEND + ); } } diff --git a/src/App/Arguments.php b/src/App/Arguments.php index 6f527d2f4d..95fccbb256 100644 --- a/src/App/Arguments.php +++ b/src/App/Arguments.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\App; diff --git a/src/App/BaseURL.php b/src/App/BaseURL.php index cc20343d1c..7aaafdc8bf 100644 --- a/src/App/BaseURL.php +++ b/src/App/BaseURL.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\App; @@ -31,8 +17,7 @@ use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; /** - * A class which checks and contains the basic - * environment for the BaseURL (url, urlpath, ssl_policy, hostname, scheme) + * A class which checks and contains the basic environment for the BaseURL (url) */ class BaseURL extends Uri implements UriInterface { @@ -57,8 +42,7 @@ class BaseURL extends Uri implements UriInterface /* Relative script path to the web server root * Not all of those $_SERVER properties can be present, so we do by inverse priority order */ - $relativeScriptPath = - ($server['REDIRECT_URL'] ?? '') ?: + $relativeScriptPath = ($server['REDIRECT_URL'] ?? '') ?: ($server['REDIRECT_URI'] ?? '') ?: ($server['REDIRECT_SCRIPT_URL'] ?? '') ?: ($server['SCRIPT_URL'] ?? '') ?: @@ -117,6 +101,8 @@ class BaseURL extends Uri implements UriInterface * @throws HTTPException\TemporaryRedirectException * * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node + * + * @return never */ public function redirect(string $toUrl = '', bool $ssl = false) { diff --git a/src/App/Mode.php b/src/App/Mode.php index 7cd16b4bb9..ebf9f46163 100644 --- a/src/App/Mode.php +++ b/src/App/Mode.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\App; @@ -67,11 +53,8 @@ class Mode 'objects', 'outbox', 'poco', - 'pubsub', - 'pubsubhubbub', 'receive', 'rsd_xml', - 'salmon', 'statistics_json', 'xrd', ]; @@ -227,7 +210,7 @@ class Mode public function isInstall(): bool { return !$this->has(Mode::LOCALCONFIGPRESENT) || - !$this->has(MODE::DBAVAILABLE); + !$this->has(Mode::DBAVAILABLE); } /** diff --git a/src/App/Page.php b/src/App/Page.php index 71f75df157..a7337485ec 100644 --- a/src/App/Page.php +++ b/src/App/Page.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\App; @@ -25,22 +11,24 @@ use ArrayAccess; use DOMDocument; use DOMXPath; use Friendica\App; +use Friendica\AppHelper; use Friendica\Content\Nav; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Hook; use Friendica\Core\L10n; -use Friendica\Core\Logger; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Renderer; +use Friendica\Core\Session\Model\UserSession; use Friendica\Core\System; use Friendica\Core\Theme; -use Friendica\Module\Response; +use Friendica\DI; +use Friendica\Event\HtmlFilterEvent; use Friendica\Network\HTTPException; use Friendica\Util\Images; use Friendica\Util\Network; use Friendica\Util\Profiler; use Friendica\Util\Strings; use GuzzleHttp\Psr7\Utils; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; /** @@ -83,6 +71,8 @@ class Page implements ArrayAccess */ private $basePath; + private EventDispatcherInterface $eventDispatcher; + private $timestamp = 0; private $method = ''; private $module = ''; @@ -91,10 +81,11 @@ class Page implements ArrayAccess /** * @param string $basepath The Page basepath */ - public function __construct(string $basepath) + public function __construct(string $basepath, EventDispatcherInterface $eventDispatcher) { - $this->timestamp = microtime(true); - $this->basePath = $basepath; + $this->timestamp = microtime(true); + $this->basePath = $basepath; + $this->eventDispatcher = $eventDispatcher; } public function setLogging(string $method, string $module, string $command) @@ -115,7 +106,7 @@ class Page implements ArrayAccess $load = number_format(System::currentLoad(), 2); $runtime = number_format(microtime(true) - $this->timestamp, 3); if ($runtime > $config->get('system', 'runtime_loglimit')) { - Logger::debug('Runtime', ['method' => $this->method, 'module' => $this->module, 'runtime' => $runtime, 'load' => $load, 'origin' => $origin, 'signature' => $signature, 'request' => $_SERVER['REQUEST_URI'] ?? '']); + DI::logger()->debug('Runtime', ['method' => $this->method, 'module' => $this->module, 'runtime' => $runtime, 'load' => $load, 'origin' => $origin, 'signature' => $signature, 'request' => $_SERVER['REQUEST_URI'] ?? '']); } } @@ -189,40 +180,34 @@ class Page implements ArrayAccess * - Infinite scroll data * - head.tpl template * - * @param App $app The Friendica App instance - * @param Arguments $args The Friendica App Arguments - * @param L10n $l10n The l10n language instance - * @param IManageConfigValues $config The Friendica configuration - * @param IManagePersonalConfigValues $pConfig The Friendica personal configuration (for user) - * @param int $localUID The local user id + * @param Arguments $args The Friendica App Arguments + * @param L10n $l10n The l10n language instance + * @param IManageConfigValues $config The Friendica configuration + * @param IManagePersonalConfigValues $pConfig The Friendica personal configuration (for user) + * @param int $localUID The local user id * * @throws HTTPException\InternalServerErrorException */ - private function initHead(App $app, Arguments $args, L10n $l10n, IManageConfigValues $config, IManagePersonalConfigValues $pConfig, int $localUID) - { - $interval = ($localUID ? $pConfig->get($localUID, 'system', 'update_interval') : 40000); - - // If the update is 'deactivated' set it to the highest integer number (~24 days) - if ($interval < 0) { - $interval = 2147483647; - } - - if ($interval < 10000) { - $interval = 40000; - } - + private function initHead( + AppHelper $appHelper, + Arguments $args, + L10n $l10n, + IManageConfigValues $config, + IManagePersonalConfigValues $pConfig, + int $localUID + ) { // Default title: current module called if (empty($this->page['title']) && $args->getModuleName()) { - $this->page['title'] = ucfirst($args->getModuleName()); + $this->page['title'] = $l10n->t(ucfirst($args->getModuleName())); } - // Prepend the sitename to the page title - $this->page['title'] = $config->get('config', 'sitename', '') . (!empty($this->page['title']) ? ' | ' . $this->page['title'] : ''); + // Append the sitename to the page title + $this->page['title'] = (!empty($this->page['title']) ? $this->page['title'] . ' | ' : '') . $config->get('config', 'sitename', ''); if (!empty(Renderer::$theme['stylesheet'])) { $stylesheet = Renderer::$theme['stylesheet']; } else { - $stylesheet = $app->getCurrentThemeStylesheetPath(); + $stylesheet = $appHelper->getCurrentThemeStylesheetPath(); } $this->registerStylesheet($stylesheet); @@ -237,7 +222,9 @@ class Page implements ArrayAccess $touch_icon = 'images/friendica-192.png'; } - Hook::callAll('head', $this->page['htmlhead']); + $this->page['htmlhead'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::HEAD, $this->page['htmlhead']) + )->getHtml(); $tpl = Renderer::getMarkupTemplate('head.tpl'); /* put the head template at the beginning of page['htmlhead'] @@ -274,18 +261,25 @@ class Page implements ArrayAccess 'dictMaxFilesExceeded' => $l10n->t("You can't upload any more files."), ], - '$local_user' => $localUID, - '$generator' => 'Friendica' . ' ' . App::VERSION, - '$update_interval' => $interval, - '$shortcut_icon' => $shortcut_icon, - '$touch_icon' => $touch_icon, - '$block_public' => intval($config->get('system', 'block_public')), - '$stylesheets' => $this->stylesheets, + '$local_user' => $localUID, + '$generator' => 'Friendica' . ' ' . App::VERSION, + '$update_content' => (int)$pConfig->get($localUID, 'system', 'update_content'), + '$shortcut_icon' => $shortcut_icon, + '$touch_icon' => $touch_icon, + '$block_public' => intval($config->get('system', 'block_public')), + '$stylesheets' => $this->stylesheets, // Dropzone '$max_imagesize' => round(Images::getMaxUploadBytes() / 1000000, 0), ]) . $this->page['htmlhead']; + + if ($pConfig->get($localUID, 'accessibility', 'hide_empty_descriptions')) { + $this->page['htmlhead'] .= "\n"; + } + if ($pConfig->get($localUID, 'accessibility', 'hide_custom_emojis')) { + $this->page['htmlhead'] .= "\n"; + } } /** @@ -319,19 +313,18 @@ class Page implements ArrayAccess * - Registered footer scripts (through App->registerFooterScript()) * - footer.tpl template * - * @param App $app The Friendica App instance * @param Mode $mode The Friendica runtime mode * @param L10n $l10n The l10n instance * * @throws HTTPException\InternalServerErrorException */ - private function initFooter(App $app, Mode $mode, L10n $l10n) + private function initFooter(UserSession $session, Mode $mode, L10n $l10n) { // If you're just visiting, let javascript take you home if (!empty($_SESSION['visitor_home'])) { $homebase = $_SESSION['visitor_home']; - } elseif (!empty($app->getLoggedInUserNickname())) { - $homebase = 'profile/' . $app->getLoggedInUserNickname(); + } elseif (!empty($session->getLocalUserNickname())) { + $homebase = 'profile/' . $session->getLocalUserNickname(); } if (isset($homebase)) { @@ -353,11 +346,14 @@ class Page implements ArrayAccess ]); } - Hook::callAll('footer', $this->page['footer']); + $this->page['footer'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::FOOTER, $this->page['footer']) + )->getHtml(); $tpl = Renderer::getMarkupTemplate('footer.tpl'); $this->page['footer'] = Renderer::replaceMacros($tpl, [ '$footerScripts' => array_unique($this->footerScripts), + '$close' => $l10n->t('Close'), ]) . $this->page['footer']; } @@ -377,7 +373,9 @@ class Page implements ArrayAccess { // initialise content region if ($mode->isNormal()) { - Hook::callAll('page_content_top', $this->page['content']); + $this->page['content'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::PAGE_CONTENT_TOP, $this->page['content']) + )->getHtml(); } $this->page['content'] .= (string)$response->getBody(); @@ -405,23 +403,34 @@ class Page implements ArrayAccess /** * Executes the creation of the current page and prints it to the screen * - * @param App $app The Friendica App - * @param BaseURL $baseURL The Friendica Base URL - * @param Arguments $args The Friendica App arguments - * @param Mode $mode The current node mode - * @param ResponseInterface $response The Response of the module class, including type, content & headers - * @param L10n $l10n The l10n language class + * @param BaseURL $baseURL The Friendica Base URL + * @param Arguments $args The Friendica App arguments + * @param Mode $mode The current node mode + * @param ResponseInterface $response The Response of the module class, including type, content & headers + * @param L10n $l10n The l10n language class * @param Profiler $profiler - * @param IManageConfigValues $config The Configuration of this node - * @param IManagePersonalConfigValues $pconfig The personal/user configuration + * @param IManageConfigValues $config The Configuration of this node + * @param IManagePersonalConfigValues $pconfig The personal/user configuration * @param Nav $nav * @param int $localUID * @throws HTTPException\MethodNotAllowedException * @throws HTTPException\InternalServerErrorException * @throws HTTPException\ServiceUnavailableException */ - public function run(App $app, BaseURL $baseURL, Arguments $args, Mode $mode, ResponseInterface $response, L10n $l10n, Profiler $profiler, IManageConfigValues $config, IManagePersonalConfigValues $pconfig, Nav $nav, int $localUID) - { + public function run( + AppHelper $appHelper, + UserSession $session, + BaseURL $baseURL, + Arguments $args, + Mode $mode, + ResponseInterface $response, + L10n $l10n, + Profiler $profiler, + IManageConfigValues $config, + IManagePersonalConfigValues $pconfig, + Nav $nav, + int $localUID + ) { $moduleName = $args->getModuleName(); $this->command = $moduleName; @@ -436,7 +445,7 @@ class Page implements ArrayAccess $this->initContent($response, $mode); // Load current theme info after module has been initialized as theme could have been set in module - $currentTheme = $app->getCurrentTheme(); + $currentTheme = $appHelper->getCurrentTheme(); $theme_info_file = 'view/theme/' . $currentTheme . '/theme.php'; if (file_exists($theme_info_file)) { require_once $theme_info_file; @@ -444,7 +453,7 @@ class Page implements ArrayAccess if (function_exists(str_replace('-', '_', $currentTheme) . '_init')) { $func = str_replace('-', '_', $currentTheme) . '_init'; - $func($app); + $func($appHelper); } /* Create the page head after setting the language @@ -454,23 +463,25 @@ class Page implements ArrayAccess * all the module functions have executed so that all * theme choices made by the modules can take effect. */ - $this->initHead($app, $args, $l10n, $config, $pconfig, $localUID); + $this->initHead($appHelper, $args, $l10n, $config, $pconfig, $localUID); /* Build the page ending -- this is stuff that goes right before * the closing tag */ - $this->initFooter($app, $mode, $l10n); + $this->initFooter($session, $mode, $l10n); $profiler->set(microtime(true) - $timestamp, 'aftermath'); if (!$mode->isAjax()) { - Hook::callAll('page_end', $this->page['content']); + $this->page['content'] = $this->eventDispatcher->dispatch( + new HtmlFilterEvent(HtmlFilterEvent::PAGE_END, $this->page['content']) + )->getHtml(); } // Add the navigation (menu) template if ($moduleName != 'install' && $moduleName != 'maintenance') { $this->page['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('nav_head.tpl'), []); - $this->page['nav'] = $nav->getHtml(); + $this->page['nav'] = $nav->getHtml(); } // Build the page - now that we have all the components @@ -503,7 +514,7 @@ class Page implements ArrayAccess } } - $page = $this->page; + $page = $this->page; // add and escape some common but crucial content for direct "echo" in HTML (security) $page['title'] = htmlspecialchars($page['title'] ?? ''); @@ -540,7 +551,7 @@ class Page implements ArrayAccess } // Theme templates expect $a as an App instance - $a = $app; + $a = $appHelper; // Used as is in view/php/default.php $lang = $l10n->getCurrentLang(); diff --git a/src/App/Request.php b/src/App/Request.php index d61303d5d7..e82c48b278 100644 --- a/src/App/Request.php +++ b/src/App/Request.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\App; diff --git a/src/App/Router.php b/src/App/Router.php index 6d90fb0138..439b0a743b 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -1,33 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\App; -use Dice\Dice; use FastRoute\DataGenerator\GroupCountBased; use FastRoute\Dispatcher; use FastRoute\RouteCollector; use FastRoute\RouteParser\Std; -use Friendica\Capabilities\ICanHandleRequests; -use Friendica\Core\Addon; +use Friendica\Core\Addon\AddonHelper; use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Config\Capability\IManageConfigValues; @@ -35,6 +19,7 @@ use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Core\Session\Capability\IHandleUserSessions; +use Friendica\Event\CollectRoutesEvent; use Friendica\LegacyModule; use Friendica\Module\HTTPException\MethodNotAllowed; use Friendica\Module\HTTPException\PageNotFound; @@ -44,6 +29,7 @@ use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\MethodNotAllowedException; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Util\Router\FriendicaGroupCountBased; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; /** @@ -100,15 +86,13 @@ class Router /** @var LoggerInterface */ private $logger; + private EventDispatcherInterface $eventDispatcher; + + private AddonHelper $addonHelper; + /** @var bool */ private $isLocalUser; - /** @var float */ - private $dice_profiler_threshold; - - /** @var Dice */ - private $dice; - /** @var string */ private $baseRoutesFilepath; @@ -127,23 +111,22 @@ class Router * @param IManageConfigValues $config * @param Arguments $args * @param LoggerInterface $logger - * @param Dice $dice * @param IHandleUserSessions $userSession * @param RouteCollector|null $routeCollector */ - public function __construct(array $server, string $baseRoutesFilepath, L10n $l10n, ICanCache $cache, ICanLock $lock, IManageConfigValues $config, Arguments $args, LoggerInterface $logger, Dice $dice, IHandleUserSessions $userSession, RouteCollector $routeCollector = null) + public function __construct(array $server, string $baseRoutesFilepath, L10n $l10n, ICanCache $cache, ICanLock $lock, IManageConfigValues $config, Arguments $args, LoggerInterface $logger, EventDispatcherInterface $eventDispatcher, AddonHelper $addonHelper, IHandleUserSessions $userSession, RouteCollector $routeCollector = null) { - $this->baseRoutesFilepath = $baseRoutesFilepath; - $this->l10n = $l10n; - $this->cache = $cache; - $this->lock = $lock; - $this->args = $args; - $this->config = $config; - $this->dice = $dice; - $this->server = $server; - $this->logger = $logger; - $this->isLocalUser = !empty($userSession->getLocalUserId()); - $this->dice_profiler_threshold = $config->get('system', 'dice_profiler_threshold', 0); + $this->baseRoutesFilepath = $baseRoutesFilepath; + $this->l10n = $l10n; + $this->cache = $cache; + $this->lock = $lock; + $this->args = $args; + $this->config = $config; + $this->server = $server; + $this->logger = $logger; + $this->eventDispatcher = $eventDispatcher; + $this->addonHelper = $addonHelper; + $this->isLocalUser = !empty($userSession->getLocalUserId()); $this->routeCollector = $routeCollector ?? new RouteCollector(new Std(), new GroupCountBased()); @@ -170,10 +153,12 @@ class Router $this->addRoutes($routeCollector, $routes); - $this->routeCollector = $routeCollector; - // Add routes from addons - Hook::callAll('route_collection', $this->routeCollector); + $routeCollector = $this->eventDispatcher->dispatch( + new CollectRoutesEvent(CollectRoutesEvent::COLLECT_ROUTES, $routeCollector), + )->getRouteCollector(); + + $this->routeCollector = $routeCollector; return $this; } @@ -298,14 +283,14 @@ class Router try { // Check if the HTTP method is OPTIONS and return the special Options Module with the possible HTTP methods if ($this->args->getMethod() === static::OPTIONS) { - $this->moduleClass = Options::class; + $this->moduleClass = Options::class; $this->parameters[] = ['AllowedMethods' => $dispatcher->getOptions($cmd)]; } else { $routeInfo = $dispatcher->dispatch($this->args->getMethod(), $cmd); if ($routeInfo[0] === Dispatcher::FOUND) { - $this->moduleClass = $routeInfo[1]; + $this->moduleClass = $routeInfo[1]; $this->parameters[] = $routeInfo[2]; - } else if ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { + } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { throw new HTTPException\MethodNotAllowedException($this->l10n->t('Method not allowed for this module. Allowed method(s): %s', implode(', ', $routeInfo[1]))); } else { throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); @@ -316,7 +301,7 @@ class Router } catch (NotFoundException $e) { $moduleName = $this->args->getModuleName(); // Then we try addon-provided modules that we wrap in the LegacyModule class - if (Addon::isEnabled($moduleName) && file_exists("addon/{$moduleName}/{$moduleName}.php")) { + if ($this->addonHelper->isAddonEnabled($moduleName) && file_exists("addon/{$moduleName}/{$moduleName}.php")) { //Check if module is an app and if public access to apps is allowed or not $privateapps = $this->config->get('config', 'private_addons', false); if (!$this->isLocalUser && Hook::isAddonApp($moduleName) && $privateapps) { @@ -342,23 +327,9 @@ class Router } } - public function getModule(?string $module_class = null): ICanHandleRequests + public function getParameters(): array { - $moduleClass = $module_class ?? $this->getModuleClass(); - - $stamp = microtime(true); - - try { - /** @var ICanHandleRequests $module */ - return $this->dice->create($moduleClass, $this->parameters); - } finally { - if ($this->dice_profiler_threshold > 0) { - $dur = floatval(microtime(true) - $stamp); - if ($dur >= $this->dice_profiler_threshold) { - $this->logger->notice('Dice module creation lasts too long.', ['duration' => round($dur, 3), 'module' => $moduleClass, 'parameters' => $this->parameters]); - } - } - } + return $this->parameters; } /** diff --git a/src/AppHelper.php b/src/AppHelper.php new file mode 100644 index 0000000000..66d3c61d9a --- /dev/null +++ b/src/AppHelper.php @@ -0,0 +1,139 @@ + $queue + */ + public function setQueue(array $queue); + + /** + * Fetch workerqueue information + * + * @return array Worker queue + */ + public function getQueue(); + + /** + * Fetch a specific workerqueue field + * + * @param string $index Work queue record to fetch + * + * @return mixed|null Work queue item or NULL if not found + */ + public function getQueueValue(string $index); + + /** + * Returns the current theme name. May be overridden by the mobile theme name. + * + * @return string Current theme name or empty string in installation phase + * @throws Exception + */ + public function getCurrentTheme(): string; + + /** + * Returns the current mobile theme name. + * + * @return string Mobile theme name or empty string if installer + * @throws Exception + */ + public function getCurrentMobileTheme(): string; + + /** + * Setter for current theme name + * + * @param string $theme Name of current theme + */ + public function setCurrentTheme(string $theme); + + /** + * Setter for current mobile theme name + * + * @param string $theme Name of current mobile theme + */ + public function setCurrentMobileTheme(string $theme); + + public function setThemeInfoValue(string $index, $value); + + public function getThemeInfo(); + + public function getThemeInfoValue(string $index, $default = null); + + /** + * Provide a sane default if nothing is chosen or the specified theme does not exist. + * + * @return string Current theme's stylesheet path + * @throws Exception + */ + public function getCurrentThemeStylesheetPath(): string; + + /** + * Returns the current config cache of this node + * + * @return Cache + */ + public function getConfigCache(); + + /** + * The basepath of this app + * + * @return string Base path from configuration + */ + public function getBasePath(): string; + + public function redirect(string $toUrl); +} diff --git a/src/AppLegacy.php b/src/AppLegacy.php new file mode 100644 index 0000000000..8ec6b0aabc --- /dev/null +++ b/src/AppLegacy.php @@ -0,0 +1,400 @@ + 425, + 'videoheight' => 350, + ]; + + /** + * @var Database The Friendica database connection + */ + private $database; + + /** + * @var IManageConfigValues The config + */ + private $config; + + /** + * @var Mode The Mode of the Application + */ + private $mode; + + /** + * @var BaseURL + */ + private $baseURL; + + /** + * @var L10n The translator + */ + private $l10n; + + /** + * @var IManagePersonalConfigValues + */ + private $pConfig; + + /** + * @var IHandleUserSessions + */ + private $session; + + public function __construct( + Database $database, + IManageConfigValues $config, + Mode $mode, + BaseURL $baseURL, + L10n $l10n, + IManagePersonalConfigValues $pConfig, + IHandleUserSessions $session + ) { + $this->database = $database; + $this->config = $config; + $this->mode = $mode; + $this->l10n = $l10n; + $this->baseURL = $baseURL; + $this->pConfig = $pConfig; + $this->session = $session; + } + + /** + * Set the profile owner ID + */ + public function setProfileOwner(int $owner_id): void + { + $this->profile_owner = $owner_id; + } + + /** + * Get the profile owner ID + */ + public function getProfileOwner(): int + { + return $this->profile_owner; + } + + /** + * Set the timezone + * + * @param string $timezone A valid time zone identifier, see https://www.php.net/manual/en/timezones.php + */ + public function setTimeZone(string $timezone): void + { + $this->timezone = (new DateTimeZone($timezone))->getName(); + + DateTimeFormat::setLocalTimeZone($this->timezone); + } + + /** + * Get the timezone name + */ + public function getTimeZone(): string + { + return $this->timezone; + } + + /** + * Set the contact ID + */ + public function setContactId(int $contact_id): void + { + $this->contact_id = $contact_id; + } + + /** + * Get the contact ID + */ + public function getContactId(): int + { + return $this->contact_id; + } + + /** + * Set workerqueue information + * + * @param array $queue + */ + public function setQueue(array $queue): void + { + $this->queue = $queue; + } + + /** + * Fetch workerqueue information + * + * @return array Worker queue + */ + public function getQueue(): array + { + return $this->queue; + } + + /** + * Fetch a specific workerqueue field + * + * @param string $index Work queue record to fetch + * + * @return mixed|null Work queue item or NULL if not found + */ + public function getQueueValue(string $index) + { + return $this->queue[$index] ?? null; + } + + /** + * Returns the current theme name. May be overridden by the mobile theme name. + * + * @return string Current theme name or empty string in installation phase + * @throws Exception + */ + public function getCurrentTheme(): string + { + if ($this->mode->isInstall()) { + return ''; + } + + // Specific mobile theme override + if (($this->mode->isMobile() || $this->mode->isTablet()) && $this->session->get('show-mobile', true)) { + $user_mobile_theme = $this->getCurrentMobileTheme(); + + // --- means same mobile theme as desktop + if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') { + return $user_mobile_theme; + } + } + + if (!$this->currentTheme) { + $this->computeCurrentTheme(); + } + + return $this->currentTheme; + } + + /** + * Returns the current mobile theme name. + * + * @return string Mobile theme name or empty string if installer + * @throws Exception + */ + public function getCurrentMobileTheme(): string + { + if ($this->mode->isInstall()) { + return ''; + } + + if (is_null($this->currentMobileTheme)) { + $this->computeCurrentMobileTheme(); + } + + return $this->currentMobileTheme; + } + + /** + * Setter for current theme name + * + * @param string $theme Name of current theme + */ + public function setCurrentTheme(string $theme): void + { + $this->currentTheme = $theme; + } + + /** + * Setter for current mobile theme name + * + * @param string $theme Name of current mobile theme + */ + public function setCurrentMobileTheme(string $theme): void + { + $this->currentMobileTheme = $theme; + } + + public function setThemeInfoValue(string $index, $value): void + { + $this->theme_info[$index] = $value; + } + + public function getThemeInfo(): array + { + return $this->theme_info; + } + + public function getThemeInfoValue(string $index, $default = null) + { + return $this->theme_info[$index] ?? $default; + } + + /** + * Provide a sane default if nothing is chosen or the specified theme does not exist. + * + * @return string Current theme's stylesheet path + * @throws Exception + */ + public function getCurrentThemeStylesheetPath(): string + { + return Theme::getStylesheetPath($this->getCurrentTheme()); + } + + /** + * Returns the current config cache of this node + */ + public function getConfigCache(): Cache + { + return $this->config->getCache(); + } + + /** + * The basepath of this app + * + * @return string Base path from configuration + */ + public function getBasePath(): string + { + return $this->config->get('system', 'basepath'); + } + + /** + * Computes the current theme name based on the node settings, the page owner settings and the user settings + * + * @throws Exception + */ + private function computeCurrentTheme() + { + $system_theme = $this->config->get('system', 'theme'); + if (!$system_theme) { + throw new Exception($this->l10n->t('No system theme config value set.')); + } + + // Sane default + $this->setCurrentTheme($system_theme); + + $page_theme = null; + $profile_owner = $this->getProfileOwner(); + + // Find the theme that belongs to the user whose stuff we are looking at + if (!empty($profile_owner) && ($profile_owner != $this->session->getLocalUserId())) { + // Allow folks to override user themes and always use their own on their own site. + // This works only if the user is on the same server + $user = $this->database->selectFirst('user', ['theme'], ['uid' => $profile_owner]); + if ($this->database->isResult($user) && !$this->session->getLocalUserId()) { + $page_theme = $user['theme']; + } + } + + $theme_name = $page_theme ?: $this->session->get('theme', $system_theme); + + $theme_name = Strings::sanitizeFilePathItem($theme_name); + if ($theme_name + && in_array($theme_name, Theme::getAllowedList()) + && (file_exists('view/theme/' . $theme_name . '/style.css') + || file_exists('view/theme/' . $theme_name . '/style.php')) + ) { + $this->setCurrentTheme($theme_name); + } + } + + /** + * Computes the current mobile theme name based on the node settings, the page owner settings and the user settings + */ + private function computeCurrentMobileTheme() + { + $system_mobile_theme = $this->config->get('system', 'mobile-theme', ''); + + // Sane default + $this->setCurrentMobileTheme($system_mobile_theme); + + $page_mobile_theme = null; + $profile_owner = $this->getProfileOwner(); + + // Find the theme that belongs to the user whose stuff we are looking at + if (!empty($profile_owner) && ($profile_owner != $this->session->getLocalUserId())) { + // Allow folks to override user themes and always use their own on their own site. + // This works only if the user is on the same server + if (!$this->session->getLocalUserId()) { + $page_mobile_theme = $this->pConfig->get($profile_owner, 'system', 'mobile-theme'); + } + } + + $mobile_theme_name = $page_mobile_theme ?: $this->session->get('mobile-theme', $system_mobile_theme); + + $mobile_theme_name = Strings::sanitizeFilePathItem($mobile_theme_name); + if ($mobile_theme_name == '---' + || + in_array($mobile_theme_name, Theme::getAllowedList()) + && (file_exists('view/theme/' . $mobile_theme_name . '/style.css') + || file_exists('view/theme/' . $mobile_theme_name . '/style.php')) + ) { + $this->setCurrentMobileTheme($mobile_theme_name); + } + } + + /** + * Automatically redirects to relative or absolute URL + * Should only be used if it isn't clear if the URL is either internal or external + * + * @param string $toUrl The target URL + * + * @throws InternalServerErrorException + */ + public function redirect(string $toUrl) + { + if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) { + System::externalRedirect($toUrl); + } else { + $this->baseURL->redirect($toUrl); + } + } +} diff --git a/src/BaseCollection.php b/src/BaseCollection.php index f2a64151d8..d7d707960a 100644 --- a/src/BaseCollection.php +++ b/src/BaseCollection.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; @@ -38,7 +24,7 @@ class BaseCollection extends \ArrayIterator * @param BaseEntity[] $entities * @param int|null $totalCount */ - public function __construct(array $entities = [], int $totalCount = null) + public function __construct(array $entities = [], ?int $totalCount = null) { parent::__construct($entities); @@ -103,7 +89,9 @@ class BaseCollection extends \ArrayIterator */ public function map(callable $callback): BaseCollection { - return new static(array_map($callback, $this->getArrayCopy()), $this->getTotalCount()); + $class = get_class($this); + + return new $class(array_map($callback, $this->getArrayCopy()), $this->getTotalCount()); } /** @@ -114,26 +102,27 @@ class BaseCollection extends \ArrayIterator * @return BaseCollection * @see array_filter() */ - public function filter(callable $callback = null, int $flag = 0): BaseCollection + public function filter(?callable $callback = null, int $flag = 0): BaseCollection { - return new static(array_filter($this->getArrayCopy(), $callback, $flag)); + $class = get_class($this); + + return new $class(array_filter($this->getArrayCopy(), $callback, $flag)); } /** * Reverse the orders of the elements in the collection - * - * @return $this */ public function reverse(): BaseCollection { - return new static(array_reverse($this->getArrayCopy()), $this->getTotalCount()); + $class = get_class($this); + + return new $class(array_reverse($this->getArrayCopy()), $this->getTotalCount()); } /** * Split the collection in smaller collections no bigger than the provided length * * @param int $length - * @return static[] */ public function chunk(int $length): array { @@ -141,9 +130,14 @@ class BaseCollection extends \ArrayIterator throw new \RangeException('BaseCollection->chunk(): Size parameter expected to be greater than 0'); } - return array_map(function ($array) { - return new static($array); - }, array_chunk($this->getArrayCopy(), $length)); + return array_map( + function ($array) { + $class = get_class($this); + + return new $class($array); + }, + array_chunk($this->getArrayCopy(), $length) + ); } diff --git a/src/BaseDataTransferObject.php b/src/BaseDataTransferObject.php index 6fae3596dd..c223c4f2a6 100644 --- a/src/BaseDataTransferObject.php +++ b/src/BaseDataTransferObject.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; diff --git a/src/BaseEntity.php b/src/BaseEntity.php index dca1af4f62..e4028cdfbe 100644 --- a/src/BaseEntity.php +++ b/src/BaseEntity.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; -use Friendica\Network\HTTPException; +use Friendica\Network\HTTPException\InternalServerErrorException; /** * The Entity classes directly inheriting from this abstract class are meant to represent a single business entity. @@ -43,12 +29,12 @@ abstract class BaseEntity extends BaseDataTransferObject /** * @param string $name * @return mixed - * @throws HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ public function __get(string $name) { if (!property_exists($this, $name)) { - throw new HTTPException\InternalServerErrorException('Unknown property ' . $name . ' in Entity ' . static::class); + throw new InternalServerErrorException('Unknown property ' . $name . ' in Entity ' . static::class); } return $this->$name; @@ -57,12 +43,12 @@ abstract class BaseEntity extends BaseDataTransferObject /** * @param mixed $name * @return bool - * @throws HTTPException\InternalServerErrorException + * @throws InternalServerErrorException */ public function __isset($name): bool { if (!property_exists($this, $name)) { - throw new HTTPException\InternalServerErrorException('Unknown property ' . $name . ' of type ' . gettype($name) . ' in Entity ' . static::class); + throw new InternalServerErrorException('Unknown property ' . $name . ' of type ' . gettype($name) . ' in Entity ' . static::class); } return !empty($this->$name); diff --git a/src/BaseFactory.php b/src/BaseFactory.php index e40da58773..b15ce4e7a0 100644 --- a/src/BaseFactory.php +++ b/src/BaseFactory.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; diff --git a/src/BaseModel.php b/src/BaseModel.php index 1189f7f33b..e79270843f 100644 --- a/src/BaseModel.php +++ b/src/BaseModel.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; @@ -29,7 +15,7 @@ use Psr\Log\LoggerInterface; * The Model classes inheriting from this abstract class are meant to represent a single database record. * The associated table name has to be provided in the child class, and the table is expected to have a unique `id` field. * - * @property int id + * @property int $id */ abstract class BaseModel extends BaseDataTransferObject { @@ -55,8 +41,6 @@ abstract class BaseModel extends BaseDataTransferObject private $originalData = []; /** - * @param Database $dba - * @param LoggerInterface $logger * @param array $data Table row attributes */ public function __construct(Database $dba, LoggerInterface $logger, array $data = []) diff --git a/src/BaseModule.php b/src/BaseModule.php index c04b835c52..933f38600b 100644 --- a/src/BaseModule.php +++ b/src/BaseModule.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; @@ -26,7 +12,6 @@ use Friendica\Capabilities\ICanHandleRequests; use Friendica\Capabilities\ICanCreateResponses; use Friendica\Core\Hook; use Friendica\Core\L10n; -use Friendica\Core\Logger; use Friendica\Core\System; use Friendica\Model\User; use Friendica\Module\Response; @@ -178,6 +163,19 @@ abstract class BaseModule implements ICanHandleRequests { } + /** + * Module GET method to process submitted data + * + * Extend this method if the module is supposed to process GET requests. + * Doesn't display any content + * + * @param string[] $request The $_REQUEST content + * @return void + */ + protected function get(array $request = []) + { + } + /** * {@inheritDoc} */ @@ -235,6 +233,9 @@ abstract class BaseModule implements ICanHandleRequests case Router::PUT: $this->put($request); break; + case Router::GET: + $this->get($request); + break; } $timestamp = microtime(true); @@ -259,10 +260,10 @@ abstract class BaseModule implements ICanHandleRequests $this->response->setStatus($e->getCode(), $e->getMessage()); $this->response->addContent($httpException->content($e)); - } finally { - $this->profiler->set(microtime(true) - $timestamp, 'content'); } + $this->profiler->set(microtime(true) - $timestamp, 'content'); + return $this->response->generate(); } @@ -283,7 +284,7 @@ abstract class BaseModule implements ICanHandleRequests $request[$parameter] = $this->getRequestValue($input, $parameter, $defaultvalue); } - foreach ($input ?? [] as $parameter => $value) { + foreach ($input as $parameter => $value) { if ($parameter == 'pagename') { continue; } @@ -356,7 +357,7 @@ abstract class BaseModule implements ICanHandleRequests */ public static function getFormSecurityToken(string $typename = ''): string { - $user = User::getById(DI::app()->getLoggedInUserId(), ['guid', 'prvkey']); + $user = User::getById(DI::userSession()->getLocalUserId(), ['guid', 'prvkey']); $timestamp = time(); $sec_hash = hash('whirlpool', ($user['guid'] ?? '') . ($user['prvkey'] ?? '') . session_id() . $timestamp . $typename); @@ -390,7 +391,7 @@ abstract class BaseModule implements ICanHandleRequests $max_livetime = 10800; // 3 hours - $user = User::getById(DI::app()->getLoggedInUserId(), ['guid', 'prvkey']); + $user = User::getById(DI::userSession()->getLocalUserId(), ['guid', 'prvkey']); $x = explode('.', $hash); if (time() > (intval($x[0]) + $max_livetime)) { @@ -410,8 +411,8 @@ abstract class BaseModule implements ICanHandleRequests public static function checkFormSecurityTokenRedirectOnError(string $err_redirect, string $typename = '', string $formname = 'form_security_token') { if (!self::checkFormSecurityToken($typename, $formname)) { - Logger::notice('checkFormSecurityToken failed: user ' . DI::app()->getLoggedInUserNickname() . ' - form element ' . $typename); - Logger::debug('checkFormSecurityToken failed', ['request' => $_REQUEST]); + DI::logger()->notice('checkFormSecurityToken failed: user ' . DI::userSession()->getLocalUserNickname() . ' - form element ' . $typename); + DI::logger()->debug('checkFormSecurityToken failed', ['request' => $_REQUEST]); DI::sysmsg()->addNotice(self::getFormSecurityStandardErrorMessage()); DI::baseUrl()->redirect($err_redirect); } @@ -420,8 +421,8 @@ abstract class BaseModule implements ICanHandleRequests public static function checkFormSecurityTokenForbiddenOnError(string $typename = '', string $formname = 'form_security_token') { if (!self::checkFormSecurityToken($typename, $formname)) { - Logger::notice('checkFormSecurityToken failed: user ' . DI::app()->getLoggedInUserNickname() . ' - form element ' . $typename); - Logger::debug('checkFormSecurityToken failed', ['request' => $_REQUEST]); + DI::logger()->notice('checkFormSecurityToken failed: user ' . DI::userSession()->getLocalUserNickname() . ' - form element ' . $typename); + DI::logger()->debug('checkFormSecurityToken failed', ['request' => $_REQUEST]); throw new \Friendica\Network\HTTPException\ForbiddenException(); } @@ -470,7 +471,7 @@ abstract class BaseModule implements ICanHandleRequests * @param string $content * @param string $type * @param string|null $content_type - * @return void + * @return never * @throws HTTPException\InternalServerErrorException */ public function httpExit(string $content, string $type = Response::TYPE_HTML, ?string $content_type = null) @@ -507,11 +508,11 @@ abstract class BaseModule implements ICanHandleRequests * @param mixed $content * @param string $content_type * @param int $options A combination of json_encode() binary flags - * @return void + * @return never * @throws HTTPException\InternalServerErrorException * @see json_encode() */ - public function jsonExit($content, string $content_type = 'application/json', int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) + public function jsonExit($content, string $content_type = 'application/json; charset=utf-8', int $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) { $this->httpExit(json_encode($content, $options), ICanCreateResponses::TYPE_JSON, $content_type); } @@ -522,7 +523,7 @@ abstract class BaseModule implements ICanHandleRequests * @param int $httpCode * @param mixed $content * @param string $content_type - * @return void + * @return never * @throws HTTPException\InternalServerErrorException */ public function jsonError(int $httpCode, $content, string $content_type = 'application/json') diff --git a/src/BaseRepository.php b/src/BaseRepository.php index 1ffe82b0b5..bf495a6a5d 100644 --- a/src/BaseRepository.php +++ b/src/BaseRepository.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica; @@ -144,17 +130,33 @@ abstract class BaseRepository } /** - * @param array $condition - * @param array $params - * @return BaseEntity + * Selects the fields of the first row as array + * + * @throws NotFoundException + * + * @return array The resulted fields as array + */ + final protected function _selectFirstRowAsArray(array $condition, array $params = []): array + { + $fields = $this->db->selectFirst(static::$table_name, [], $condition, $params); + + if (!$this->db->isResult($fields)) { + throw new NotFoundException(); + } + + return $fields; + } + + /** + * @deprecated 2025.05 Use `\Friendica\BaseRepository::_selectFirstRowAsArray()` instead + * * @throws NotFoundException */ protected function _selectOne(array $condition, array $params = []): BaseEntity { - $fields = $this->db->selectFirst(static::$table_name, [], $condition, $params); - if (!$this->db->isResult($fields)) { - throw new NotFoundException(); - } + @trigger_error('`' . __METHOD__ . '()` is deprecated since 2025.05 and will be removed after 5 months, use `\Friendica\BaseRepository::_selectFirstRowAsArray()` instead.', E_USER_DEPRECATED); + + $fields = $this->_selectFirstRowAsArray( $condition, $params); return $this->factory->createFromTableRow($fields); } diff --git a/src/Capabilities/ICanCreateFromTableRow.php b/src/Capabilities/ICanCreateFromTableRow.php index c18bb981d9..05a60e7d05 100644 --- a/src/Capabilities/ICanCreateFromTableRow.php +++ b/src/Capabilities/ICanCreateFromTableRow.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Capabilities; diff --git a/src/Capabilities/ICanCreateResponses.php b/src/Capabilities/ICanCreateResponses.php index 837e0cd886..c3e64d9e4c 100644 --- a/src/Capabilities/ICanCreateResponses.php +++ b/src/Capabilities/ICanCreateResponses.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Capabilities; diff --git a/src/Capabilities/ICanHandleRequests.php b/src/Capabilities/ICanHandleRequests.php index ae29aee9b3..f81f36b3c8 100644 --- a/src/Capabilities/ICanHandleRequests.php +++ b/src/Capabilities/ICanHandleRequests.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Capabilities; diff --git a/src/Collection/Api/Mastodon/Emojis.php b/src/Collection/Api/Mastodon/Emojis.php index c92e0d431d..08f072ece1 100644 --- a/src/Collection/Api/Mastodon/Emojis.php +++ b/src/Collection/Api/Mastodon/Emojis.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Collection\Api\Mastodon; diff --git a/src/Collection/Api/Mastodon/Fields.php b/src/Collection/Api/Mastodon/Fields.php index b98438ce26..3aba73a349 100644 --- a/src/Collection/Api/Mastodon/Fields.php +++ b/src/Collection/Api/Mastodon/Fields.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Collection\Api\Mastodon; -use Friendica\Api\Entity\Mastodon\Field; use Friendica\BaseCollection; +use Friendica\Object\Api\Mastodon\Field; class Fields extends BaseCollection { diff --git a/src/Collection/Api/Mastodon/Mentions.php b/src/Collection/Api/Mastodon/Mentions.php index 2155ffbe42..b71e276def 100644 --- a/src/Collection/Api/Mastodon/Mentions.php +++ b/src/Collection/Api/Mastodon/Mentions.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Collection\Api\Mastodon; diff --git a/src/Collection/Api/Notifications.php b/src/Collection/Api/Notifications.php index 4bb1eaa566..383116ac9f 100644 --- a/src/Collection/Api/Notifications.php +++ b/src/Collection/Api/Notifications.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Collection\Api; diff --git a/src/Console/Addon.php b/src/Console/Addon.php index aa47d41a24..c279ef2976 100644 --- a/src/Console/Addon.php +++ b/src/Console/Addon.php @@ -1,31 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Console_Table; -use Friendica\App; -use Friendica\Content\Pager; +use Friendica\App\Mode; use Friendica\Core\L10n; -use Friendica\Core\Addon as AddonCore; +use Friendica\Core\Addon\AddonHelper; use Friendica\Database\Database; use Friendica\Util\Strings; use RuntimeException; @@ -38,7 +23,7 @@ class Addon extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -49,6 +34,7 @@ class Addon extends \Asika\SimpleConsole\Console * @var Database */ private $dba; + private AddonHelper $addonHelper; protected function getHelp() { @@ -71,15 +57,16 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, L10n $l10n, Database $dba, array $argv = null) + public function __construct(Mode $appMode, L10n $l10n, Database $dba, AddonHelper $addonHelper, array $argv = null) { parent::__construct($argv); $this->appMode = $appMode; $this->l10n = $l10n; $this->dba = $dba; + $this->addonHelper = $addonHelper; - AddonCore::loadAddons(); + $this->addonHelper->loadAddons(); } protected function doExecute(): int @@ -136,28 +123,30 @@ HELP; $this->out($this->getHelp()); return false; } - foreach (AddonCore::getAvailableList() as $addon) { - $addon_name = $addon[0]; - $enabled = AddonCore::isEnabled($addon_name) ? "enabled" : "disabled"; - switch ($subCmd) { - case 'all': - $table->addRow([$addon_name, $enabled]); - break; - case 'enabled': - if (!$enabled) { - continue 2; - } - $table->addRow([$addon_name]); - case 'disabled': - if ($enabled) { - continue 2; - } - $table->addRow([$addon_name]); - break; + + foreach ($this->addonHelper->getAvailableAddons() as $addonId) { + $enabled = $this->addonHelper->isAddonEnabled($addonId); + + if ($subCmd === 'all') { + $table->addRow([$addonId, $enabled ? 'enabled' : 'disabled']); + + continue; } + if ($subCmd === 'enabled' && $enabled === true) { + $table->addRow([$addonId]); + continue; + } + + if ($subCmd === 'disabled' && $enabled === false) { + $table->addRow([$addonId]); + continue; + } } + $this->out($table->getTable()); + + return 0; } /** @@ -175,11 +164,11 @@ HELP; throw new RuntimeException($this->l10n->t('Addon not found')); } - if (AddonCore::isEnabled($addon)) { + if ($this->addonHelper->isAddonEnabled($addon)) { throw new RuntimeException($this->l10n->t('Addon already enabled')); } - AddonCore::install($addon); + $this->addonHelper->installAddon($addon); return 0; } @@ -199,11 +188,11 @@ HELP; throw new RuntimeException($this->l10n->t('Addon not found')); } - if (!AddonCore::isEnabled($addon)) { + if (!$this->addonHelper->isAddonEnabled($addon)) { throw new RuntimeException($this->l10n->t('Addon already disabled')); } - AddonCore::uninstall($addon); + $this->addonHelper->uninstallAddon($addon); return 0; } diff --git a/src/Console/ArchiveContact.php b/src/Console/ArchiveContact.php index 7e1b07af24..fef6065823 100644 --- a/src/Console/ArchiveContact.php +++ b/src/Console/ArchiveContact.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Database\Database; use Friendica\DI; use Friendica\Model\Contact; @@ -42,7 +28,7 @@ class ArchiveContact extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -71,7 +57,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, Database $dba, \Friendica\Core\L10n $l10n, array $argv = null) + public function __construct(Mode $appMode, Database $dba, \Friendica\Core\L10n $l10n, array $argv = null) { parent::__construct($argv); diff --git a/src/Console/AutomaticInstallation.php b/src/Console/AutomaticInstallation.php index 33b6f8303f..198317f7b4 100644 --- a/src/Console/AutomaticInstallation.php +++ b/src/Console/AutomaticInstallation.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Asika\SimpleConsole\Console; -use Friendica\App; -use Friendica\App\BaseURL; +use Exception; +use Friendica\App\Mode; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\ValueObject\Cache; use Friendica\Core\Installer; @@ -34,7 +20,7 @@ use RuntimeException; class AutomaticInstallation extends Console { - /** @var App\Mode */ + /** @var Mode */ private $appMode; /** @var \Friendica\Core\Config\ValueObject\Cache */ private $configCache; @@ -68,12 +54,12 @@ Options -d|--dbdata The name of the mysql/mariadb database (env MYSQL_DATABASE) -u|--dbuser The username of the mysql/mariadb database login (env MYSQL_USER or MYSQL_USERNAME) -P|--dbpass The password of the mysql/mariadb database login (env MYSQL_PASSWORD) - -U|--url The full base URL of Friendica - f.e. 'https://friendica.local/sub' (env FRIENDICA_URL) + -U|--url The full base URL of Friendica - f.e. 'https://friendica.local/sub' (env FRIENDICA_URL) -B|--phppath The path of the PHP binary (env FRIENDICA_PHP_PATH) -b|--basepath The basepath of Friendica (env FRIENDICA_BASE_PATH) -t|--tz The timezone of Friendica (env FRIENDICA_TZ) -L|--lang The language of Friendica (env FRIENDICA_LANG) - + Environment variables MYSQL_HOST The host of the mysql/mariadb database (mandatory if mysql and environment is used) MYSQL_PORT The port of the mysql/mariadb database @@ -87,7 +73,7 @@ Environment variables FRIENDICA_ADMIN_MAIL The admin email address of Friendica (this email will be used for admin access) FRIENDICA_TZ The timezone of Friendica FRIENDICA_LANG The langauge of Friendica - + Examples bin/console autoinstall -f 'input.config.php Installs Friendica with the prepared 'input.config.php' file @@ -100,7 +86,7 @@ Examples HELP; } - public function __construct(App\Mode $appMode, Cache $configCache, IManageConfigValues $config, Database $dba, array $argv = null) + public function __construct(Mode $appMode, Cache $configCache, IManageConfigValues $config, Database $dba, array $argv = null) { parent::__construct($argv); diff --git a/src/Console/Cache.php b/src/Console/Cache.php index c553716993..c019c18429 100644 --- a/src/Console/Cache.php +++ b/src/Console/Cache.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Asika\SimpleConsole\CommandArgsException; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\Cache\Capability\ICanCache; use RuntimeException; @@ -39,7 +25,7 @@ class Cache extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; @@ -82,7 +68,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, ICanCache $cache, array $argv = null) + public function __construct(Mode $appMode, ICanCache $cache, array $argv = null) { parent::__construct($argv); @@ -99,7 +85,7 @@ HELP; $this->out('Options: ' . var_export($this->options, true)); } - if (!$this->appMode->has(App\Mode::DBAVAILABLE)) { + if (!$this->appMode->has(Mode::DBAVAILABLE)) { $this->out('Database isn\'t ready or populated yet, database cache won\'t be available'); } diff --git a/src/Console/ClearAvatarCache.php b/src/Console/ClearAvatarCache.php new file mode 100644 index 0000000000..e80684c354 --- /dev/null +++ b/src/Console/ClearAvatarCache.php @@ -0,0 +1,106 @@ +dba = $dba; + $this->baseUrl = $baseUrl; + $this->l10n = $l10n; + $this->config = $config; + } + + protected function doExecute(): int + { + if ($this->config->get('system', 'avatar_cache')) { + $this->err($this->l10n->t('The avatar cache needs to be disabled in local.config.php to use this command.')); + return 2; + } + + // Contacts (but not self contacts) with cached avatars. + $condition = ["NOT `self` AND (`photo` != ? OR `thumb` != ? OR `micro` != ?)", '', '', '']; + $total = $this->dba->count('contact', $condition); + $count = 0; + $contacts = $this->dba->select('contact', ['id', 'uri-id', 'url', 'uid', 'photo', 'thumb', 'micro'], $condition); + while ($contact = $this->dba->fetch($contacts)) { + if (Avatar::deleteCache($contact) || $this->isAvatarCache($contact)) { + Contact::update(['photo' => '', 'thumb' => '', 'micro' => ''], ['id' => $contact['id']]); + } + $this->out(++$count . '/' . $total . "\t" . $contact['id'] . "\t" . $contact['url'] . "\t" . $contact['photo']); + } + $this->dba->close($contacts); + return 0; + } + + private function isAvatarCache(array $contact): bool + { + if (!empty($contact['photo']) && strpos($contact['photo'], Avatar::baseUrl()) === 0) { + return true; + } + if (!empty($contact['thumb']) && strpos($contact['thumb'], Avatar::baseUrl()) === 0) { + return true; + } + if (!empty($contact['micro']) && strpos($contact['micro'], Avatar::baseUrl()) === 0) { + return true; + } + return false; + } +} diff --git a/src/Console/Config.php b/src/Console/Config.php index efb795360c..7b222a4481 100644 --- a/src/Console/Config.php +++ b/src/Console/Config.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Asika\SimpleConsole\CommandArgsException; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Core\Config\Capability\IManageConfigValues; use RuntimeException; @@ -52,7 +38,7 @@ class Config extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -94,7 +80,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, IManageConfigValues $config, array $argv = null) + public function __construct(Mode $appMode, IManageConfigValues $config, array $argv = null) { parent::__construct($argv); diff --git a/src/Console/Contact.php b/src/Console/Contact.php index a80b88349a..d54979c11b 100644 --- a/src/Console/Contact.php +++ b/src/Console/Contact.php @@ -1,28 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Console_Table; -use Friendica\App; +use Friendica\App\Mode; +use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\DI; use Friendica\Model\Contact as ContactModel; use Friendica\Model\User as UserModel; @@ -39,11 +26,11 @@ class Contact extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** - * @var IPConfig + * @var IManagePersonalConfigValues */ private $pConfig; @@ -69,7 +56,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, array $argv = null) + public function __construct(Mode $appMode, ?array $argv = null) { parent::__construct($argv); @@ -97,13 +84,13 @@ HELP; switch ($command) { case 'add': - return $this->addContact(); + return ($this->addContact()) ? 0 : 1; case 'remove': - return $this->removeContact(); + return ($this->removeContact()) ? 0 : 1; case 'search': - return $this->searchContact(); + return ($this->searchContact()) ? 0 : 1; case 'terminate': - return $this->terminateContact(); + return ($this->terminateContact()) ? 0 : 1; default: throw new \Asika\SimpleConsole\CommandArgsException('Wrong command.'); } @@ -172,9 +159,10 @@ HELP; if ($result['success']) { $this->out('User ' . $user['nickname'] . ' now connected to ' . $url . ', contact ID ' . $result['cid']); - } else { - throw new RuntimeException($result['message']); + return true; } + + throw new RuntimeException($result['message']); } /** @@ -218,7 +206,7 @@ HELP; /** * Marks a contact for removal */ - private function removeContact() + private function removeContact(): bool { $cid = $this->getArgument(1); if (empty($cid)) { @@ -230,6 +218,8 @@ HELP; } ContactModel::remove($cid); + + return true; } /** diff --git a/src/Console/CreateDoxygen.php b/src/Console/CreateDoxygen.php index 1ceb34106e..3dc3c7e3ec 100644 --- a/src/Console/CreateDoxygen.php +++ b/src/Console/CreateDoxygen.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/Daemon.php b/src/Console/Daemon.php new file mode 100644 index 0000000000..a21ff26fc6 --- /dev/null +++ b/src/Console/Daemon.php @@ -0,0 +1,224 @@ +mode = $mode; + $this->config = $config; + $this->keyValue = $keyValue; + $this->basePath = $basePath; + $this->system = $system; + $this->logger = $logger; + $this->dba = $dba; + $this->daemon = $daemon; + } + + protected function getHelp(): string + { + return <<mode->isInstall()) { + throw new RuntimeException("Friendica isn't properly installed yet"); + } + + $this->mode->setExecutor(Mode::DAEMON); + + $this->config->reload(); + + if (empty($this->config->get('system', 'pidfile'))) { + throw new RuntimeException( + <<< TXT + Please set system.pidfile in config/local.config.php. For example: + + 'system' => [ + 'pidfile' => '/path/to/daemon.pid', + ], + TXT + ); + } + + $pidfile = $this->config->get('system', 'pidfile'); + + $daemonMode = $this->getArgument(0); + $foreground = $this->getOption(['f', 'foreground']) ?? false; + + if (empty($daemonMode)) { + throw new CommandArgsException("Please use either 'start', 'stop' or 'status'"); + } + + $this->daemon->init($pidfile); + + if ($daemonMode == 'status') { + if ($this->daemon->isRunning()) { + $this->out(sprintf("Daemon process %s is running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + } else { + $this->out(sprintf("Daemon process %s isn't running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + } + return 0; + } + + if ($daemonMode == 'stop') { + if (!$this->daemon->isRunning()) { + $this->out(sprintf("Daemon process %s isn't running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + return 0; + } + + if ($this->daemon->stop()) { + $this->keyValue->set('worker_daemon_mode', false); + $this->out(sprintf("Daemon process %s was killed (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + return 0; + } + + return 1; + } + + if ($this->daemon->isRunning()) { + $this->out(sprintf("Daemon process %s is already running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + return 1; + } + + if ($daemonMode == "start") { + $this->out("Starting Friendica daemon"); + + $this->daemon->start(function () { + $wait_interval = intval($this->config->get('system', 'cron_interval', 5)) * 60; + + $do_cron = true; + $last_cron = 0; + + $path = $this->basePath->getPath(); + + // Now running as a daemon. + while (true) { + // Check the database structure and possibly fixes it + Update::check($path, true); + + if (!$do_cron && ($last_cron + $wait_interval) < time()) { + $this->logger->info('Forcing cron worker call.', ['pid' => $this->daemon->getPid()]); + $do_cron = true; + } + + if ($do_cron || (!$this->system->isMaxLoadReached() && Worker::entriesExists() && Worker::isReady())) { + Worker::spawnWorker($do_cron); + } else { + $this->logger->info('Cool down for 5 seconds', ['pid' => $this->daemon->getPid()]); + sleep(5); + } + + if ($do_cron) { + // We force a reconnect of the database connection. + // This is done to ensure that the connection don't get lost over time. + $this->dba->reconnect(); + + $last_cron = time(); + } + + $start = time(); + $this->logger->info('Sleeping', ['pid' => $this->daemon->getPid(), 'until' => gmdate(DateTimeFormat::MYSQL, $start + $wait_interval)]); + + do { + $seconds = (time() - $start); + + // logarithmic wait time calculation. + // Background: After jobs had been started, they often fork many workers. + // To not waste too much time, the sleep period increases. + $arg = (($seconds + 1) / ($wait_interval / 9)) + 1; + $sleep = min(1000000, round(log10($arg) * 1000000, 0)); + + $this->daemon->sleep((int)$sleep); + + $timeout = ($seconds >= $wait_interval); + } while (!$timeout && !Worker\IPC::JobsExists()); + + if ($timeout) { + $do_cron = true; + $this->logger->info('Woke up after $wait_interval seconds.', ['pid' => $this->daemon->getPid(), 'sleep' => $wait_interval]); + } else { + $do_cron = false; + $this->logger->info('Worker jobs are calling to be forked.', ['pid' => $this->daemon->getPid()]); + } + } + }, $foreground); + + return 0; + } + + $this->err('Invalid command'); + $this->out($this->getHelp()); + return 1; + } +} diff --git a/src/Console/DatabaseStructure.php b/src/Console/DatabaseStructure.php index 4d4125e88a..cdc46aa435 100644 --- a/src/Console/DatabaseStructure.php +++ b/src/Console/DatabaseStructure.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/DocBloxErrorChecker.php b/src/Console/DocBloxErrorChecker.php index 7028de8a13..694940c95e 100644 --- a/src/Console/DocBloxErrorChecker.php +++ b/src/Console/DocBloxErrorChecker.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Friendica\AppHelper; /** * When I installed docblox, I had the experience that it does not generate any output at all. @@ -47,14 +33,14 @@ class DocBloxErrorChecker extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; - /** @var App */ - private $app; + /** @var string */ + private $basePath; - public function __construct(App $app, array $argv = null) + public function __construct(AppHelper $appHelper, array $argv = null) { parent::__construct($argv); - $this->app = $app; + $this->basePath = $appHelper->getBasePath(); } protected function getHelp() @@ -87,7 +73,7 @@ HELP; throw new \RuntimeException('DocBlox isn\'t available.'); } - $dir = $this->app->getBasePath(); + $dir = $this->basePath; //stack for dirs to search $dirstack = []; diff --git a/src/Console/Extract.php b/src/Console/Extract.php index 1f84d442b5..0c05a39705 100644 --- a/src/Console/Extract.php +++ b/src/Console/Extract.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/FixAPDeliveryWorkerTaskParameters.php b/src/Console/FixAPDeliveryWorkerTaskParameters.php index d2357c318d..9cced29dec 100644 --- a/src/Console/FixAPDeliveryWorkerTaskParameters.php +++ b/src/Console/FixAPDeliveryWorkerTaskParameters.php @@ -1,32 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Asika\SimpleConsole\CommandArgsException; +use Friendica\App\Mode; +use Friendica\Core\L10n; use Friendica\Database\Database; -use Friendica\Database\DBA; -use Friendica\DI; use Friendica\Model\Contact; -use Friendica\Util\Strings; use RuntimeException; /** @@ -37,13 +22,17 @@ class FixAPDeliveryWorkerTaskParameters extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** * @var Database */ private $dba; + /** + * @var L10n + */ + private $l10n; /** * @var int */ @@ -67,7 +56,7 @@ Usage Description During the 2020.12 RC period some worker task parameters have been corrupted, resulting in the impossibility to execute them. This command restores their expected parameters. - If you didn't run Friendica during the 2020.12 RC period, you do not need to use this command. + If you didn't run Friendica during the 2020.12 RC period, you do not need to use this command. Options -h|--help|-? Show help information @@ -76,7 +65,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, Database $dba, \Friendica\Core\L10n $l10n, array $argv = null) + public function __construct(Mode $appMode, Database $dba, L10n $l10n, array $argv = null) { parent::__construct($argv); @@ -94,7 +83,7 @@ HELP; } if (count($this->args) > 0) { - throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments'); + throw new CommandArgsException('Too many arguments'); } if ($this->appMode->isInstall()) { diff --git a/src/Console/GlobalCommunityBlock.php b/src/Console/GlobalCommunityBlock.php index 6eb1703489..2e85491455 100644 --- a/src/Console/GlobalCommunityBlock.php +++ b/src/Console/GlobalCommunityBlock.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Core\L10n; use Friendica\Model\Contact; @@ -36,7 +22,7 @@ class GlobalCommunityBlock extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -62,7 +48,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, L10n $l10n, $argv = null) + public function __construct(Mode $appMode, L10n $l10n, $argv = null) { parent::__construct($argv); diff --git a/src/Console/GlobalCommunitySilence.php b/src/Console/GlobalCommunitySilence.php index 5d1f692634..9843af99cb 100644 --- a/src/Console/GlobalCommunitySilence.php +++ b/src/Console/GlobalCommunitySilence.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/JetstreamDaemon.php b/src/Console/JetstreamDaemon.php new file mode 100644 index 0000000000..365c26ebce --- /dev/null +++ b/src/Console/JetstreamDaemon.php @@ -0,0 +1,161 @@ +mode = $mode; + $this->config = $config; + $this->keyValue = $keyValue; + $this->jetstream = $jetstream; + $this->daemon = $daemon; + $this->addonHelper = $addonHelper; + } + + protected function getHelp(): string + { + return <<mode->isInstall()) { + throw new RuntimeException("Friendica isn't properly installed yet"); + } + + $this->config->reload(); + + if (empty($this->config->get('jetstream', 'pidfile'))) { + throw new RuntimeException( + <<< TXT + Please set jetstream.pidfile in config/local.config.php. For example: + + 'jetstream' => [ + 'pidfile' => '/path/to/jetstream.pid', + ], + TXT + ); + } + + $this->addonHelper->loadAddons(); + Hook::loadHooks(); + + if (!$this->addonHelper->isAddonEnabled('bluesky')) { + throw new RuntimeException("Bluesky has to be enabled.\n"); + } + + $pidfile = $this->config->get('jetstream', 'pidfile'); + + $daemonMode = $this->getArgument(0); + $foreground = $this->getOption(['f', 'foreground']) ?? false; + + if (empty($daemonMode)) { + throw new RuntimeException("Please use either 'start', 'stop' or 'status'"); + } + + $this->daemon->init($pidfile); + + if ($daemonMode == 'status') { + if ($this->daemon->isRunning()) { + $this->out(sprintf("Daemon process %s is running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + } else { + $this->out(sprintf("Daemon process %s isn't running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + } + return 0; + } + + if ($daemonMode == 'stop') { + if (!$this->daemon->isRunning()) { + $this->out(sprintf("Daemon process %s isn't running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + return 0; + } + + if ($this->daemon->stop()) { + $this->keyValue->set('worker_daemon_mode', false); + $this->out(sprintf("Daemon process %s was killed (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + return 0; + } + + return 1; + } + + if ($this->daemon->isRunning()) { + $this->out(sprintf("Daemon process %s is already running (%s)", $this->daemon->getPid(), $this->daemon->getPidfile())); + return 1; + } + + if ($daemonMode == "start") { + $this->out("Starting Jetstream daemon"); + + $this->daemon->start(function () { + $this->jetstream->listen(); + }, $foreground); + + return 0; + } + + $this->err('Invalid command'); + $this->out($this->getHelp()); + return 1; + } +} diff --git a/src/Console/Lock.php b/src/Console/Lock.php index 0acede5df8..f883adfc6e 100644 --- a/src/Console/Lock.php +++ b/src/Console/Lock.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Asika\SimpleConsole\CommandArgsException; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Core\Lock\Capability\ICanLock; use RuntimeException; @@ -37,7 +23,7 @@ class Lock extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; @@ -76,7 +62,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, ICanLock $lock, array $argv = null) + public function __construct(Mode $appMode, ICanLock $lock, array $argv = null) { parent::__construct($argv); @@ -93,7 +79,7 @@ HELP; $this->out('Options: ' . var_export($this->options, true)); } - if (!$this->appMode->has(App\Mode::DBAVAILABLE)) { + if (!$this->appMode->has(Mode::DBAVAILABLE)) { $this->out('Database isn\'t ready or populated yet, database cache won\'t be available'); } diff --git a/src/Console/Maintenance.php b/src/Console/Maintenance.php index 076b89db82..168bccd164 100644 --- a/src/Console/Maintenance.php +++ b/src/Console/Maintenance.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Core\Config\Capability\IManageConfigValues; /** @@ -32,7 +18,7 @@ class Maintenance extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -69,7 +55,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, IManageConfigValues $config, $argv = null) + public function __construct(Mode $appMode, IManageConfigValues $config, $argv = null) { parent::__construct($argv); diff --git a/src/Console/MergeContacts.php b/src/Console/MergeContacts.php index f04b64a23d..54f3fe5142 100644 --- a/src/Console/MergeContacts.php +++ b/src/Console/MergeContacts.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; @@ -33,7 +19,7 @@ class MergeContacts extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var $dba Database + * @var Database */ private $dba; diff --git a/src/Console/MoveToAvatarCache.php b/src/Console/MoveToAvatarCache.php index 055b0c2c0d..567c82f2b6 100644 --- a/src/Console/MoveToAvatarCache.php +++ b/src/Console/MoveToAvatarCache.php @@ -1,35 +1,21 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Friendica\App\BaseURL; use Friendica\Contact\Avatar; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; +use Friendica\Core\Protocol; +use Friendica\Database\Database; use Friendica\Model\Contact; use Friendica\Model\Photo; -use Friendica\Util\Images; use Friendica\Object\Image; -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Protocol; /** * tool to move cached avatars to the avatar file cache. @@ -39,12 +25,12 @@ class MoveToAvatarCache extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var $dba Friendica\Database\Database + * @var Database */ private $dba; /** - * @var $baseurl Friendica\App\BaseURL + * @var BaseURL */ private $baseUrl; @@ -129,6 +115,10 @@ HELP; private function storeAvatar(string $resourceid, array $contact, bool $quit_on_invalid) { + $photo = false; + $imgdata = false; + $image = null; + $valid = !empty($resourceid); if ($valid) { $this->out('1', false); @@ -150,14 +140,14 @@ HELP; if ($valid) { $this->out('3', false); - $image = new Image($imgdata, Images::getMimeTypeByData($imgdata)); + $image = new Image($imgdata); if (!$image->isValid()) { $this->out(' ' . $this->l10n->t('invalid image for id %s', $resourceid) . ' ', false); $valid = false; } } - if ($valid) { + if ($valid && $image instanceof Image) { $this->out('4', false); $fields = Avatar::storeAvatarByImage($contact, $image); } else { diff --git a/src/Console/PhpToPo.php b/src/Console/PhpToPo.php index a400fea23c..ad377561a2 100644 --- a/src/Console/PhpToPo.php +++ b/src/Console/PhpToPo.php @@ -1,27 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Friendica\AppHelper; +use stdClass; /** * Read a strings.php file and create messages.po in the same directory @@ -34,14 +21,14 @@ class PhpToPo extends \Asika\SimpleConsole\Console private $normBaseMsgIds = []; const NORM_REGEXP = "|[\\\]|"; - /** @var App */ - private $app; + /** @var AppHelper */ + private $appHelper; - public function __construct(App $app, array $argv = null) + public function __construct(AppHelper $appHelper, array $argv = null) { parent::__construct($argv); - $this->app = $app; + $this->appHelper = $appHelper; } protected function getHelp() @@ -80,7 +67,8 @@ HELP; throw new \Asika\SimpleConsole\CommandArgsException('Too many arguments'); } - $a = $this->app; + $a = new stdClass(); + $a->strings = []; $phpfile = realpath($this->getArgument(0)); diff --git a/src/Console/PoToPhp.php b/src/Console/PoToPhp.php index e178e80e72..480da23c46 100644 --- a/src/Console/PoToPhp.php +++ b/src/Console/PoToPhp.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; @@ -148,7 +134,7 @@ HELP; ); $fnname = 'string_plural_select_' . $lang; - $out = 'if(! function_exists("' . $fnname . '")) {' . "\n"; + $out = 'if(! function_exists("' . $fnname . '")) {' . "\n"; $out .= 'function ' . $fnname . '($n){' . "\n"; $out .= ' $n = intval($n);' . "\n"; $out .= ' ' . $return . "\n"; @@ -189,11 +175,11 @@ HELP; * @param string $string * @param array|string $node */ - private static function parse(string $string, &$node = []) + private static function parse(string $string, &$node) { // Removes extra outward parentheses if (strpos($string, '(') === 0 && strrpos($string, ')') === strlen($string) - 1) { - $string = substr($string, 1, -1); + $string = (string) substr($string, 1, -1); } $q = strpos($string, '?'); @@ -206,13 +192,13 @@ HELP; if ($q === false || $s < $q) { list($then, $else) = explode(':', $string, 2); - $node['then'] = $then; - $parsedElse = []; + $node['then'] = $then; + $parsedElse = []; self::parse($else, $parsedElse); $node['else'] = $parsedElse; } else { list($if, $thenelse) = explode('?', $string, 2); - $node['if'] = $if; + $node['if'] = $if; self::parse($thenelse, $node); } } @@ -228,7 +214,7 @@ HELP; private static function render($tree): string { if (is_array($tree)) { - $if = trim($tree['if']); + $if = trim($tree['if']); $then = trim($tree['then']); $else = self::render($tree['else']); diff --git a/src/Console/PostUpdate.php b/src/Console/PostUpdate.php index 42cc63e109..318712cdc5 100644 --- a/src/Console/PostUpdate.php +++ b/src/Console/PostUpdate.php @@ -1,30 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs; use Friendica\Core\L10n; use Friendica\Core\Update; +use Friendica\DI; /** * Performs database post updates @@ -34,7 +21,7 @@ class PostUpdate extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -45,6 +32,10 @@ class PostUpdate extends \Asika\SimpleConsole\Console * @var L10n */ private $l10n; + /** + * @var string + */ + private $basePath; protected function getHelp() { @@ -60,19 +51,18 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, IManageKeyValuePairs $keyValue, L10n $l10n, array $argv = null) + public function __construct(Mode $appMode, IManageKeyValuePairs $keyValue, L10n $l10n, array $argv = null) { parent::__construct($argv); $this->appMode = $appMode; $this->keyValue = $keyValue; $this->l10n = $l10n; + $this->basePath = DI::appHelper()->getBasePath(); } protected function doExecute(): int { - $a = \Friendica\DI::app(); - if ($this->getOption($this->helpOptions)) { $this->out($this->getHelp()); return 0; @@ -93,7 +83,7 @@ HELP; } echo $this->l10n->t('Check for pending update actions.') . "\n"; - Update::run($a->getBasePath(), true, false, true, false); + Update::run($this->basePath, true, false, true, false); echo $this->l10n->t('Done.') . "\n"; echo $this->l10n->t('Execute pending post updates.') . "\n"; diff --git a/src/Console/Relay.php b/src/Console/Relay.php index 11d1dbf821..3aeaad458f 100644 --- a/src/Console/Relay.php +++ b/src/Console/Relay.php @@ -1,27 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Asika\SimpleConsole\CommandArgsException; +use Friendica\Database\Database; use Friendica\Model\APContact; use Friendica\Protocol\ActivityPub\Transmitter; use Friendica\Protocol\Relay as ProtocolRelay; @@ -37,7 +24,7 @@ class Relay extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var $dba Friendica\Database\Database + * @var Database */ private $dba; @@ -104,12 +91,17 @@ HELP; $actor = $this->getArgument(1); $apcontact = APContact::getByURL($actor); - if (empty($apcontact) || !in_array($apcontact['type'], ['Application', 'Service'])) { - $this->out($actor . ' is no relay actor'); + if (empty($apcontact)) { + $this->out($actor . ' wasn\'t found'); return 1; } if ($mode == 'add') { + if (!APContact::isRelay($apcontact)) { + $this->out($actor . ' is no relay actor'); + return 1; + } + if (Transmitter::sendRelayFollow($actor)) { $this->out('Successfully added ' . $actor); } else { diff --git a/src/Console/Relocate.php b/src/Console/Relocate.php index 22de2903c5..9e3682076f 100644 --- a/src/Console/Relocate.php +++ b/src/Console/Relocate.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/ServerBlock.php b/src/Console/ServerBlock.php index 4d04dcff81..f7a7963db1 100644 --- a/src/Console/ServerBlock.php +++ b/src/Console/ServerBlock.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/Storage.php b/src/Console/Storage.php index ea9f5ae20d..58d20d06b2 100644 --- a/src/Console/Storage.php +++ b/src/Console/Storage.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; @@ -25,6 +11,9 @@ use Asika\SimpleConsole\CommandArgsException; use Friendica\Core\Storage\Repository\StorageManager; use Friendica\Core\Storage\Exception\ReferenceStorageException; use Friendica\Core\Storage\Exception\StorageException; +use Friendica\Database\DBA; +use Friendica\Model\Contact; +use Friendica\Model\Photo; /** * tool to manage storage backend and stored data from CLI @@ -53,14 +42,17 @@ console storage - manage storage backend and stored data Synopsis bin/console storage [-h|--help|-?] [-v] Show this help - + bin/console storage list List available storage backends - + + bin/console storage clear + Remove the contact avatar cache data + bin/console storage set Set current storage backend name storage backend to use. see "list". - + bin/console storage move [table] [-n 5000] Move stored data to current storage backend. table one of "photo" or "attach". default to both @@ -87,6 +79,9 @@ HELP; case 'list': return $this->doList(); break; + case 'clear': + return $this->clear(); + break; case 'set': return $this->doSet(); break; @@ -101,7 +96,7 @@ HELP; protected function doList() { - $rowfmt = ' %-3s | %-20s'; + $rowfmt = ' %-3s | %-20s'; $current = $this->storageManager->getBackend(); $this->out(sprintf($rowfmt, 'Sel', 'Name')); $this->out('-----------------------'); @@ -109,7 +104,7 @@ HELP; foreach ($this->storageManager->listBackends() as $name) { $issel = ' '; if ($current && $current::getName() == $name) { - $issel = '*'; + $issel = '*'; $isregisterd = true; }; $this->out(sprintf($rowfmt, $issel, $name)); @@ -126,6 +121,22 @@ HELP; return 0; } + protected function clear() + { + $fields = ['photo' => '', 'thumb' => '', 'micro' => '']; + $photos = DBA::select('photo', ['id', 'contact-id'], ['uid' => 0, 'photo-type' => [Photo::CONTACT_AVATAR, Photo::CONTACT_BANNER]]); + while ($photo = DBA::fetch($photos)) { + if (Photo::delete(['id' => $photo['id']])) { + Contact::update($fields, ['id' => $photo['contact-id']]); + $this->out('Cleared photo id ' . $photo['id'] . ' - contact id ' . $photo['contact-id']); + } else { + $this->out('Photo id ' . $photo['id'] . ' was not deleted.'); + } + } + DBA::close($photos); + return 0; + } + protected function doSet() { if (count($this->args) !== 2 || empty($this->args[1])) { @@ -165,7 +176,7 @@ HELP; } $current = $this->storageManager->getBackend(); - $total = 0; + $total = 0; if (is_null($current)) { throw new StorageException(sprintf("Cannot move to legacy storage. Please select a storage backend.")); @@ -181,5 +192,7 @@ HELP; } while ($moved); $this->out(sprintf(date('[Y-m-d H:i:s] ') . 'Moved %d files total', $total)); + + return 0; } } diff --git a/src/Console/Test.php b/src/Console/Test.php index bb81ef97be..cd40b74cf8 100644 --- a/src/Console/Test.php +++ b/src/Console/Test.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/Typo.php b/src/Console/Typo.php index a3e9b77050..ed314197ea 100644 --- a/src/Console/Typo.php +++ b/src/Console/Typo.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; diff --git a/src/Console/User.php b/src/Console/User.php index eb061ede31..5a8f62cc8c 100644 --- a/src/Console/User.php +++ b/src/Console/User.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Console; use Console_Table; -use Friendica\App; +use Friendica\App\Mode; use Friendica\Content\Pager; use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; @@ -40,7 +26,7 @@ class User extends \Asika\SimpleConsole\Console protected $helpOptions = ['h', 'help', '?']; /** - * @var App\Mode + * @var Mode */ private $appMode; /** @@ -58,7 +44,7 @@ class User extends \Asika\SimpleConsole\Console console user - Modify user settings per console commands. Usage bin/console user password [] [-h|--help|-?] [-v] - bin/console user add [ [ [ []]]] [-h|--help|-?] [-v] + bin/console user add [ [ [ [ []]]]] [-h|--help|-?] [-v] bin/console user delete [] [-y] [-h|--help|-?] [-v] bin/console user allow [] [-h|--help|-?] [-v] bin/console user deny [] [-h|--help|-?] [-v] @@ -88,7 +74,7 @@ HELP; return $help; } - public function __construct(App\Mode $appMode, L10n $l10n, IManagePersonalConfigValues $pConfig, array $argv = null) + public function __construct(Mode $appMode, L10n $l10n, IManagePersonalConfigValues $pConfig, array $argv = null) { parent::__construct($argv); @@ -120,23 +106,23 @@ HELP; case 'password': return $this->password(); case 'add': - return $this->addUser(); + return ($this->addUser()) ? 0 : 1; case 'allow': - return $this->pendingUser(true); + return ($this->pendingUser(true)) ? 0 : 1; case 'deny': - return $this->pendingUser(false); + return ($this->pendingUser(false)) ? 0 : 1; case 'block': - return $this->blockUser(true); + return ($this->blockUser(true)) ? 0 : 1; case 'unblock': - return $this->blockUser(false); + return ($this->blockUser(false)) ? 0 : 1; case 'delete': - return $this->deleteUser(); + return ($this->deleteUser()) ? 0 : 1; case 'list': - return $this->listUser(); + return ($this->listUser()) ? 0 : 1; case 'search': - return $this->searchUser(); + return ($this->searchUser()) ? 0 : 1; case 'config': - return $this->configUser(); + return ($this->configUser()) ? 0 : 1; default: throw new \Asika\SimpleConsole\CommandArgsException('Wrong command.'); } @@ -192,7 +178,7 @@ HELP; * * @throws \Exception */ - private function password() + private function password(): int { $user = $this->getUserByNick(1); @@ -226,12 +212,13 @@ HELP; * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private function addUser() + private function addUser(): bool { - $name = $this->getArgument(1); - $nick = $this->getArgument(2); - $email = $this->getArgument(3); - $lang = $this->getArgument(4); + $name = $this->getArgument(1); + $nick = $this->getArgument(2); + $email = $this->getArgument(3); + $lang = $this->getArgument(4); + $avatar = $this->getArgument(5); if (empty($name)) { $this->out($this->l10n->t('Enter user name: ')); @@ -262,10 +249,15 @@ HELP; $lang = CliPrompt::prompt(); } + if (empty($avatar)) { + $this->out($this->l10n->t('Enter URL of an image to use as avatar (optional): ')); + $avatar = CliPrompt::prompt(); + } + if (empty($lang)) { return UserModel::createMinimal($name, $email, $nick); } else { - return UserModel::createMinimal($name, $email, $nick, $lang); + return UserModel::createMinimal($name, $email, $nick, $lang, $avatar); } } @@ -520,5 +512,7 @@ HELP; $this->out($this->getHelp()); return false; } + + return true; } } diff --git a/src/Console/Worker.php b/src/Console/Worker.php new file mode 100644 index 0000000000..97b7160d03 --- /dev/null +++ b/src/Console/Worker.php @@ -0,0 +1,96 @@ +mode = $mode; + $this->basePath = $basePath; + $this->processRepo = $processRepo; + } + + protected function getHelp(): string + { + return <<mode->setExecutor(Mode::WORKER); + + // Check the database structure and possibly fixes it + Update::check($this->basePath->getPath(), true); + + // Quit when in maintenance + if (!$this->mode->has(Mode::MAINTENANCEDISABLED)) { + return; + } + + $spawn = $this->getOption(['s', 'spawn'], false); + + if ($spawn) { + CoreWorker::spawnWorker(); + exit(); + } + + $run_cron = !$this->getOption(['n', 'no_cron'], false); + + $process = $this->processRepo->create(getmypid(), 'worker.php'); + + CoreWorker::processQueue($run_cron, $process); + CoreWorker::unclaimProcess($process); + + $this->processRepo->delete($process); + } +} diff --git a/src/Contact/Avatar.php b/src/Contact/Avatar.php index fc4b7e38cb..677ab8e048 100644 --- a/src/Contact/Avatar.php +++ b/src/Contact/Avatar.php @@ -1,36 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact; -use Friendica\Core\Logger; use Friendica\DI; -use Friendica\Model\Item; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Object\Image; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPSignature; -use Friendica\Util\Images; -use Friendica\Util\Network; use Friendica\Util\Proxy; /** @@ -57,51 +39,56 @@ class Avatar return $fields; } - if (Network::isLocalLink($avatar) || empty($avatar)) { + if (DI::baseUrl()->isLocalUrl($avatar) || empty($avatar)) { self::deleteCache($contact); return $fields; } if (($avatar != $contact['avatar']) || $force) { self::deleteCache($contact); - Logger::debug('Avatar file name changed', ['new' => $avatar, 'old' => $contact['avatar']]); + DI::logger()->debug('Avatar file name changed', ['new' => $avatar, 'old' => $contact['avatar']]); } elseif (self::isCacheFile($contact['photo']) && self::isCacheFile($contact['thumb']) && self::isCacheFile($contact['micro'])) { $fields['photo'] = $contact['photo']; $fields['thumb'] = $contact['thumb']; $fields['micro'] = $contact['micro']; - Logger::debug('Using existing cache files', ['uri-id' => $contact['uri-id'], 'fields' => $fields]); + DI::logger()->debug('Using existing cache files', ['uri-id' => $contact['uri-id'], 'fields' => $fields]); return $fields; } try { $fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]); } catch (\Exception $exception) { - Logger::notice('Avatar is invalid', ['avatar' => $avatar, 'exception' => $exception]); + DI::logger()->notice('Avatar is invalid', ['avatar' => $avatar, 'exception' => $exception]); return $fields; } - $img_str = $fetchResult->getBody(); + if (!$fetchResult->isSuccess()) { + DI::logger()->debug('Fetching was unsuccessful', ['avatar' => $avatar]); + return $fields; + } + + $img_str = $fetchResult->getBodyString(); if (empty($img_str)) { - Logger::debug('Avatar is invalid', ['avatar' => $avatar]); + DI::logger()->debug('Avatar is invalid', ['avatar' => $avatar]); return $fields; } - $image = new Image($img_str, Images::getMimeTypeByData($img_str)); + $image = new Image($img_str, $fetchResult->getContentType(), $avatar); if (!$image->isValid()) { - Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]); + DI::logger()->debug('Avatar picture is invalid', ['avatar' => $avatar]); return $fields; } - $filename = self::getFilename($contact['url'], $avatar); + $filename = self::getFilename($contact['url']); $timestamp = time(); - $fields['blurhash'] = $image->getBlurHash(); + $fields['blurhash'] = $image->getBlurHash($img_str); $fields['photo'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_SMALL, $timestamp); $fields['thumb'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_THUMB, $timestamp); $fields['micro'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_MICRO, $timestamp); - Logger::debug('Storing new avatar cache', ['uri-id' => $contact['uri-id'], 'fields' => $fields]); + DI::logger()->debug('Storing new avatar cache', ['uri-id' => $contact['uri-id'], 'fields' => $fields]); return $fields; } @@ -115,12 +102,12 @@ class Avatar return $fields; } - if (Network::isLocalLink($contact['avatar']) || empty($contact['avatar'])) { + if (DI::baseUrl()->isLocalUrl($contact['avatar']) || empty($contact['avatar'])) { self::deleteCache($contact); return $fields; } - $filename = self::getFilename($contact['url'], $contact['avatar']); + $filename = self::getFilename($contact['url']); $timestamp = time(); $fields['photo'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_SMALL, $timestamp); @@ -130,12 +117,10 @@ class Avatar return $fields; } - private static function getFilename(string $url, string $host): string + private static function getFilename(string $url): string { - $guid = Item::guidFromUri($url, $host); - - return substr($guid, 0, 2) . '/' . substr($guid, 3, 2) . '/' . substr($guid, 5, 3) . '/' . - substr($guid, 9, 2) .'/' . substr($guid, 11, 2) . '/' . substr($guid, 13, 4). '/' . substr($guid, 18) . '-'; + $guid = hash('ripemd128', $url); + return substr($guid, 0, 3) . '/' . substr($guid, 4) . '-'; } private static function storeAvatarCache(Image $image, string $filename, int $size, int $timestamp): string @@ -145,7 +130,7 @@ class Avatar return ''; } - $path = $filename . $size . '.' . $image->getExt(); + $path = $filename . $size . $image->getExt(); $basepath = self::basePath(); if (empty($basepath)) { @@ -169,36 +154,36 @@ class Avatar if (!file_exists($dirpath)) { if (!@mkdir($dirpath, $dir_perm) && !file_exists($dirpath)) { - Logger::warning('Directory could not be created', ['directory' => $dirpath]); + DI::logger()->warning('Directory could not be created', ['directory' => $dirpath]); } } elseif ((($old_perm = fileperms($dirpath) & 0777) != $dir_perm) && !chmod($dirpath, $dir_perm)) { - Logger::warning('Directory permissions could not be changed', ['directory' => $dirpath, 'old' => $old_perm, 'new' => $dir_perm]); + DI::logger()->warning('Directory permissions could not be changed', ['directory' => $dirpath, 'old' => $old_perm, 'new' => $dir_perm]); } if ((($old_group = filegroup($dirpath)) != $group) && !chgrp($dirpath, $group)) { - Logger::warning('Directory group could not be changed', ['directory' => $dirpath, 'old' => $old_group, 'new' => $group]); + DI::logger()->warning('Directory group could not be changed', ['directory' => $dirpath, 'old' => $old_group, 'new' => $group]); } } if (!file_put_contents($filepath, $image->asString())) { - Logger::warning('File could not be created', ['file' => $filepath]); + DI::logger()->warning('File could not be created', ['file' => $filepath]); } $old_perm = fileperms($filepath) & 0666; $old_group = filegroup($filepath); if (($old_perm != $file_perm) && !chmod($filepath, $file_perm)) { - Logger::warning('File permissions could not be changed', ['file' => $filepath, 'old' => $old_perm, 'new' => $file_perm]); + DI::logger()->warning('File permissions could not be changed', ['file' => $filepath, 'old' => $old_perm, 'new' => $file_perm]); } if (($old_group != $group) && !chgrp($filepath, $group)) { - Logger::warning('File group could not be changed', ['file' => $filepath, 'old' => $old_group, 'new' => $group]); + DI::logger()->warning('File group could not be changed', ['file' => $filepath, 'old' => $old_group, 'new' => $group]); } DI::profiler()->stopRecording(); if (!file_exists($filepath)) { - Logger::warning('Avatar cache file could not be stored', ['file' => $filepath]); + DI::logger()->warning('Avatar cache file could not be stored', ['file' => $filepath]); return ''; } @@ -230,7 +215,7 @@ class Avatar } $avatarpath = parse_url(self::baseUrl(), PHP_URL_PATH); - $pos = strpos($parts['path'], $avatarpath); + $pos = strpos($parts['path'], $avatarpath); if ($pos !== 0) { return ''; } @@ -249,9 +234,6 @@ class Avatar /** * Delete locally cached avatar pictures of a contact - * - * @param string $avatar - * @return bool */ public static function deleteCache(array $contact): bool { @@ -274,7 +256,7 @@ class Avatar $localFile = self::getCacheFile($avatar); if (!empty($localFile)) { @unlink($localFile); - Logger::debug('Unlink avatar', ['avatar' => $avatar]); + DI::logger()->debug('Unlink avatar', ['avatar' => $avatar, 'local' => $localFile]); } } @@ -294,11 +276,11 @@ class Avatar if (!file_exists($basepath)) { // We only automatically create the folder when it is in the web root if (strpos($basepath, DI::basePath()) !== 0) { - Logger::warning('Base directory does not exist', ['directory' => $basepath]); + DI::logger()->warning('Base directory does not exist', ['directory' => $basepath]); return ''; } if (!mkdir($basepath, 0775)) { - Logger::warning('Base directory could not be created', ['directory' => $basepath]); + DI::logger()->warning('Base directory could not be created', ['directory' => $basepath]); return ''; } } @@ -311,7 +293,7 @@ class Avatar * * @return string */ - private static function baseUrl(): string + public static function baseUrl(): string { $baseurl = DI::config()->get('system', 'avatar_cache_url'); if (!empty($baseurl)) { diff --git a/src/Contact/FriendSuggest/Collection/FriendSuggests.php b/src/Contact/FriendSuggest/Collection/FriendSuggests.php index 8777087bc9..87ec2ebcc8 100644 --- a/src/Contact/FriendSuggest/Collection/FriendSuggests.php +++ b/src/Contact/FriendSuggest/Collection/FriendSuggests.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\FriendSuggest\Collection; diff --git a/src/Contact/FriendSuggest/Entity/FriendSuggest.php b/src/Contact/FriendSuggest/Entity/FriendSuggest.php index 7ff72e393f..67159a9ea0 100644 --- a/src/Contact/FriendSuggest/Entity/FriendSuggest.php +++ b/src/Contact/FriendSuggest/Entity/FriendSuggest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\FriendSuggest\Entity; @@ -33,7 +19,7 @@ use Friendica\BaseEntity; * @property-read string $request * @property-read string $photo * @property-read string $note - * @property-read \DateTime created + * @property-read \DateTime $created * @property-read int|null $id */ class FriendSuggest extends BaseEntity diff --git a/src/Contact/FriendSuggest/Exception/FriendSuggestNotFoundException.php b/src/Contact/FriendSuggest/Exception/FriendSuggestNotFoundException.php index 799e069df3..35662e7a6c 100644 --- a/src/Contact/FriendSuggest/Exception/FriendSuggestNotFoundException.php +++ b/src/Contact/FriendSuggest/Exception/FriendSuggestNotFoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\FriendSuggest\Exception; diff --git a/src/Contact/FriendSuggest/Exception/FriendSuggestPersistenceException.php b/src/Contact/FriendSuggest/Exception/FriendSuggestPersistenceException.php index 96787d35aa..03d15e46ce 100644 --- a/src/Contact/FriendSuggest/Exception/FriendSuggestPersistenceException.php +++ b/src/Contact/FriendSuggest/Exception/FriendSuggestPersistenceException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\FriendSuggest\Exception; diff --git a/src/Contact/FriendSuggest/Factory/FriendSuggest.php b/src/Contact/FriendSuggest/Factory/FriendSuggest.php index 8d6fdc9db0..5aebeb1f2e 100644 --- a/src/Contact/FriendSuggest/Factory/FriendSuggest.php +++ b/src/Contact/FriendSuggest/Factory/FriendSuggest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\FriendSuggest\Factory; diff --git a/src/Contact/FriendSuggest/Repository/FriendSuggest.php b/src/Contact/FriendSuggest/Repository/FriendSuggest.php index 59c387f8f3..833421c7f6 100644 --- a/src/Contact/FriendSuggest/Repository/FriendSuggest.php +++ b/src/Contact/FriendSuggest/Repository/FriendSuggest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\FriendSuggest\Repository; use Friendica\BaseRepository; -use Friendica\Contact\FriendSuggest\Collection; -use Friendica\Contact\FriendSuggest\Entity; +use Friendica\Contact\FriendSuggest\Collection\FriendSuggests as FriendSuggestsCollection; +use Friendica\Contact\FriendSuggest\Entity\FriendSuggest as FriendSuggestEntity; use Friendica\Contact\FriendSuggest\Exception\FriendSuggestNotFoundException; use Friendica\Contact\FriendSuggest\Exception\FriendSuggestPersistenceException; -use Friendica\Contact\FriendSuggest\Factory; +use Friendica\Contact\FriendSuggest\Factory\FriendSuggest as FriendSuggestFactory; use Friendica\Database\Database; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Util\DateTimeFormat; @@ -34,17 +20,17 @@ use Psr\Log\LoggerInterface; class FriendSuggest extends BaseRepository { - /** @var Factory\FriendSuggest */ + /** @var FriendSuggestFactory */ protected $factory; protected static $table_name = 'fsuggest'; - public function __construct(Database $database, LoggerInterface $logger, Factory\FriendSuggest $factory) + public function __construct(Database $database, LoggerInterface $logger, FriendSuggestFactory $factory) { parent::__construct($database, $logger, $factory); } - private function convertToTableRow(Entity\FriendSuggest $fsuggest): array + private function convertToTableRow(FriendSuggestEntity $fsuggest): array { return [ 'uid' => $fsuggest->uid, @@ -59,39 +45,27 @@ class FriendSuggest extends BaseRepository } /** - * @param array $condition - * @param array $params - * - * @return Entity\FriendSuggest - * * @throws NotFoundException The underlying exception if there's no FriendSuggest with the given conditions */ - private function selectOne(array $condition, array $params = []): Entity\FriendSuggest + private function selectOne(array $condition, array $params = []): FriendSuggestEntity { - return parent::_selectOne($condition, $params); + $fields = $this->_selectFirstRowAsArray($condition, $params); + + return $this->factory->createFromTableRow($fields); } /** - * @param array $condition - * @param array $params - * - * @return Collection\FriendSuggests - * * @throws \Exception */ - private function select(array $condition, array $params = []): Collection\FriendSuggests + private function select(array $condition, array $params = []): FriendSuggestsCollection { - return new Collection\FriendSuggests(parent::_select($condition, $params)->getArrayCopy()); + return new FriendSuggestsCollection(parent::_select($condition, $params)->getArrayCopy()); } /** - * @param int $id - * - * @return Entity\FriendSuggest - * * @throws FriendSuggestNotFoundException in case there's no suggestion for this id */ - public function selectOneById(int $id): Entity\FriendSuggest + public function selectOneById(int $id): FriendSuggestEntity { try { return $this->selectOne(['id' => $id]); @@ -101,13 +75,9 @@ class FriendSuggest extends BaseRepository } /** - * @param int $cid - * - * @return Collection\FriendSuggests - * * @throws FriendSuggestPersistenceException In case the underlying storage cannot select the suggestion */ - public function selectForContact(int $cid): Collection\FriendSuggests + public function selectForContact(int $cid): FriendSuggestsCollection { try { return $this->select(['cid' => $cid]); @@ -117,13 +87,9 @@ class FriendSuggest extends BaseRepository } /** - * @param Entity\FriendSuggest $fsuggest - * - * @return Entity\FriendSuggest - * * @throws FriendSuggestNotFoundException in case the underlying storage cannot save the suggestion */ - public function save(Entity\FriendSuggest $fsuggest): Entity\FriendSuggest + public function save(FriendSuggestEntity $fsuggest): FriendSuggestEntity { try { $fields = $this->convertToTableRow($fsuggest); @@ -141,13 +107,9 @@ class FriendSuggest extends BaseRepository } /** - * @param Collection\FriendSuggests $fsuggests - * - * @return bool - * * @throws FriendSuggestNotFoundException in case the underlying storage cannot delete the suggestion */ - public function delete(Collection\FriendSuggests $fsuggests): bool + public function delete(FriendSuggestsCollection $fsuggests): bool { try { $ids = $fsuggests->column('id'); diff --git a/src/Contact/Header.php b/src/Contact/Header.php index d5116d2907..2415d4cb43 100644 --- a/src/Contact/Header.php +++ b/src/Contact/Header.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact; diff --git a/src/Contact/Introduction/Collection/Introductions.php b/src/Contact/Introduction/Collection/Introductions.php index 884f5ab23a..ee7d9c469c 100644 --- a/src/Contact/Introduction/Collection/Introductions.php +++ b/src/Contact/Introduction/Collection/Introductions.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\Introduction\Collection; diff --git a/src/Contact/Introduction/Entity/Introduction.php b/src/Contact/Introduction/Entity/Introduction.php index 5bc000f054..3d71b3247d 100644 --- a/src/Contact/Introduction/Entity/Introduction.php +++ b/src/Contact/Introduction/Entity/Introduction.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\Introduction\Entity; diff --git a/src/Contact/Introduction/Exception/IntroductionNotFoundException.php b/src/Contact/Introduction/Exception/IntroductionNotFoundException.php index 124b92d957..9b55505f69 100644 --- a/src/Contact/Introduction/Exception/IntroductionNotFoundException.php +++ b/src/Contact/Introduction/Exception/IntroductionNotFoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\Introduction\Exception; diff --git a/src/Contact/Introduction/Exception/IntroductionPersistenceException.php b/src/Contact/Introduction/Exception/IntroductionPersistenceException.php index 03c4374fae..d39aceb284 100644 --- a/src/Contact/Introduction/Exception/IntroductionPersistenceException.php +++ b/src/Contact/Introduction/Exception/IntroductionPersistenceException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\Introduction\Exception; diff --git a/src/Contact/Introduction/Factory/Introduction.php b/src/Contact/Introduction/Factory/Introduction.php index ee3bd5c371..3474bd0c58 100644 --- a/src/Contact/Introduction/Factory/Introduction.php +++ b/src/Contact/Introduction/Factory/Introduction.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\Introduction\Factory; diff --git a/src/Contact/Introduction/Repository/Introduction.php b/src/Contact/Introduction/Repository/Introduction.php index f61de3055a..a624017a2a 100644 --- a/src/Contact/Introduction/Repository/Introduction.php +++ b/src/Contact/Introduction/Repository/Introduction.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\Introduction\Repository; use Friendica\BaseRepository; use Friendica\Contact\Introduction\Exception\IntroductionNotFoundException; use Friendica\Contact\Introduction\Exception\IntroductionPersistenceException; -use Friendica\Contact\Introduction\Collection; -use Friendica\Contact\Introduction\Entity; -use Friendica\Contact\Introduction\Factory; +use Friendica\Contact\Introduction\Collection\Introductions as IntroductionsCollection; +use Friendica\Contact\Introduction\Entity\Introduction as IntroductionEntity; +use Friendica\Contact\Introduction\Factory\Introduction as IntroductionFactory; use Friendica\Database\Database; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Util\DateTimeFormat; @@ -34,37 +20,30 @@ use Psr\Log\LoggerInterface; class Introduction extends BaseRepository { - /** @var Factory\Introduction */ + /** @var IntroductionFactory */ protected $factory; protected static $table_name = 'intro'; - public function __construct(Database $database, LoggerInterface $logger, Factory\Introduction $factory) + public function __construct(Database $database, LoggerInterface $logger, IntroductionFactory $factory) { parent::__construct($database, $logger, $factory); } /** - * @param array $condition - * @param array $params - * - * @return Entity\Introduction - * * @throws NotFoundException the underlying exception if there's no Introduction with the given conditions */ - private function selectOne(array $condition, array $params = []): Entity\Introduction + private function selectOne(array $condition, array $params = []): IntroductionEntity { - return parent::_selectOne($condition, $params); + $fields = $this->_selectFirstRowAsArray( $condition, $params); + + return $this->factory->createFromTableRow($fields); } /** * Converts a given Introduction into a DB compatible row array - * - * @param Entity\Introduction $introduction - * - * @return array */ - protected function convertToTableRow(Entity\Introduction $introduction): array + protected function convertToTableRow(IntroductionEntity $introduction): array { return [ 'uid' => $introduction->uid, @@ -79,14 +58,9 @@ class Introduction extends BaseRepository } /** - * @param int $id - * @param int $uid - * - * @return Entity\Introduction - * * @throws IntroductionNotFoundException in case there is no Introduction with this id */ - public function selectOneById(int $id, int $uid): Entity\Introduction + public function selectOneById(int $id, int $uid): IntroductionEntity { try { return $this->selectOne(['id' => $id, 'uid' => $uid]); @@ -102,33 +76,30 @@ class Introduction extends BaseRepository * @param int|null $min_id * @param int|null $max_id * @param int $limit - * - * @return Collection\Introductions */ - public function selectForUser(int $uid, int $min_id = null, int $max_id = null, int $limit = self::LIMIT): Collection\Introductions + public function selectForUser(int $uid, ?int $min_id = null, ?int $max_id = null, int $limit = self::LIMIT): IntroductionsCollection { try { $BaseCollection = parent::_selectByBoundaries( ['`uid` = ? AND NOT `ignore`',$uid], ['order' => ['id' => 'DESC']], - $min_id, $max_id, $limit); + $min_id, + $max_id, + $limit + ); } catch (\Exception $e) { throw new IntroductionPersistenceException(sprintf('Cannot select Introductions for used %d', $uid), $e); } - return new Collection\Introductions($BaseCollection->getArrayCopy(), $BaseCollection->getTotalCount()); + return new IntroductionsCollection($BaseCollection->getArrayCopy(), $BaseCollection->getTotalCount()); } /** * Selects the introduction for a given contact * - * @param int $cid - * - * @return Entity\Introduction - * * @throws IntroductionNotFoundException in case there is not Introduction for this contact */ - public function selectForContact(int $cid): Entity\Introduction + public function selectForContact(int $cid): IntroductionEntity { try { return $this->selectOne(['contact-id' => $cid]); @@ -164,13 +135,9 @@ class Introduction extends BaseRepository } /** - * @param Entity\Introduction $introduction - * - * @return bool - * * @throws IntroductionPersistenceException in case the underlying storage cannot delete the Introduction */ - public function delete(Entity\Introduction $introduction): bool + public function delete(IntroductionEntity $introduction): bool { if (!$introduction->id) { return false; @@ -184,13 +151,9 @@ class Introduction extends BaseRepository } /** - * @param Entity\Introduction $introduction - * - * @return Entity\Introduction - * * @throws IntroductionPersistenceException In case the underlying storage cannot save the Introduction */ - public function save(Entity\Introduction $introduction): Entity\Introduction + public function save(IntroductionEntity $introduction): IntroductionEntity { try { $fields = $this->convertToTableRow($introduction); diff --git a/src/Contact/LocalRelationship/Entity/LocalRelationship.php b/src/Contact/LocalRelationship/Entity/LocalRelationship.php index 4fc7ae3660..2fa51e0085 100644 --- a/src/Contact/LocalRelationship/Entity/LocalRelationship.php +++ b/src/Contact/LocalRelationship/Entity/LocalRelationship.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\LocalRelationship\Entity; @@ -39,7 +25,6 @@ use Friendica\Model\Contact; * @property-read int $remoteSelf * @property-read int $fetchFurtherInformation * @property-read string $ffiKeywordDenylist - * @property-read bool $subhub * @property-read string $hubVerify * @property-read string $protocol * @property-read int $rating @@ -83,8 +68,6 @@ class LocalRelationship extends \Friendica\BaseEntity protected $fetchFurtherInformation; /** @var string */ protected $ffiKeywordDenylist; - /** @var bool */ - protected $subhub; /** @var string */ protected $hubVerify; /** @var string */ @@ -94,7 +77,7 @@ class LocalRelationship extends \Friendica\BaseEntity /** @var int */ protected $priority; - public function __construct(int $userId, int $contactId, bool $blocked = false, bool $ignored = false, bool $collapsed = false, bool $hidden = false, bool $pending = false, int $rel = Contact::NOTHING, string $info = '', bool $notifyNewPosts = false, int $remoteSelf = self::MIRROR_DEACTIVATED, int $fetchFurtherInformation = self::FFI_NONE, string $ffiKeywordDenylist = '', bool $subhub = false, string $hubVerify = '', string $protocol = Protocol::PHANTOM, ?int $rating = null, ?int $priority = null) + public function __construct(int $userId, int $contactId, bool $blocked = false, bool $ignored = false, bool $collapsed = false, bool $hidden = false, bool $pending = false, int $rel = Contact::NOTHING, string $info = '', bool $notifyNewPosts = false, int $remoteSelf = self::MIRROR_DEACTIVATED, int $fetchFurtherInformation = self::FFI_NONE, string $ffiKeywordDenylist = '', string $hubVerify = '', string $protocol = Protocol::PHANTOM, ?int $rating = null, ?int $priority = null) { $this->userId = $userId; $this->contactId = $contactId; @@ -109,7 +92,6 @@ class LocalRelationship extends \Friendica\BaseEntity $this->remoteSelf = $remoteSelf; $this->fetchFurtherInformation = $fetchFurtherInformation; $this->ffiKeywordDenylist = $ffiKeywordDenylist; - $this->subhub = $subhub; $this->hubVerify = $hubVerify; $this->protocol = $protocol; $this->rating = $rating; diff --git a/src/Contact/LocalRelationship/Exception/LocalRelationshipPersistenceException.php b/src/Contact/LocalRelationship/Exception/LocalRelationshipPersistenceException.php index d23688d7fe..38d1ec7b91 100644 --- a/src/Contact/LocalRelationship/Exception/LocalRelationshipPersistenceException.php +++ b/src/Contact/LocalRelationship/Exception/LocalRelationshipPersistenceException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\LocalRelationship\Exception; diff --git a/src/Contact/LocalRelationship/Factory/LocalRelationship.php b/src/Contact/LocalRelationship/Factory/LocalRelationship.php index 54fc86215c..1cdc308ed4 100644 --- a/src/Contact/LocalRelationship/Factory/LocalRelationship.php +++ b/src/Contact/LocalRelationship/Factory/LocalRelationship.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\LocalRelationship\Factory; @@ -48,7 +34,6 @@ class LocalRelationship extends BaseFactory implements ICanCreateFromTableRow $row['remote_self'] ?? Entity\LocalRelationship::MIRROR_DEACTIVATED, $row['fetch_further_information'] ?? Entity\LocalRelationship::FFI_NONE, $row['ffi_keyword_denylist'] ?? '', - $row['subhub'] ?? false, $row['hub-verify'] ?? '', $row['protocol'] ?? Protocol::PHANTOM, $row['rating'] ?? null, diff --git a/src/Contact/LocalRelationship/Repository/LocalRelationship.php b/src/Contact/LocalRelationship/Repository/LocalRelationship.php index 490a84e44d..d1a87d5569 100644 --- a/src/Contact/LocalRelationship/Repository/LocalRelationship.php +++ b/src/Contact/LocalRelationship/Repository/LocalRelationship.php @@ -1,79 +1,60 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Contact\LocalRelationship\Repository; -use Friendica\Contact\LocalRelationship\Entity; -use Friendica\Contact\LocalRelationship\Exception; -use Friendica\Contact\LocalRelationship\Factory; +use Exception; +use Friendica\BaseRepository; +use Friendica\Contact\LocalRelationship\Entity\LocalRelationship as LocalRelationshipEntity; +use Friendica\Contact\LocalRelationship\Exception\LocalRelationshipPersistenceException; +use Friendica\Contact\LocalRelationship\Factory\LocalRelationship as LocalRelationshipFactory; use Friendica\Database\Database; -use Friendica\Network\HTTPException; +use Friendica\Network\HTTPException\NotFoundException; use Psr\Log\LoggerInterface; -class LocalRelationship extends \Friendica\BaseRepository +class LocalRelationship extends BaseRepository { protected static $table_name = 'user-contact'; - /** @var Factory\LocalRelationship */ + /** @var LocalRelationshipFactory */ protected $factory; - public function __construct(Database $database, LoggerInterface $logger, Factory\LocalRelationship $factory) + public function __construct(Database $database, LoggerInterface $logger, LocalRelationshipFactory $factory) { parent::__construct($database, $logger, $factory); } /** - * @param int $uid - * @param int $cid - * @return Entity\LocalRelationship - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function selectForUserContact(int $uid, int $cid): Entity\LocalRelationship + public function selectForUserContact(int $uid, int $cid): LocalRelationshipEntity { - return $this->_selectOne(['uid' => $uid, 'cid' => $cid]); + $fields = $this->_selectFirstRowAsArray(['uid' => $uid, 'cid' => $cid]); + + return $this->factory->createFromTableRow($fields); } /** * Returns the existing local relationship between a user and a public contact or a default * relationship if it doesn't. * - * @param int $uid - * @param int $cid - * @return Entity\LocalRelationship - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function getForUserContact(int $uid, int $cid): Entity\LocalRelationship + public function getForUserContact(int $uid, int $cid): LocalRelationshipEntity { try { return $this->selectForUserContact($uid, $cid); - } catch (HTTPException\NotFoundException $e) { + } catch (NotFoundException $e) { return $this->factory->createFromTableRow(['uid' => $uid, 'cid' => $cid]); } } /** - * @param int $uid - * @param int $cid - * @return bool - * @throws \Exception + * @throws Exception */ public function existsForUserContact(int $uid, int $cid): bool { @@ -82,12 +63,8 @@ class LocalRelationship extends \Friendica\BaseRepository /** * Converts a given local relationship into a DB compatible row array - * - * @param Entity\LocalRelationship $localRelationship - * - * @return array */ - protected function convertToTableRow(Entity\LocalRelationship $localRelationship): array + protected function convertToTableRow(LocalRelationshipEntity $localRelationship): array { return [ 'uid' => $localRelationship->userId, @@ -103,7 +80,6 @@ class LocalRelationship extends \Friendica\BaseRepository 'remote_self' => $localRelationship->remoteSelf, 'fetch_further_information' => $localRelationship->fetchFurtherInformation, 'ffi_keyword_denylist' => $localRelationship->ffiKeywordDenylist, - 'subhub' => $localRelationship->subhub, 'hub-verify' => $localRelationship->hubVerify, 'protocol' => $localRelationship->protocol, 'rating' => $localRelationship->rating, @@ -112,13 +88,9 @@ class LocalRelationship extends \Friendica\BaseRepository } /** - * @param Entity\LocalRelationship $localRelationship - * - * @return Entity\LocalRelationship - * - * @throws Exception\LocalRelationshipPersistenceException In case the underlying storage cannot save the LocalRelationship + * @throws LocalRelationshipPersistenceException In case the underlying storage cannot save the LocalRelationship */ - public function save(Entity\LocalRelationship $localRelationship): Entity\LocalRelationship + public function save(LocalRelationshipEntity $localRelationship): LocalRelationshipEntity { try { $fields = $this->convertToTableRow($localRelationship); @@ -126,8 +98,8 @@ class LocalRelationship extends \Friendica\BaseRepository $this->db->insert(self::$table_name, $fields, Database::INSERT_UPDATE); return $localRelationship; - } catch (\Exception $exception) { - throw new Exception\LocalRelationshipPersistenceException(sprintf('Cannot insert/update the local relationship %d for user %d', $localRelationship->contactId, $localRelationship->userId), $exception); + } catch (Exception $exception) { + throw new LocalRelationshipPersistenceException(sprintf('Cannot insert/update the local relationship %d for user %d', $localRelationship->contactId, $localRelationship->userId), $exception); } } } diff --git a/src/Content/BoundariesPager.php b/src/Content/BoundariesPager.php index 6ab64fdef8..6522f6a374 100644 --- a/src/Content/BoundariesPager.php +++ b/src/Content/BoundariesPager.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Content; use Friendica\Core\L10n; use Friendica\Core\Renderer; -use Friendica\Util\Network; use Friendica\Util\Strings; +use GuzzleHttp\Psr7\Uri; /** * This pager should be used by lists using the min_id†/max_id† parameters @@ -67,7 +53,7 @@ class BoundariesPager extends Pager $parsed['query'] = http_build_query($queryParameters); - $url = Network::unparseURL($parsed); + $url = (string)Uri::fromParts((array)$parsed); $this->setQueryString($url); } diff --git a/src/Content/ContactSelector.php b/src/Content/ContactSelector.php index af41935a0e..c864f159bb 100644 --- a/src/Content/ContactSelector.php +++ b/src/Content/ContactSelector.php @@ -1,31 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Content; -use Friendica\Core\Hook; use Friendica\Core\Protocol; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Util\Network; +use Friendica\Event\ArrayFilterEvent; use Friendica\Util\Strings; /** @@ -33,8 +18,14 @@ use Friendica\Util\Strings; */ class ContactSelector { - static $serverdata = []; - static $server_url = []; + const SVG_DISABLED = -1; + const SVG_COLOR_BLACK = 0; + const SVG_BLACK = 1; + const SVG_COLOR_WHITE = 2; + const SVG_WHITE = 3; + + public static $serverdata = []; + public static $server_id = []; /** * @param string $current current @@ -44,7 +35,7 @@ class ContactSelector public static function pollInterval(string $current, bool $disabled = false): string { $dis = (($disabled) ? ' disabled="disabled" ' : ''); - $o = ''; + $o = ''; $o .= "'; } else { - $publish_tpl = Renderer::getMarkupTemplate('profile/publish.tpl'); + $publish_tpl = Renderer::getMarkupTemplate('profile/publish.tpl'); $profile_publish = Renderer::replaceMacros($publish_tpl, [ '$instance' => 'reg', '$pubdesc' => DI::l10n()->t('Include your profile in member directory?'), @@ -136,51 +133,55 @@ class Register extends BaseModule $tpl = Renderer::getMarkupTemplate('register.tpl'); - $arr = ['template' => $tpl]; + $hook_data = [ + 'template' => $tpl, + ]; - Hook::callAll('register_form', $arr); + $hook_data = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ACCOUNT_REGISTER_FORM, $hook_data), + )->getArray(); - $tpl = $arr['template']; + $tpl = $hook_data['template'] ?? $tpl; $o = Renderer::replaceMacros($tpl, [ - '$invitations' => DI::config()->get('system', 'invitation_only'), - '$permonly' => intval(DI::config()->get('config', 'register_policy')) === self::APPROVE, - '$permonlybox' => ['permonlybox', DI::l10n()->t('Note for the admin'), '', DI::l10n()->t('Leave a message for the admin, why you want to join this node'), DI::l10n()->t('Required')], - '$invite_desc' => DI::l10n()->t('Membership on this site is by invitation only.'), - '$invite_label' => DI::l10n()->t('Your invitation code: '), - '$invite_id' => $invite_id, - '$regtitle' => DI::l10n()->t('Registration'), - '$registertext' => BBCode::convertForUriId(User::getSystemUriId(), DI::config()->get('config', 'register_text', '')), - '$fillwith' => $fillwith, - '$fillext' => $fillext, - '$oidlabel' => $oidlabel, - '$openid' => $openid_url, - '$namelabel' => DI::l10n()->t('Your Display Name (as you would like it to be displayed on this system'), - '$addrlabel' => DI::l10n()->t('Your Email Address: (Initial information will be send there, so this has to be an existing address.)'), - '$addrlabel2' => DI::l10n()->t('Please repeat your e-mail address:'), - '$ask_password' => $ask_password, - '$password1' => ['password1', DI::l10n()->t('New Password:'), '', DI::l10n()->t('Leave empty for an auto generated password.')], - '$password2' => ['confirm', DI::l10n()->t('Confirm:'), '', ''], - '$nickdesc' => DI::l10n()->t('Choose a profile nickname. This must begin with a text character. Your profile address on this site will then be "nickname@%s".', DI::baseUrl()->getHost()), - '$nicklabel' => DI::l10n()->t('Choose a nickname: '), - '$photo' => $photo, - '$publish' => $profile_publish, - '$regbutt' => DI::l10n()->t('Register'), - '$username' => $username, - '$email' => $email, - '$nickname' => $nickname, - '$sitename' => DI::baseUrl()->getHost(), - '$importh' => DI::l10n()->t('Import'), - '$importt' => DI::l10n()->t('Import your profile to this friendica instance'), - '$showtoslink' => DI::config()->get('system', 'tosdisplay'), - '$tostext' => DI::l10n()->t('Terms of Service'), - '$showprivstatement' => DI::config()->get('system', 'tosprivstatement'), - '$privstatement'=> $this->tos->privacy_complete, - '$form_security_token' => BaseModule::getFormSecurityToken('register'), - '$explicit_content' => DI::config()->get('system', 'explicit_content', false), + '$invitations' => DI::config()->get('system', 'invitation_only'), + '$permonly' => self::getPolicy() === self::APPROVE, + '$permonlybox' => ['permonlybox', DI::l10n()->t('Note for the admin'), '', DI::l10n()->t('Leave a message for the admin, why you want to join this node'), DI::l10n()->t('Required')], + '$invite_desc' => DI::l10n()->t('Membership on this site is by invitation only.'), + '$invite_label' => DI::l10n()->t('Your invitation code: '), + '$invite_id' => $invite_id, + '$regtitle' => DI::l10n()->t('Registration'), + '$registertext' => BBCode::convertForUriId(User::getSystemUriId(), DI::config()->get('config', 'register_text', '')), + '$fillwith' => $fillwith, + '$fillext' => $fillext, + '$oidlabel' => $oidlabel, + '$openid' => $openid_url, + '$namelabel' => DI::l10n()->t('Your Display Name (as you would like it to be displayed on this system):'), + '$addrlabel' => DI::l10n()->t('Your Email Address (initial information will be sent there, so this must be a valid address):'), + '$addrlabel2' => DI::l10n()->t('Please repeat your e-mail address:'), + '$ask_password' => $ask_password, + '$password1' => ['password1', DI::l10n()->t('New Password:'), '', DI::l10n()->t('Leave empty for an auto generated password.')], + '$password2' => ['confirm', DI::l10n()->t('Confirm:'), '', ''], + '$nickdesc' => DI::l10n()->t('Choose a profile nickname. This must begin with a text character. Your profile address on this site will then be "nickname@%s".', DI::baseUrl()->getHost()), + '$nicklabel' => DI::l10n()->t('Choose a nickname: '), + '$photo' => $photo, + '$publish' => $profile_publish, + '$regbutt' => DI::l10n()->t('Register'), + '$username' => $username, + '$email' => $email, + '$nickname' => $nickname, + '$sitename' => DI::baseUrl()->getHost(), + '$importh' => DI::l10n()->t('Import'), + '$importt' => DI::l10n()->t('Import your profile to this friendica instance'), + '$showtoslink' => DI::config()->get('system', 'tosdisplay'), + '$tostext' => DI::l10n()->t('Terms of Service'), + '$showprivstatement' => DI::config()->get('system', 'tosprivstatement'), + '$privstatement' => $this->tos->privacy_complete, + '$form_security_token' => BaseModule::getFormSecurityToken('register'), + '$explicit_content' => DI::config()->get('system', 'explicit_content', false), '$explicit_content_note' => DI::l10n()->t('Note: This node explicitly contains adult content'), - '$additional' => !empty(DI::userSession()->getLocalUserId()), - '$parent_password' => ['parent_password', DI::l10n()->t('Parent Password:'), '', DI::l10n()->t('Please enter the password of the parent account to legitimize your request.')] + '$additional' => !empty(DI::userSession()->getLocalUserId()), + '$parent_password' => ['parent_password', DI::l10n()->t('Parent Password:'), '', DI::l10n()->t('Please enter the password of the parent account to legitimize your request.')] ]); @@ -197,8 +198,13 @@ class Register extends BaseModule { BaseModule::checkFormSecurityTokenRedirectOnError('/register', 'register'); - $arr = ['post' => $_POST]; - Hook::callAll('register_post', $arr); + $arr = [ + 'post' => $_POST, + ]; + + $arr = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ACCOUNT_REGISTER_POST, $arr), + )->getArray(); $additional_account = false; @@ -228,24 +234,24 @@ class Register extends BaseModule } } - switch (DI::config()->get('config', 'register_policy')) { + switch (self::getPolicy()) { case self::OPEN: - $blocked = 0; + $blocked = 0; $verified = 1; break; case self::APPROVE: - $blocked = 1; + $blocked = 1; $verified = 0; break; case self::CLOSED: default: - if (empty($_SESSION['authenticated']) && empty($_SESSION['administrator'])) { + if (!$this->session->isSiteAdmin()) { DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.')); return; } - $blocked = 1; + $blocked = 1; $verified = 0; break; } @@ -256,7 +262,7 @@ class Register extends BaseModule // Is there text in the tar pit? if (!empty($arr['email'])) { - Logger::info('Tar pit', $arr); + $this->logger->info('Tar pit', $arr); DI::sysmsg()->addNotice(DI::l10n()->t('You have entered too much information.')); DI::baseUrl()->redirect('register/'); } @@ -268,24 +274,36 @@ class Register extends BaseModule DI::baseUrl()->redirect('register'); } - $blocked = 0; + $blocked = 0; $verified = 1; $arr['password1'] = $arr['confirm'] = $arr['parent_password']; - $arr['repeat'] = $arr['email'] = $user['email']; + $arr['repeat'] = $arr['email'] = $user['email']; } else { // Overwriting the "tar pit" field with the real one $arr['email'] = $arr['field1']; } if ($arr['email'] != $arr['repeat']) { - Logger::info('Mail mismatch', $arr); + $this->logger->info('Mail mismatch', $arr); DI::sysmsg()->addNotice(DI::l10n()->t('Please enter the identical mail address in the second field.')); $regdata = ['email' => $arr['email'], 'nickname' => $arr['nickname'], 'username' => $arr['username']]; DI::baseUrl()->redirect('register?' . http_build_query($regdata)); } - $arr['blocked'] = $blocked; + //Check if nickname contains only US-ASCII and do not start with a digit + if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $arr['nickname'])) { + if (is_numeric(substr($arr['nickname'], 0, 1))) { + DI::sysmsg()->addNotice(DI::l10n()->t("Nickname cannot start with a digit.")); + } else { + DI::sysmsg()->addNotice(DI::l10n()->t("Nickname can only contain US-ASCII characters.")); + } + $regdata = ['email' => $arr['email'], 'nickname' => $arr['nickname'], 'username' => $arr['username']]; + DI::baseUrl()->redirect('register?' . http_build_query($regdata)); + return; + } + + $arr['blocked'] = $blocked; $arr['verified'] = $verified; $arr['language'] = L10n::detectLanguage($_SERVER, $_GET, DI::config()->get('system', 'language')); @@ -300,7 +318,7 @@ class Register extends BaseModule $base_url = (string)DI::baseUrl(); - if ($netpublish && intval(DI::config()->get('config', 'register_policy')) !== self::APPROVE) { + if ($netpublish && self::getPolicy() !== self::APPROVE) { $url = $base_url . '/profile/' . $user['nickname']; Worker::add(Worker::PRIORITY_LOW, 'Directory', $url); } @@ -313,9 +331,9 @@ class Register extends BaseModule $using_invites = DI::config()->get('system', 'invitation_only'); $num_invites = DI::config()->get('system', 'number_invites'); - $invite_id = (!empty($_POST['invite_id']) ? trim($_POST['invite_id']) : ''); + $invite_id = (!empty($_POST['invite_id']) ? trim($_POST['invite_id']) : ''); - if (intval(DI::config()->get('config', 'register_policy')) === self::OPEN) { + if (self::getPolicy() === self::OPEN) { if ($using_invites && $invite_id) { Model\Register::deleteByHash($invite_id); DI::pConfig()->set($user['uid'], 'system', 'invites_remaining', $num_invites); @@ -339,9 +357,11 @@ class Register extends BaseModule DI::baseUrl()->redirect(); } else { DI::sysmsg()->addNotice( - DI::l10n()->t('Failed to send email message. Here your accout details:
                              login: %s
                              password: %s

                              You can change your password after login.', + DI::l10n()->t( + 'Failed to send email message. Here your accout details:
                              login: %s
                              password: %s

                              You can change your password after login.', $user['email'], - $result['password']) + $result['password'] + ) ); } } else { @@ -351,7 +371,7 @@ class Register extends BaseModule } DI::baseUrl()->redirect(); } - } elseif (intval(DI::config()->get('config', 'register_policy')) === self::APPROVE) { + } elseif (self::getPolicy() === self::APPROVE) { if (!User::getAdminEmailList()) { $this->logger->critical('Registration policy is set to APPROVE but no admin email address has been set in config.admin_email'); DI::sysmsg()->addNotice(DI::l10n()->t('Your registration can not be processed.')); @@ -414,4 +434,20 @@ class Register extends BaseModule ]); } } + public static function getPolicy(): int + { + $admins = User::getAdminList(['login_date']); + $days = DI::config()->get('system', 'admin_inactivity_limit'); + if ($days == 0 || empty($admins)) { + return intval(DI::config()->get('config', 'register_policy')); + } + + $inactive_since = DateTimeFormat::utc('now - ' . $days . ' day'); + foreach ($admins as $admin) { + if (strtotime($admin['login_date']) > strtotime($inactive_since)) { + return intval(DI::config()->get('config', 'register_policy')); + } + } + return self::CLOSED; + } } diff --git a/src/Module/Response.php b/src/Module/Response.php index 78db953bb4..87f2af3b66 100644 --- a/src/Module/Response.php +++ b/src/Module/Response.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; diff --git a/src/Module/RobotsTxt.php b/src/Module/RobotsTxt.php index 96a2451ad2..f9bb4308e6 100644 --- a/src/Module/RobotsTxt.php +++ b/src/Module/RobotsTxt.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; diff --git a/src/Module/Search/Acl.php b/src/Module/Search/Acl.php index 857e2fdf9f..410bae9a5e 100644 --- a/src/Module/Search/Acl.php +++ b/src/Module/Search/Acl.php @@ -1,42 +1,29 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Search; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; use Friendica\BaseModule; use Friendica\Content\Widget; -use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Protocol; use Friendica\Core\Search; use Friendica\Core\Session\Capability\IHandleUserSessions; -use Friendica\Core\System; use Friendica\Database\Database; use Friendica\Database\DBA; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; use Friendica\Model\Post; use Friendica\Module\Response; -use Friendica\Network\HTTPException; +use Friendica\Network\HTTPException\UnauthorizedException; use Friendica\Util\Profiler; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; /** @@ -58,19 +45,37 @@ class Acl extends BaseModule private $session; /** @var Database */ private $database; + private EventDispatcherInterface $eventDispatcher; - public function __construct(Database $database, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) - { + public function __construct( + Database $database, + IHandleUserSessions $session, + EventDispatcherInterface $eventDispatcher, + L10n $l10n, + BaseURL $baseUrl, + Arguments $args, + LoggerInterface $logger, + Profiler $profiler, + Response $response, + array $server, + array $parameters = [] + ) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->session = $session; - $this->database = $database; + $this->session = $session; + $this->database = $database; + $this->eventDispatcher = $eventDispatcher; + } + + protected function post(array $request = []) + { + $this->rawContent($request); } protected function rawContent(array $request = []) { if (!$this->session->getLocalUserId()) { - throw new HTTPException\UnauthorizedException($this->t('You must be logged in to use this module.')); + throw new UnauthorizedException($this->t('You must be logged in to use this module.')); } $type = $request['type'] ?? self::TYPE_MENTION_CONTACT_CIRCLE; @@ -113,9 +118,9 @@ class Acl extends BaseModule private function regularContactSearch(array $request, string $type): array { - $start = $request['start'] ?? 0; - $count = $request['count'] ?? 100; - $search = $request['search'] ?? ''; + $start = $request['start'] ?? 0; + $count = $request['count'] ?? 100; + $search = $request['search'] ?? ''; $conv_id = $request['conversation'] ?? null; // For use with jquery.textcomplete for private mail completion @@ -133,9 +138,9 @@ class Acl extends BaseModule $condition_circle = ["`uid` = ? AND NOT `deleted`", $this->session->getLocalUserId()]; if ($search != '') { - $sql_extra = "AND `name` LIKE '%%" . $this->database->escape($search) . "%%'"; - $condition = DBA::mergeConditions($condition, ["(`attag` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?)", - '%' . $search . '%', '%' . $search . '%', '%' . $search . '%']); + $sql_extra = "AND `name` LIKE '%%" . $this->database->escape($search) . "%%'"; + $condition = DBA::mergeConditions($condition, ["(`attag` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?)", + '%' . $search . '%', '%' . $search . '%', '%' . $search . '%']); $condition_circle = DBA::mergeConditions($condition_circle, ["`name` LIKE ?", '%' . $search . '%']); } @@ -150,31 +155,32 @@ class Acl extends BaseModule switch ($type) { case self::TYPE_MENTION_CONTACT_CIRCLE: - $condition = DBA::mergeConditions($condition, - ["NOT `self` AND NOT `blocked` AND `notify` != ? AND `network` != ?", '', Protocol::OSTATUS - ]); - break; - case self::TYPE_MENTION_CONTACT: - $condition = DBA::mergeConditions($condition, - ["NOT `self` AND NOT `blocked` AND `notify` != ?", '' - ]); + $condition = DBA::mergeConditions( + $condition, + ["NOT `self` AND NOT `blocked`", + ] + ); break; case self::TYPE_MENTION_GROUP: - $condition = DBA::mergeConditions($condition, - ["NOT `self` AND NOT `blocked` AND `notify` != ? AND `contact-type` = ?", '', Contact::TYPE_COMMUNITY - ]); + $condition = DBA::mergeConditions( + $condition, + ["NOT `self` AND NOT `blocked` AND (NOT `ap-posting-restricted` OR `ap-posting-restricted` IS NULL) AND `contact-type` = ?", Contact::TYPE_COMMUNITY + ] + ); break; case self::TYPE_PRIVATE_MESSAGE: - $condition = DBA::mergeConditions($condition, - ["NOT `self` AND NOT `blocked` AND `notify` != ? AND `network` IN (?, ?, ?)", '', Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA - ]); + $condition = DBA::mergeConditions( + $condition, + ["NOT `self` AND NOT `blocked` AND `network` IN (?, ?, ?)", Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA + ] + ); break; } - $contact_count = $this->database->count('contact', $condition); + $contact_count = $this->database->count('account-user-view', $condition); $resultTotal = $circle_count + $contact_count; @@ -184,7 +190,8 @@ class Acl extends BaseModule if ($type == self::TYPE_MENTION_CONTACT_CIRCLE || $type == self::TYPE_MENTION_CIRCLE) { /// @todo We should cache this query. // This can be done when we can delete cache entries via wildcard - $circles = $this->database->toArray($this->database->p("SELECT `circle`.`id`, `circle`.`name`, GROUP_CONCAT(DISTINCT `circle_member`.`contact-id` SEPARATOR ',') AS uids + $circles = $this->database->toArray($this->database->p( + "SELECT `circle`.`id`, `circle`.`name`, GROUP_CONCAT(DISTINCT `circle_member`.`contact-id` SEPARATOR ',') AS uids FROM `group` AS `circle` INNER JOIN `group_member` AS `circle_member` ON `circle_member`.`gid` = `circle`.`id` WHERE NOT `circle`.`deleted` AND `circle`.`uid` = ? @@ -215,7 +222,7 @@ class Acl extends BaseModule $contacts = []; if ($type != self::TYPE_MENTION_CIRCLE) { - $contacts = Contact::selectToArray([], $condition, ['order' => ['name']]); + $contacts = Contact::selectAccountToArray([], $condition, ['order' => ['name']]); } $groups = []; @@ -294,7 +301,7 @@ class Acl extends BaseModule $resultTotal += count($unknown_contacts); } - $results = [ + $hook_data = [ 'tot' => $resultTotal, 'start' => $start, 'count' => $count, @@ -305,13 +312,15 @@ class Acl extends BaseModule 'search' => $search, ]; - Hook::callAll('acl_lookup_end', $results); + $hook_data = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ACL_LOOKUP_END, $hook_data), + )->getArray(); $o = [ - 'tot' => $results['tot'], - 'start' => $results['start'], - 'count' => $results['count'], - 'items' => $results['items'], + 'tot' => $hook_data['tot'], + 'start' => $hook_data['start'], + 'count' => $hook_data['count'], + 'items' => $hook_data['items'], ]; $this->logger->info('ACL {action} - {subaction} - done', ['module' => 'acl', 'action' => 'content', 'subaction' => 'search', 'search' => $search, 'type' => $type, 'conversation' => $conv_id]); diff --git a/src/Module/Search/Directory.php b/src/Module/Search/Directory.php index b3231a9085..a8fa8b3117 100644 --- a/src/Module/Search/Directory.php +++ b/src/Module/Search/Directory.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Search; diff --git a/src/Module/Search/Filed.php b/src/Module/Search/Filed.php index 5f519b60c2..f07a6d5a48 100644 --- a/src/Module/Search/Filed.php +++ b/src/Module/Search/Filed.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Search; diff --git a/src/Module/Search/Index.php b/src/Module/Search/Index.php index f1d99bdf13..8d395ba880 100644 --- a/src/Module/Search/Index.php +++ b/src/Module/Search/Index.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Search; @@ -29,7 +15,6 @@ use Friendica\Content\Text\HTML; use Friendica\Content\Widget; use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\L10n; -use Friendica\Core\Logger; use Friendica\Core\Renderer; use Friendica\Core\Search; use Friendica\Database\DBA; @@ -74,12 +59,14 @@ class Index extends BaseSearch // 10 requests are "free", after the 11th only a call per minute is allowed $free_crawls = intval(DI::config()->get('system', 'free_crawls')); - if ($free_crawls == 0) + if ($free_crawls == 0) { $free_crawls = 10; + } $crawl_permit_period = intval(DI::config()->get('system', 'crawl_permit_period')); - if ($crawl_permit_period == 0) + if ($crawl_permit_period == 0) { $crawl_permit_period = 10; + } $remote = $this->remoteAddress; $result = DI::cache()->get('remote_search:' . $remote); @@ -102,16 +89,16 @@ class Index extends BaseSearch $tag = false; if (!empty($_GET['tag'])) { - $tag = true; + $tag = true; $search = '#' . trim(rawurldecode($_GET['tag'])); } // construct a wrapper for the search header $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('content_wrapper.tpl'), [ - 'name' => 'search-header', - '$title' => DI::l10n()->t('Search'), + 'name' => 'search-header', + '$title' => DI::l10n()->t('Search'), '$title_size' => 3, - '$content' => HTML::search($search, 'search-box', false) + '$content' => HTML::search($search, 'search-box', false) ]); if (!$search) { @@ -119,7 +106,7 @@ class Index extends BaseSearch } if (strpos($search, '#') === 0) { - $tag = true; + $tag = true; $search = substr($search, 1); } else { if (strpos($search, '@') === 0 || strpos($search, '!') === 0) { @@ -148,7 +135,7 @@ class Index extends BaseSearch // Don't perform a fulltext or tag search on search results that look like an URL // Tags don't look like an URL and the fulltext search does only work with natural words if (parse_url($search, PHP_URL_SCHEME) && parse_url($search, PHP_URL_HOST)) { - Logger::info('Skipping tag and fulltext search since the search looks like a URL.', ['q' => $search]); + $this->logger->info('Skipping tag and fulltext search since the search looks like a URL.', ['q' => $search]); DI::sysmsg()->addNotice(DI::l10n()->t('No results.')); return $o; } @@ -161,11 +148,19 @@ class Index extends BaseSearch // No items will be shown if the member has a blocked profile wall. if (DI::mode()->isMobile()) { - $itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network', - DI::config()->get('system', 'itemspage_network_mobile')); + $itemsPerPage = DI::pConfig()->get( + DI::userSession()->getLocalUserId(), + 'system', + 'itemspage_mobile_network', + DI::config()->get('system', 'itemspage_network_mobile') + ); } else { - $itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_network', - DI::config()->get('system', 'itemspage_network')); + $itemsPerPage = DI::pConfig()->get( + DI::userSession()->getLocalUserId(), + 'system', + 'itemspage_network', + DI::config()->get('system', 'itemspage_network') + ); } $last_uriid = isset($_GET['last_uriid']) ? intval($_GET['last_uriid']) : 0; @@ -173,20 +168,20 @@ class Index extends BaseSearch $pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage); if ($tag) { - Logger::info('Start tag search.', ['q' => $search, 'start' => $pager->getStart(), 'items' => $pager->getItemsPerPage(), 'last' => $last_uriid]); + $this->logger->info('Start tag search.', ['q' => $search, 'start' => $pager->getStart(), 'items' => $pager->getItemsPerPage(), 'last' => $last_uriid]); $uriids = Tag::getURIIdListByTag($search, DI::userSession()->getLocalUserId(), $pager->getStart(), $pager->getItemsPerPage(), $last_uriid); - $count = Tag::countByTag($search, DI::userSession()->getLocalUserId()); + $count = Tag::countByTag($search, DI::userSession()->getLocalUserId()); } else { - Logger::info('Start fulltext search.', ['q' => $search]); + $this->logger->info('Start fulltext search.', ['q' => $search]); $uriids = Post\Content::getURIIdListBySearch($search, DI::userSession()->getLocalUserId(), $pager->getStart(), $pager->getItemsPerPage(), $last_uriid); - $count = Post\Content::countBySearch($search, DI::userSession()->getLocalUserId()); + $count = Post\Content::countBySearch($search, DI::userSession()->getLocalUserId()); } if (!empty($uriids)) { $condition = ["(`uid` = ? OR (`uid` = ? AND NOT `global`))", 0, DI::userSession()->getLocalUserId()]; $condition = DBA::mergeConditions($condition, ['uri-id' => $uriids]); - $params = ['order' => ['uri-id' => true]]; - $items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), Item::DISPLAY_FIELDLIST, $condition, $params)); + $params = ['order' => ['uri-id' => true]]; + $items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), Item::DISPLAY_FIELDLIST, $condition, $params)); } if (empty($items)) { @@ -211,7 +206,7 @@ class Index extends BaseSearch '$title' => $title ]); - Logger::info('Start Conversation.', ['q' => $search]); + $this->logger->info('Start Conversation.', ['q' => $search]); $o .= DI::conversation()->render($items, Conversation::MODE_SEARCH, false, false, 'commented', DI::userSession()->getLocalUserId()); @@ -287,7 +282,7 @@ class Index extends BaseSearch */ private static function tryRedirectToPost(string $search) { - if (parse_url($search, PHP_URL_SCHEME) == '') { + if (!parse_url($search, PHP_URL_SCHEME) && !preg_match('=^[a-z]+://=', $search)) { return; } diff --git a/src/Module/Search/Saved.php b/src/Module/Search/Saved.php index efc74adf08..913ee06a13 100644 --- a/src/Module/Search/Saved.php +++ b/src/Module/Search/Saved.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Search; @@ -48,7 +34,11 @@ class Saved extends BaseModule $action = $this->args->get(2, 'none'); $search = trim(rawurldecode($_GET['term'] ?? '')); - $return_url = $_GET['return_url'] ?? Search::getSearchPath($search); + if (!empty($_GET['return_url'])) { + $return_url = hex2bin($_GET['return_url']); + } else { + $return_url = Search::getSearchPath($search); + } if (DI::userSession()->getLocalUserId() && $search) { switch ($action) { diff --git a/src/Module/Search/Tags.php b/src/Module/Search/Tags.php index 12196f8058..f7cd766672 100644 --- a/src/Module/Search/Tags.php +++ b/src/Module/Search/Tags.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Search; @@ -49,7 +35,7 @@ class Tags extends BaseModule $this->database = $database; } - protected function rawContent(array $request = []) + protected function post(array $request = []) { $tags = $request['s'] ?? ''; $perPage = intval($request['n'] ?? self::DEFAULT_ITEMS_PER_PAGE); diff --git a/src/Module/Security/Login.php b/src/Module/Security/Login.php index c5028ffc57..39fa9035ac 100644 --- a/src/Module/Security/Login.php +++ b/src/Module/Security/Login.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security; @@ -60,13 +46,17 @@ class Login extends BaseModule protected function content(array $request = []): string { - $return_path = $request['return_path'] ?? $this->session->pop('return_path', '') ; + if (!empty($request['return_authorize'])) { + $return_path = 'oauth/authorize?' . $request['return_authorize']; + } else { + $return_path = $request['return_path'] ?? $this->session->pop('return_path', '') ; + } if ($this->session->getLocalUserId()) { $this->baseUrl->redirect($return_path); } - return self::form($return_path, intval($this->config->get('config', 'register_policy')) !== \Friendica\Module\Register::CLOSED); + return self::form($return_path, \Friendica\Module\Register::getPolicy() !== \Friendica\Module\Register::CLOSED); } protected function post(array $request = []) @@ -86,7 +76,6 @@ class Login extends BaseModule if (!empty($request['auth-params']) && $request['auth-params'] === 'login') { $this->auth->withPassword( - DI::app(), trim($request['username']), trim($request['password']), !empty($request['remember']), @@ -118,7 +107,7 @@ class Login extends BaseModule } $reg = false; - if ($register && intval(DI::config()->get('config', 'register_policy')) !== Register::CLOSED) { + if ($register && Register::getPolicy() !== Register::CLOSED) { $reg = [ 'title' => DI::l10n()->t('Create a New Account'), 'desc' => DI::l10n()->t('Register'), diff --git a/src/Module/Security/Logout.php b/src/Module/Security/Logout.php index 180dcaad2a..d6b2a1b058 100644 --- a/src/Module/Security/Logout.php +++ b/src/Module/Security/Logout.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security; @@ -56,6 +42,11 @@ class Logout extends BaseModule $this->session = $session; } + protected function post(array $request = []) + { + // @todo check if POST is really used here + $this->rawContent($request); + } /** * Process logout requests @@ -64,7 +55,7 @@ class Logout extends BaseModule { $visitor_home = null; if ($this->session->getRemoteUserId()) { - $visitor_home = Profile::getMyURL(); + $visitor_home = $this->session->getMyUrl(); $this->cache->delete('zrlInit:' . $visitor_home); } diff --git a/src/Module/Security/OpenID.php b/src/Module/Security/OpenID.php index 3db063f28c..ab77e551ef 100644 --- a/src/Module/Security/OpenID.php +++ b/src/Module/Security/OpenID.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security; @@ -71,7 +57,7 @@ class OpenID extends BaseModule // successful OpenID login $session->remove('openid'); - DI::auth()->setForUser(DI::app(), $user, true, true); + DI::auth()->setForUser($user, true, true); $this->baseUrl->redirect(DI::session()->pop('return_path', '')); } @@ -83,10 +69,12 @@ class OpenID extends BaseModule // Detect the server URL $open_id_obj = new LightOpenID(DI::baseUrl()->getHost()); + /** @phpstan-ignore-next-line $openid->identity is private, but will be set via magic setter */ $open_id_obj->identity = $authId; + /** @phpstan-ignore-next-line $openid->identity is private, but will be retrieved via magic getter */ $session->set('openid_server', $open_id_obj->discover($open_id_obj->identity)); - if (intval(DI::config()->get('config', 'register_policy')) === \Friendica\Module\Register::CLOSED) { + if (\Friendica\Module\Register::getPolicy() === \Friendica\Module\Register::CLOSED) { DI::sysmsg()->addNotice($l10n->t('Account not found. Please login to your existing account to add the OpenID to it.')); } else { DI::sysmsg()->addNotice($l10n->t('Account not found. Please register a new account or login to your existing account to add the OpenID to it.')); diff --git a/src/Module/Security/PasswordTooLong.php b/src/Module/Security/PasswordTooLong.php index eeeae4084e..ea42f89f21 100644 --- a/src/Module/Security/PasswordTooLong.php +++ b/src/Module/Security/PasswordTooLong.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security; diff --git a/src/Module/Security/TwoFactor/Recovery.php b/src/Module/Security/TwoFactor/Recovery.php index 3ba8addf53..e93b446f1b 100644 --- a/src/Module/Security/TwoFactor/Recovery.php +++ b/src/Module/Security/TwoFactor/Recovery.php @@ -1,27 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security\TwoFactor; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; +use Friendica\AppHelper; use Friendica\BaseModule; use Friendica\Core\L10n; use Friendica\Core\Renderer; @@ -43,18 +31,18 @@ class Recovery extends BaseModule { /** @var IHandleUserSessions */ protected $session; - /** @var App */ - protected $app; + /** @var AppHelper */ + protected $appHelper; /** @var Authentication */ protected $auth; - public function __construct(App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, Authentication $auth, IHandleUserSessions $session, array $server, array $parameters = []) + public function __construct(AppHelper $appHelper, L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, Authentication $auth, IHandleUserSessions $session, array $server, array $parameters = []) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->app = $app; - $this->auth = $auth; - $this->session = $session; + $this->appHelper = $appHelper; + $this->auth = $auth; + $this->session = $session; } protected function post(array $request = []) @@ -73,7 +61,7 @@ class Recovery extends BaseModule $this->session->set('2fa', true); DI::sysmsg()->addInfo($this->t('Remaining recovery codes: %d', RecoveryCode::countValidForUser($this->session->getLocalUserId()))); - $this->auth->setForUser($this->app, User::getById($this->app->getLoggedInUserId()), true, true); + $this->auth->setForUser(User::getById($this->session->getLocalUserId()), true, true); $this->baseUrl->redirect($this->session->pop('return_path', '')); } else { diff --git a/src/Module/Security/TwoFactor/SignOut.php b/src/Module/Security/TwoFactor/SignOut.php index a62c427b03..33f807f3b7 100644 --- a/src/Module/Security/TwoFactor/SignOut.php +++ b/src/Module/Security/TwoFactor/SignOut.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security\TwoFactor; diff --git a/src/Module/Security/TwoFactor/Trust.php b/src/Module/Security/TwoFactor/Trust.php index 31bd3e4cc3..f5ce8c930f 100644 --- a/src/Module/Security/TwoFactor/Trust.php +++ b/src/Module/Security/TwoFactor/Trust.php @@ -1,27 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security\TwoFactor; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; +use Friendica\AppHelper; use Friendica\BaseModule; use Friendica\Core\L10n; use Friendica\Core\Renderer; @@ -47,8 +35,8 @@ use Psr\Log\LoggerInterface; */ class Trust extends BaseModule { - /** @var App */ - protected $app; + /** @var AppHelper */ + protected $appHelper; /** @var Authentication */ protected $auth; /** @var IHandleUserSessions */ @@ -60,11 +48,11 @@ class Trust extends BaseModule /** @var TwoFactor\Repository\TrustedBrowser */ protected $trustedBrowserRepository; - public function __construct(App $app, Authentication $auth, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, IHandleUserSessions $session, Cookie $cookie, TwoFactor\Factory\TrustedBrowser $trustedBrowserFactory, TwoFactor\Repository\TrustedBrowser $trustedBrowserRepository, Response $response, array $server, array $parameters = []) + public function __construct(AppHelper $appHelper, Authentication $auth, L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, IHandleUserSessions $session, Cookie $cookie, TwoFactor\Factory\TrustedBrowser $trustedBrowserFactory, TwoFactor\Repository\TrustedBrowser $trustedBrowserRepository, Response $response, array $server, array $parameters = []) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->app = $app; + $this->appHelper = $appHelper; $this->auth = $auth; $this->session = $session; $this->cookie = $cookie; @@ -102,13 +90,13 @@ class Trust extends BaseModule } try { - $this->auth->setForUser($this->app, User::getById($this->app->getLoggedInUserId()), true, true); + $this->auth->setForUser(User::getById($this->session->getLocalUserId()), true, true); $this->baseUrl->redirect($this->session->pop('return_path', '')); } catch (FoundException | TemporaryRedirectException | MovedPermanentlyException $e) { // exception wanted! throw $e; } catch (\Exception $e) { - $this->logger->warning('Unexpected error during authentication.', ['user' => $this->app->getLoggedInUserId(), 'exception' => $exception]); + $this->logger->warning('Unexpected error during authentication.', ['user' => $this->session->getLocalUserId(), 'exception' => $e]); } } } @@ -123,15 +111,15 @@ class Trust extends BaseModule try { $trustedBrowser = $this->trustedBrowserRepository->selectOneByHash($this->cookie->get('2fa_cookie_hash')); if (!$trustedBrowser->trusted) { - $this->auth->setForUser($this->app, User::getById($this->app->getLoggedInUserId()), true, true); + $this->auth->setForUser(User::getById($this->session->getLocalUserId()), true, true); $this->baseUrl->redirect($this->session->pop('return_path', '')); } } catch (TrustedBrowserNotFoundException $exception) { - $this->logger->notice('Trusted Browser of the cookie not found.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->app->getLoggedInUserId(), 'exception' => $exception]); + $this->logger->notice('Trusted Browser of the cookie not found.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->session->getLocalUserId(), 'exception' => $exception]); } catch (TrustedBrowserPersistenceException $exception) { - $this->logger->warning('Unexpected persistence exception.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->app->getLoggedInUserId(), 'exception' => $exception]); + $this->logger->warning('Unexpected persistence exception.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->session->getLocalUserId(), 'exception' => $exception]); } catch (\Exception $exception) { - $this->logger->warning('Unexpected exception.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->app->getLoggedInUserId(), 'exception' => $exception]); + $this->logger->warning('Unexpected exception.', ['cookie_hash' => $this->cookie->get('trusted'), 'uid' => $this->session->getLocalUserId(), 'exception' => $exception]); } } diff --git a/src/Module/Security/TwoFactor/Verify.php b/src/Module/Security/TwoFactor/Verify.php index 270913a6d8..7011ee00f0 100644 --- a/src/Module/Security/TwoFactor/Verify.php +++ b/src/Module/Security/TwoFactor/Verify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Security\TwoFactor; diff --git a/src/Module/Settings/Account.php b/src/Module/Settings/Account.php index 70d1bddc38..edbe541a14 100644 --- a/src/Module/Settings/Account.php +++ b/src/Module/Settings/Account.php @@ -1,29 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; use Exception; use Friendica\Core\ACL; -use Friendica\Core\Logger; use Friendica\Core\Renderer; use Friendica\Core\Search; use Friendica\Core\Worker; @@ -39,14 +24,13 @@ use Friendica\Module\BaseSettings; use Friendica\Network\HTTPException; use Friendica\Protocol\Activity; use Friendica\Protocol\Delivery; -use Friendica\Util\Network; use Friendica\Util\Temporal; class Account extends BaseSettings { protected function post(array $request = []) { - if (!DI::app()->isLoggedIn()) { + if (!DI::userSession()->isAuthenticated()) { throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.')); } @@ -54,9 +38,7 @@ class Account extends BaseSettings self::checkFormSecurityTokenRedirectOnError($redirectUrl, 'settings'); - $a = DI::app(); - - $user = User::getById($a->getLoggedInUserId()); + $user = User::getById($this->session->getLocalUserId()); if (!empty($request['password-submit'])) { $newpass = $request['password']; @@ -125,7 +107,7 @@ class Account extends BaseSettings } if (strlen($timezone) && $timezone != $user['timezone']) { - $a->setTimeZone($timezone); + DI::appHelper()->setTimeZone($timezone); } $fields = [ @@ -160,8 +142,6 @@ class Account extends BaseSettings $hidewall = !empty($request['hidewall']); $blockwall = empty($request['blockwall']); // this setting is inverted! $blocktags = empty($request['blocktags']); // this setting is inverted! - $unkmail = !empty($request['unkmail']); - $cntunkmail = intval($request['cntunkmail'] ?? 0); $def_gid = intval($request['circle-selection'] ?? 0); $aclFormatter = DI::aclFormatter(); @@ -185,8 +165,6 @@ class Account extends BaseSettings 'blockwall' => $blockwall, 'hidewall' => $hidewall, 'blocktags' => $blocktags, - 'unkmail' => $unkmail, - 'cntunkmail' => $cntunkmail, ]; $profile_fields = [ @@ -318,8 +296,10 @@ class Account extends BaseSettings $page_flags = User::PAGE_FLAGS_SOAPBOX; } elseif ($account_type == User::ACCOUNT_TYPE_NEWS && $page_flags != User::PAGE_FLAGS_SOAPBOX) { $page_flags = User::PAGE_FLAGS_SOAPBOX; - } elseif ($account_type == User::ACCOUNT_TYPE_COMMUNITY && !in_array($page_flags, [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP])) { + } elseif ($account_type == User::ACCOUNT_TYPE_COMMUNITY && !in_array($page_flags, [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP, User::PAGE_FLAGS_COMM_MAN])) { $page_flags = User::PAGE_FLAGS_COMMUNITY; + } elseif ($account_type == User::ACCOUNT_TYPE_RELAY && $page_flags != User::PAGE_FLAGS_SOAPBOX) { + $page_flags = User::PAGE_FLAGS_SOAPBOX; } $fields = [ @@ -332,40 +312,9 @@ class Account extends BaseSettings } User::setCommunityUserSettings(DI::userSession()->getLocalUserId()); - DI::baseUrl()->redirect($redirectUrl); - } - // Import Contacts from CSV file - if (!empty($request['importcontact-submit'])) { - if (isset($_FILES['importcontact-filename'])) { - // was there an error - if ($_FILES['importcontact-filename']['error'] > 0) { - Logger::notice('Contact CSV file upload error', ['error' => $_FILES['importcontact-filename']['error']]); - DI::sysmsg()->addNotice(DI::l10n()->t('Contact CSV file upload error')); - } else { - $csvArray = array_map('str_getcsv', file($_FILES['importcontact-filename']['tmp_name'])); - Logger::notice('Import started', ['lines' => count($csvArray)]); - // import contacts - foreach ($csvArray as $csvRow) { - // The 1st row may, or may not contain the headers of the table - // We expect the 1st field of the row to contain either the URL - // or the handle of the account, therefore we check for either - // "http" or "@" to be present in the string. - // All other fields from the row will be ignored - if ((strpos($csvRow[0], '@') !== false) || Network::isValidHttpUrl($csvRow[0])) { - Worker::add(Worker::PRIORITY_MEDIUM, 'AddContact', DI::userSession()->getLocalUserId(), trim($csvRow[0], '@')); - } else { - Logger::notice('Invalid account', ['url' => $csvRow[0]]); - } - } - Logger::notice('Import done'); - - DI::sysmsg()->addInfo(DI::l10n()->t('Importing Contacts done')); - // delete temp file - unlink($_FILES['importcontact-filename']['tmp_name']); - } - } else { - Logger::notice('Import triggered, but no import file was found.'); + if ($account_type == User::ACCOUNT_TYPE_RELAY) { + Profile::setResponsibleRelayContact(DI::userSession()->getLocalUserId()); } DI::baseUrl()->redirect($redirectUrl); @@ -394,13 +343,11 @@ class Account extends BaseSettings return ''; } - $a = DI::app(); - - $user = User::getById($a->getLoggedInUserId()); + $user = User::getById($this->session->getLocalUserId()); $username = $user['username']; $email = $user['email']; - $nickname = $a->getLoggedInUserNickname(); + $nickname = DI::userSession()->getLocalUserNickname(); $timezone = $user['timezone']; $language = $user['language']; $notify = $user['notify-flags']; @@ -408,8 +355,6 @@ class Account extends BaseSettings $openid = $user['openid']; $maxreq = $user['maxreq']; $expire = $user['expire'] ?: ''; - $unkmail = $user['unkmail']; - $cntunkmail = $user['cntunkmail']; $expire_items = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'expire', 'items', true); $expire_notes = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'expire', 'notes', true); @@ -417,18 +362,30 @@ class Account extends BaseSettings $expire_network_only = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'expire', 'network_only', false); if (!strlen($user['timezone'])) { - $timezone = $a->getTimeZone(); + $timezone = DI::appHelper()->getTimeZone(); } // Set the account type to "Community" when the page is a community page but the account type doesn't fit // This is only happening on the first visit after the update if ( - in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP]) + in_array($user['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_PRVGROUP, User::PAGE_FLAGS_COMM_MAN]) && $user['account-type'] != User::ACCOUNT_TYPE_COMMUNITY ) { $user['account-type'] = User::ACCOUNT_TYPE_COMMUNITY; } + if (!empty($user['parent-uid']) && DI::config()->get('system', 'allow_relay_channels')) { + $account_relay = [ + 'account-type', + DI::l10n()->t('Channel Relay'), + User::ACCOUNT_TYPE_RELAY, + DI::l10n()->t('Account for a service that automatically shares content based on user defined channels.'), + $user['account-type'] == User::ACCOUNT_TYPE_RELAY + ]; + } else { + $account_relay = null; + } + $pageset_tpl = Renderer::getMarkupTemplate('settings/pagetypes.tpl'); $pagetype = Renderer::replaceMacros($pageset_tpl, [ '$account_types' => DI::l10n()->t("Account Types"), @@ -439,6 +396,7 @@ class Account extends BaseSettings '$type_organisation' => User::ACCOUNT_TYPE_ORGANISATION, '$type_news' => User::ACCOUNT_TYPE_NEWS, '$type_community' => User::ACCOUNT_TYPE_COMMUNITY, + '$type_relay' => User::ACCOUNT_TYPE_RELAY, '$account_person' => [ 'account-type', DI::l10n()->t('Personal Page'), @@ -467,6 +425,7 @@ class Account extends BaseSettings DI::l10n()->t('Account for community discussions.'), $user['account-type'] == User::ACCOUNT_TYPE_COMMUNITY ], + '$account_relay' => $account_relay, '$page_normal' => [ 'page-flags', DI::l10n()->t('Normal Account Page'), @@ -488,6 +447,13 @@ class Account extends BaseSettings DI::l10n()->t('Automatically approves all contact requests.'), $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY ], + '$page_community_manually' => [ + 'page-flags', + DI::l10n()->t('Public Group - Restricted'), + User::PAGE_FLAGS_COMM_MAN, + DI::l10n()->t('Contact requests have to be manually approved.'), + $user['page-flags'] == User::PAGE_FLAGS_COMM_MAN + ], '$page_freelove' => [ 'page-flags', DI::l10n()->t('Automatic Friend Page'), @@ -571,12 +537,10 @@ class Account extends BaseSettings '$accessiblephotos' => ['accessible-photos', DI::l10n()->t('Make all posted pictures accessible'), DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'accessible-photos'), DI::l10n()->t("This option makes every posted picture accessible via the direct link. This is a workaround for the problem that most other networks can't handle permissions on pictures. Non public pictures still won't be visible for the public on your photo albums though.")], '$blockwall' => ['blockwall', DI::l10n()->t('Allow friends to post to your profile page?'), (intval($user['blockwall']) ? '0' : '1'), DI::l10n()->t('Your contacts may write posts on your profile wall. These posts will be distributed to your contacts')], '$blocktags' => ['blocktags', DI::l10n()->t('Allow friends to tag your posts?'), (intval($user['blocktags']) ? '0' : '1'), DI::l10n()->t('Your contacts can add additional tags to your posts.')], - '$unkmail' => ['unkmail', DI::l10n()->t('Permit unknown people to send you private mail?'), $unkmail, DI::l10n()->t('Friendica network users may send you private messages even if they are not in your contact list.')], - '$cntunkmail' => ['cntunkmail', DI::l10n()->t('Maximum private messages per day from unknown people:'), $cntunkmail, DI::l10n()->t("(to prevent spam abuse)")], '$circle_select' => Circle::getSelectorHTML(DI::userSession()->getLocalUserId(), $user['def_gid'], 'circle-selection', DI::l10n()->t('Default privacy circle for new contacts')), '$circle_select_group' => Circle::getSelectorHTML(DI::userSession()->getLocalUserId(), DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'default-group-gid', $user['def_gid']), 'circle-selection-group', DI::l10n()->t('Default privacy circle for new group contacts')), '$permissions' => DI::l10n()->t('Default Post Permissions'), - '$aclselect' => ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId()), + '$aclselect' => ACL::getFullSelectorHTML(DI::page(), $this->session->getLocalUserId()), '$expire' => [ 'label' => DI::l10n()->t('Expiration settings'), @@ -631,11 +595,6 @@ class Account extends BaseSettings '$h_descadvn' => DI::l10n()->t('Change the behaviour of this account for special situations'), '$pagetype' => $pagetype, - '$importcontact' => DI::l10n()->t('Import Contacts'), - '$importcontact_text' => DI::l10n()->t('Upload a CSV file that contains the handle of your followed accounts in the first column you exported from the old account.'), - '$importcontact_button' => DI::l10n()->t('Upload File'), - '$importcontact_maxsize' => DI::config()->get('system', 'max_csv_file_size', 30720), - '$relocate' => DI::l10n()->t('Relocate'), '$relocate_text' => DI::l10n()->t("If you have moved this profile from another server, and some of your contacts don't receive your updates, try pushing this button."), '$relocate_button' => DI::l10n()->t("Resend relocate message to contacts"), diff --git a/src/Module/Settings/Addons.php b/src/Module/Settings/Addons.php index b6ac406ba6..4212b72150 100644 --- a/src/Module/Settings/Addons.php +++ b/src/Module/Settings/Addons.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; diff --git a/src/Module/Settings/Channels.php b/src/Module/Settings/Channels.php index 5e45b2cbb4..0049dece27 100644 --- a/src/Module/Settings/Channels.php +++ b/src/Module/Settings/Channels.php @@ -1,52 +1,48 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; use Friendica\App; use Friendica\Content\Conversation\Factory; use Friendica\Content\Conversation\Repository\UserDefinedChannel; +use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; +use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Model\Circle; +use Friendica\Model\User; use Friendica\Module\BaseSettings; use Friendica\Module\Response; use Friendica\Network\HTTPException; use Friendica\Util\Profiler; +use Friendica\Util\Strings; use Psr\Log\LoggerInterface; class Channels extends BaseSettings { /** @var UserDefinedChannel */ private $channel; + /** @var IManagePersonalConfigValues */ + private $pConfig; /** @var Factory\UserDefinedChannel */ private $userDefinedChannel; + /** @var IManageConfigValues */ + private $config; - public function __construct(Factory\UserDefinedChannel $userDefinedChannel, UserDefinedChannel $channel, App\Page $page, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + public function __construct(Factory\UserDefinedChannel $userDefinedChannel, UserDefinedChannel $channel, App\Page $page, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, array $server, array $parameters = []) { parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); $this->userDefinedChannel = $userDefinedChannel; $this->channel = $channel; + $this->config = $config; + $this->pConfig = $pConfig; } protected function post(array $request = []) @@ -62,20 +58,30 @@ class Channels extends BaseSettings self::checkFormSecurityTokenRedirectOnError('/settings/channels', 'settings_channels'); + $channel_languages = User::getWantedLanguages($uid); + if (!empty($request['add_channel'])) { + if (!array_diff((array)$request['new_languages'], $channel_languages)) { + $request['new_languages'] = null; + } + $channel = $this->userDefinedChannel->createFromTableRow([ 'label' => $request['new_label'], 'description' => $request['new_description'], 'access-key' => substr(mb_strtolower($request['new_access_key']), 0, 1), 'uid' => $uid, 'circle' => (int)$request['new_circle'], - 'include-tags' => $this->cleanTags($request['new_include_tags']), - 'exclude-tags' => $this->cleanTags($request['new_exclude_tags']), + 'include-tags' => Strings::cleanTags($request['new_include_tags']), + 'exclude-tags' => Strings::cleanTags($request['new_exclude_tags']), + 'min-size' => $request['new_min_size'] != '' ? (int)$request['new_min_size'] : null, + 'max-size' => $request['new_max_size'] != '' ? (int)$request['new_max_size'] : null, 'full-text-search' => $request['new_text_search'], 'media-type' => ($request['new_image'] ? 1 : 0) | ($request['new_video'] ? 2 : 0) | ($request['new_audio'] ? 4 : 0), + 'languages' => $request['new_languages'], ]); $saved = $this->channel->save($channel); $this->logger->debug('New channel added', ['saved' => $saved]); + $this->enableTimeline($uid, $saved->code); return; } @@ -86,6 +92,10 @@ class Channels extends BaseSettings continue; } + if (!array_diff((array)$request['languages'][$id], $channel_languages) && (count((array)$request['languages'][$id]) == count($channel_languages))) { + $request['languages'][$id] = null; + } + $channel = $this->userDefinedChannel->createFromTableRow([ 'id' => $id, 'label' => $request['label'][$id], @@ -93,13 +103,18 @@ class Channels extends BaseSettings 'access-key' => substr(mb_strtolower($request['access_key'][$id]), 0, 1), 'uid' => $uid, 'circle' => (int)$request['circle'][$id], - 'include-tags' => $this->cleanTags($request['include_tags'][$id]), - 'exclude-tags' => $this->cleanTags($request['exclude_tags'][$id]), + 'include-tags' => Strings::cleanTags($request['include_tags'][$id]), + 'exclude-tags' => Strings::cleanTags($request['exclude_tags'][$id]), + 'min-size' => $request['min_size'][$id] != '' ? (int)$request['min_size'][$id] : null, + 'max-size' => $request['max_size'][$id] != '' ? (int)$request['max_size'][$id] : null, 'full-text-search' => $request['text_search'][$id], 'media-type' => ($request['image'][$id] ? 1 : 0) | ($request['video'][$id] ? 2 : 0) | ($request['audio'][$id] ? 4 : 0), + 'languages' => $request['languages'][$id], + 'publish' => $request['publish'][$id] ?? false, ]); $saved = $this->channel->save($channel); $this->logger->debug('Save channel', ['id' => $id, 'saved' => $saved]); + $this->enableTimeline($uid, $id); } } @@ -112,17 +127,33 @@ class Channels extends BaseSettings throw new HTTPException\ForbiddenException($this->t('Permission denied.')); } - $circles = [ - 0 => $this->l10n->t('Global Community'), - -3 => $this->l10n->t('Network'), - -1 => $this->l10n->t('Following'), - -2 => $this->l10n->t('Followers'), - ]; + $user = User::getById($uid, ['account-type']); + $account_type = $user['account-type']; + + if (in_array($account_type, [User::ACCOUNT_TYPE_COMMUNITY, User::ACCOUNT_TYPE_RELAY])) { + $intro = $this->t('This page can be used to define the channels that will automatically be reshared by your account.'); + $circles = [ + 0 => $this->l10n->t('Global Community') + ]; + } else { + $intro = $this->t('This page can be used to define your own channels.'); + $circles = [ + 0 => $this->l10n->t('Global Community'), + -5 => $this->l10n->t('Latest Activity'), + -4 => $this->l10n->t('Latest Posts'), + -3 => $this->l10n->t('Latest Creation'), + -1 => $this->l10n->t('Following'), + -2 => $this->l10n->t('Followers'), + ]; + } foreach (Circle::getByUserId($uid) as $circle) { $circles[$circle['id']] = $circle['name']; } + $languages = $this->l10n->getLanguageCodes(true); + $channel_languages = User::getWantedLanguages($uid); + $channels = []; foreach ($this->channel->selectByUid($uid) as $channel) { if (!empty($request['id'])) { @@ -133,6 +164,12 @@ class Channels extends BaseSettings $open = false; } + if ($this->config->get('system', 'allow_relay_channels') && in_array($account_type, [User::ACCOUNT_TYPE_COMMUNITY, User::ACCOUNT_TYPE_RELAY])) { + $publish = ["publish[$channel->code]", $this->t("Publish"), $channel->publish, $this->t("When selected, the channel results are reshared. This only works for public ActivityPub posts from the public timeline or the user defined circles.")]; + } else { + $publish = null; + } + $channels[] = [ 'id' => $channel->code, 'open' => $open, @@ -142,15 +179,24 @@ class Channels extends BaseSettings 'circle' => ["circle[$channel->code]", $this->t('Circle/Channel'), $channel->circle, '', $circles], 'include_tags' => ["include_tags[$channel->code]", $this->t("Include Tags"), str_replace(',', ', ', $channel->includeTags)], 'exclude_tags' => ["exclude_tags[$channel->code]", $this->t("Exclude Tags"), str_replace(',', ', ', $channel->excludeTags)], + 'min_size' => ["min_size[$channel->code]", $this->t("Minimum Size"), $channel->minSize], + 'max_size' => ["max_size[$channel->code]", $this->t("Maximum Size"), $channel->maxSize], 'text_search' => ["text_search[$channel->code]", $this->t("Full Text Search"), $channel->fullTextSearch], 'image' => ["image[$channel->code]", $this->t("Images"), $channel->mediaType & 1], 'video' => ["video[$channel->code]", $this->t("Videos"), $channel->mediaType & 2], 'audio' => ["audio[$channel->code]", $this->t("Audio"), $channel->mediaType & 4], + 'languages' => ["languages[$channel->code][]", $this->t('Languages'), $channel->languages ?? $channel_languages, $this->t('Select all languages that you want to see in this channel. "Unspecified" describes all posts for which no language information was detected (e.g. posts with just an image or too little text to be sure of the language). If you want to see all languages, you will need to select all items in the list.'), $languages, 'multiple'], + 'publish' => $publish, 'delete' => ["delete[$channel->code]", $this->t("Delete channel") . ' (' . $channel->label . ')', false, $this->t("Check to delete this entry from the channel list")] ]; } $t = Renderer::getMarkupTemplate('settings/channels.tpl'); + + $exclude_tags_translation = $this->t('Comma separated list of tags. If a post contain any of these tags, then it will not be part of this channel.'); + // @deprecated 2025.04 this translation is scheduled for removal as a new translation has been added without the typo + $exclude_tags_translation = $this->t('Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel.'); + return Renderer::replaceMacros($t, [ 'open' => count($channels) == 0, 'label' => ["new_label", $this->t('Label'), '', $this->t('Short name for the channel. It is displayed on the channels widget.'), $this->t('Required')], @@ -158,14 +204,17 @@ class Channels extends BaseSettings 'access_key' => ["new_access_key", $this->t("Access Key"), '', $this->t('When you want to access this channel via an access key, you can define it here. Pay attention to not use an already used one.')], 'circle' => ['new_circle', $this->t('Circle/Channel'), 0, $this->t('Select a circle or channel, that your channel should be based on.'), $circles], 'include_tags' => ["new_include_tags", $this->t("Include Tags"), '', $this->t('Comma separated list of tags. A post will be used when it contains any of the listed tags.')], - 'exclude_tags' => ["new_exclude_tags", $this->t("Exclude Tags"), '', $this->t('Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel.')], + 'exclude_tags' => ["new_exclude_tags", $this->t("Exclude Tags"), '', $exclude_tags_translation], + 'min_size' => ["new_min_size", $this->t("Minimum Size"), '', $this->t('Minimum post size. Leave empty for no minimum size. The size is calculated without links, attached posts, mentions or hashtags.')], + 'max_size' => ["new_max_size", $this->t("Maximum Size"), '', $this->t('Maximum post size. Leave empty for no maximum size. The size is calculated without links, attached posts, mentions or hashtags.')], 'text_search' => ["new_text_search", $this->t("Full Text Search"), '', $this->t('Search terms for the body, supports the "boolean mode" operators from MariaDB. See the help for a complete list of operators and additional keywords: %s', 'help/Channels')], 'image' => ['new_image', $this->t("Images"), false, $this->t("Check to display images in the channel.")], 'video' => ["new_video", $this->t("Videos"), false, $this->t("Check to display videos in the channel.")], 'audio' => ["new_audio", $this->t("Audio"), false, $this->t("Check to display audio in the channel.")], + 'languages' => ["new_languages[]", $this->t('Languages'), $channel_languages, $this->t('Select all languages that you want to see in this channel.'), $languages, 'multiple'], '$l10n' => [ 'title' => $this->t('Channels'), - 'intro' => $this->t('This page can be used to define your own channels.'), + 'intro' => $intro, 'addtitle' => $this->t('Add new entry to the channel list'), 'addsubmit' => $this->t('Add'), 'savechanges' => $this->t('Save'), @@ -176,23 +225,25 @@ class Channels extends BaseSettings 'confirm_delete' => $this->t('Delete entry from the channel list?'), ], '$entries' => $channels, - '$baseurl' => $this->baseUrl, '$form_security_token' => self::getFormSecurityToken('settings_channels'), ]); } - private function cleanTags(string $tag_list): string + private function enableTimeline(int $uid, int $id) { - $tags = []; + $bookmarked_timelines = $this->pConfig->get($uid, 'system', 'network_timelines'); + $enabled_timelines = $this->pConfig->get($uid, 'system', 'enabled_timelines'); - $tagitems = explode(',', mb_strtolower($tag_list)); - foreach ($tagitems as $tag) { - $tag = trim($tag, '# '); - if (!empty($tag)) { - $tags[] = preg_replace('#\s#u', '', $tag); - } + if (empty($enabled_timelines) || empty($bookmarked_timelines)) { + return; } - return implode(',', $tags); + + if (in_array($id, $enabled_timelines) || in_array($id, $bookmarked_timelines)) { + return; + } + + $enabled_timelines[] = $id; + $this->pConfig->set($uid, 'system', 'enabled_timelines', $enabled_timelines); } } diff --git a/src/Module/Settings/Connectors.php b/src/Module/Settings/Connectors.php index 0032f087d5..7e5b57a783 100644 --- a/src/Module/Settings/Connectors.php +++ b/src/Module/Settings/Connectors.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; @@ -35,6 +21,7 @@ use Friendica\Model\User; use Friendica\Module\BaseSettings; use Friendica\Module\Response; use Friendica\Navigation\SystemMessages; +use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Email; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; @@ -74,7 +61,7 @@ class Connectors extends BaseSettings $this->pconfig->set($this->session->getLocalUserId(), 'system', 'attach_link_title', intval($request['attach_link_title'])); $this->pconfig->set($this->session->getLocalUserId(), 'system', 'api_spoiler_title', intval($request['api_spoiler_title'])); $this->pconfig->set($this->session->getLocalUserId(), 'system', 'api_auto_attach', intval($request['api_auto_attach'])); - $this->pconfig->set($this->session->getLocalUserId(), 'ostatus', 'legacy_contact', $request['legacy_contact']); + $this->pconfig->set($this->session->getLocalUserId(), 'system', 'article_mode', intval($request['article_mode'])); } elseif (!empty($request['mail-submit']) && function_exists('imap_open') && !$this->config->get('system', 'imap_disabled')) { $mail_server = $request['mail_server'] ?? ''; $mail_port = $request['mail_port'] ?? ''; @@ -138,11 +125,7 @@ class Connectors extends BaseSettings $attach_link_title = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'attach_link_title')); $api_spoiler_title = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'api_spoiler_title', true)); $api_auto_attach = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'api_auto_attach', false)); - $legacy_contact = $this->pconfig->get($this->session->getLocalUserId(), 'ostatus', 'legacy_contact'); - - if (!empty($legacy_contact)) { - $this->baseUrl->redirect('ostatus/subscribe?url=' . urlencode($legacy_contact)); - } + $article_mode = intval($this->pconfig->get($this->session->getLocalUserId(), 'system', 'article_mode')); $connector_settings_forms = []; foreach ($this->database->selectToArray('hook', ['file', 'function'], ['hook' => 'connector_settings']) as $hook) { @@ -165,12 +148,8 @@ class Connectors extends BaseSettings $diasp_enabled = $this->config->get('system', 'diaspora_enabled') ? $this->t('Built-in support for %s connectivity is enabled', $this->t('Diaspora (Socialhome, Hubzilla)')) : $this->t('Built-in support for %s connectivity is disabled', $this->t('Diaspora (Socialhome, Hubzilla)')); - $ostat_enabled = $this->config->get('system', 'ostatus_disabled') ? - $this->t('Built-in support for %s connectivity is disabled', $this->t('OStatus (GNU Social)')) : - $this->t('Built-in support for %s connectivity is enabled', $this->t('OStatus (GNU Social)')); } else { $diasp_enabled = ''; - $ostat_enabled = ''; } $mail_enabled = function_exists('imap_open') && !$this->config->get('system', 'imap_disabled'); @@ -197,6 +176,12 @@ class Connectors extends BaseSettings $ssl_options['notls'] = $this->t('None'); } + $article_modes = [ + ActivityPub::ARTICLE_DEFAULT => $this->t('Default (Mastodon will display the title and a link to the post)'), + ActivityPub::ARTICLE_USE_SUMMARY => $this->t('Use the summary (Mastodon and some others will treat it as content warning)'), + ActivityPub::ARTICLE_EMBED_TITLE => $this->t('Embed the title in the body') + ]; + $tpl = Renderer::getMarkupTemplate('settings/connectors.tpl'); $o = Renderer::replaceMacros($tpl, [ '$form_security_token' => BaseSettings::getFormSecurityToken("settings_connectors"), @@ -204,7 +189,6 @@ class Connectors extends BaseSettings '$title' => $this->t('Social Networks'), '$diasp_enabled' => $diasp_enabled, - '$ostat_enabled' => $ostat_enabled, '$general_settings' => $this->t('General Social Media Settings'), '$accept_only_sharer' => [ @@ -218,15 +202,13 @@ class Connectors extends BaseSettings Item::COMPLETION_LIKE => $this->t('Any conversation my follows interacted with, including likes'), ] ], - '$enable_cw' => ['enable_cw', $this->t('Enable Content Warning'), $enable_cw, $this->t('Users on networks like Mastodon or Pleroma are able to set a content warning field which collapse their post by default. This enables the automatic collapsing instead of setting the content warning as the post title. Doesn\'t affect any other content filtering you eventually set up.')], + '$enable_cw' => ['enable_cw', $this->t("Collapse sensitive posts"), $enable_cw, $this->t('If a post is marked as "sensitive", it will be displayed in a collapsed state, if this option is enabled.')], '$enable_smart_shortening' => ['enable_smart_shortening', $this->t('Enable intelligent shortening'), $enable_smart_shortening, $this->t('Normally the system tries to find the best link to add to shortened posts. If disabled, every shortened post will always point to the original friendica post.')], '$simple_shortening' => ['simple_shortening', $this->t('Enable simple text shortening'), $simple_shortening, $this->t('Normally the system shortens posts at the next line feed. If this option is enabled then the system will shorten the text at the maximum character limit.')], '$attach_link_title' => ['attach_link_title', $this->t('Attach the link title'), $attach_link_title, $this->t('When activated, the title of the attached link will be added as a title on posts to Diaspora. This is mostly helpful with "remote-self" contacts that share feed content.')], '$api_spoiler_title' => ['api_spoiler_title', $this->t('API: Use spoiler field as title'), $api_spoiler_title, $this->t('When activated, the "spoiler_text" field in the API will be used for the title on standalone posts. When deactivated it will be used for spoiler text. For comments it will always be used for spoiler text.')], '$api_auto_attach' => ['api_auto_attach', $this->t('API: Automatically links at the end of the post as attached posts'), $api_auto_attach, $this->t('When activated, added links at the end of the post react the same way as added links in the web interface.')], - '$legacy_contact' => ['legacy_contact', $this->t('Your legacy ActivityPub/GNU Social account'), $legacy_contact, $this->t('If you enter your old account name from an ActivityPub based system or your GNU Social/Statusnet account name here (in the format user@domain.tld), your contacts will be added automatically. The field will be emptied when done.')], - '$repair_ostatus_url' => 'ostatus/repair', - '$repair_ostatus_text' => $this->t('Repair OStatus subscriptions'), + '$article_mode' => ['article_mode', $this->t('Article Mode'), $article_mode, $this->t("Controls how posts with titles are transmitted. Mastodon and its forks don't display the content of these posts if the post is created in the correct (default) way."), $article_modes], '$connector_settings_forms' => $connector_settings_forms, diff --git a/src/Module/Settings/ContactImport.php b/src/Module/Settings/ContactImport.php new file mode 100644 index 0000000000..991b447548 --- /dev/null +++ b/src/Module/Settings/ContactImport.php @@ -0,0 +1,118 @@ +config = $config; + $this->pconfig = $pconfig; + $this->systemMessages = $systemMessages; + } + + protected function post(array $request = []) + { + if (!$this->session->getLocalUserId()) { + throw new HTTPException\ForbiddenException($this->l10n->t('Permission denied.')); + } + + self::checkFormSecurityTokenRedirectOnError($this->args->getQueryString(), 'contactimport'); + + parent::post(); + + // Import Contacts from CSV file + if (!empty($request['importcontact-submit'])) { + $this->pconfig->set($this->session->getLocalUserId(), 'ostatus', 'legacy_contact', $request['legacy_contact']); + if (isset($_FILES['importcontact-filename']) && !empty($_FILES['importcontact-filename']['tmp_name'])) { + // was there an error + if ($_FILES['importcontact-filename']['error'] > 0) { + $this->logger->notice('Contact CSV file upload error', ['error' => $_FILES['importcontact-filename']['error']]); + $this->systemMessages->addNotice($this->l10n->t('Contact CSV file upload error')); + } else { + $csvArray = array_map('str_getcsv', file($_FILES['importcontact-filename']['tmp_name'])); + $this->logger->notice('Import started', ['lines' => count($csvArray)]); + // import contacts + foreach ($csvArray as $csvRow) { + // The 1st row may, or may not contain the headers of the table + // We expect the 1st field of the row to contain either the URL + // or the handle of the account, therefore we check for either + // "http" or "@" to be present in the string. + // All other fields from the row will be ignored + if ((strpos($csvRow[0], '@') !== false) || Network::isValidHttpUrl($csvRow[0])) { + Worker::add(Worker::PRIORITY_MEDIUM, 'AddContact', $this->session->getLocalUserId(), trim($csvRow[0], '@')); + } else { + $this->logger->notice('Invalid account', ['url' => $csvRow[0]]); + } + } + $this->logger->notice('Import done'); + + $this->systemMessages->addInfo($this->l10n->t('Importing Contacts done')); + // delete temp file + unlink($_FILES['importcontact-filename']['tmp_name']); + } + } else { + $this->logger->notice('Import triggered, but no import file was found.'); + } + } + $this->baseUrl->redirect($this->args->getQueryString()); + } + + protected function content(array $request = []): string + { + if (!$this->session->getLocalUserId()) { + throw new HTTPException\ForbiddenException($this->l10n->t('Permission denied.')); + } + + parent::content(); + + $legacy_contact = $this->pconfig->get($this->session->getLocalUserId(), 'ostatus', 'legacy_contact'); + + if (!empty($legacy_contact)) { + $this->baseUrl->redirect('ostatus/subscribe?url=' . urlencode($legacy_contact)); + } + + $tpl = Renderer::getMarkupTemplate('settings/contactimport.tpl'); + return Renderer::replaceMacros($tpl, [ + '$title' => $this->l10n->t('Import Contacts'), + '$submit' => $this->l10n->t('Save Settings'), + '$form_security_token' => self::getFormSecurityToken('contactimport'), + '$importcontact_text' => $this->l10n->t('Upload a CSV file that contains the handle of your followed accounts in the first column you exported from the old account.'), + '$importcontact_button' => $this->l10n->t('Upload File'), + '$importcontact_maxsize' => $this->config->get('system', 'max_csv_file_size', 30720), + '$legacy_contact' => ['legacy_contact', $this->t('Your legacy ActivityPub/GNU Social account'), $legacy_contact, $this->t('If you enter your old account name from an ActivityPub based system or your GNU Social/Statusnet account name here (in the format user@domain.tld), your contacts will be added automatically. The field will be emptied when done.')], + ]); + } +} diff --git a/src/Module/Settings/Delegation.php b/src/Module/Settings/Delegation.php index e122dc62d5..f4c565c420 100644 --- a/src/Module/Settings/Delegation.php +++ b/src/Module/Settings/Delegation.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; diff --git a/src/Module/Settings/Display.php b/src/Module/Settings/Display.php index 623651f481..97c163cea6 100644 --- a/src/Module/Settings/Display.php +++ b/src/Module/Settings/Display.php @@ -1,27 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; +use Friendica\App\Page; +use Friendica\AppHelper; +use Friendica\Content\ContactSelector; use Friendica\Content\Conversation\Collection\Timelines; use Friendica\Content\Text\BBCode; use Friendica\Content\Conversation\Factory\Channel as ChannelFactory; @@ -53,8 +43,8 @@ class Display extends BaseSettings private $config; /** @var IManagePersonalConfigValues */ private $pConfig; - /** @var App */ - private $app; + /** @var AppHelper */ + private $appHelper; /** @var SystemMessages */ private $systemMessages; /** @var ChannelFactory */ @@ -68,13 +58,13 @@ class Display extends BaseSettings /** @var TimelineFactory */ protected $timeline; - public function __construct(Repository\UserDefinedChannel $userDefinedChannel, NetworkFactory $network, CommunityFactory $community, ChannelFactory $channel, TimelineFactory $timeline, SystemMessages $systemMessages, App $app, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + public function __construct(Repository\UserDefinedChannel $userDefinedChannel, NetworkFactory $network, CommunityFactory $community, ChannelFactory $channel, TimelineFactory $timeline, SystemMessages $systemMessages, AppHelper $appHelper, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, IHandleUserSessions $session, Page $page, L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) { parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); $this->config = $config; $this->pConfig = $pConfig; - $this->app = $app; + $this->appHelper = $appHelper; $this->systemMessages = $systemMessages; $this->timeline = $timeline; $this->channel = $channel; @@ -94,29 +84,26 @@ class Display extends BaseSettings $user = User::getById($uid); - $theme = trim($request['theme']); - $mobile_theme = trim($request['mobile_theme'] ?? ''); - $enable_smile = (bool)$request['enable_smile']; - $enable = (array)$request['enable']; - $bookmark = (array)$request['bookmark']; - $channel_languages = (array)$request['channel_languages']; - $first_day_of_week = (bool)$request['first_day_of_week']; - $calendar_default_view = trim($request['calendar_default_view']); - $infinite_scroll = (bool)$request['infinite_scroll']; - $enable_smart_threading = (bool)$request['enable_smart_threading']; - $enable_dislike = (bool)$request['enable_dislike']; - $display_resharer = (bool)$request['display_resharer']; - $stay_local = (bool)$request['stay_local']; - $show_page_drop = (bool)$request['show_page_drop']; - $display_eventlist = (bool)$request['display_eventlist']; - $preview_mode = (int)$request['preview_mode']; - $browser_update = (int)$request['browser_update']; - if ($browser_update != -1) { - $browser_update = $browser_update * 1000; - if ($browser_update < 10000) { - $browser_update = 10000; - } - } + $theme = trim($request['theme']); + $mobile_theme = trim($request['mobile_theme'] ?? ''); + $enable_smile = (bool)$request['enable_smile']; + $enable = (array)$request['enable']; + $bookmark = (array)$request['bookmark']; + $channel_languages = (array)$request['channel_languages']; + $first_day_of_week = (int)$request['first_day_of_week']; + $calendar_default_view = trim($request['calendar_default_view']); + $infinite_scroll = (bool)$request['infinite_scroll']; + $enable_smart_threading = (bool)$request['enable_smart_threading']; + $enable_dislike = (bool)$request['enable_dislike']; + $display_resharer = (bool)$request['display_resharer']; + $stay_local = (bool)$request['stay_local']; + $hide_empty_descriptions = (bool)$request['hide_empty_descriptions']; + $hide_custom_emojis = (bool)$request['hide_custom_emojis']; + $platform_icon_style = (int)$request['platform_icon_style']; + $show_page_drop = (bool)$request['show_page_drop']; + $display_eventlist = (bool)$request['display_eventlist']; + $preview_mode = (int)$request['preview_mode']; + $update_content = (int)$request['update_content']; $enabled_timelines = []; foreach ($enable as $code => $enabled) { @@ -149,32 +136,36 @@ class Display extends BaseSettings $this->pConfig->set($uid, 'system', 'mobile_theme', $mobile_theme); } - $this->pConfig->set($uid, 'system', 'itemspage_network' , $itemspage_network); + $this->pConfig->set($uid, 'system', 'itemspage_network', $itemspage_network); $this->pConfig->set($uid, 'system', 'itemspage_mobile_network', $itemspage_mobile_network); - $this->pConfig->set($uid, 'system', 'update_interval' , $browser_update); - $this->pConfig->set($uid, 'system', 'no_smilies' , !$enable_smile); - $this->pConfig->set($uid, 'system', 'infinite_scroll' , $infinite_scroll); - $this->pConfig->set($uid, 'system', 'no_smart_threading' , !$enable_smart_threading); - $this->pConfig->set($uid, 'system', 'hide_dislike' , !$enable_dislike); - $this->pConfig->set($uid, 'system', 'display_resharer' , $display_resharer); - $this->pConfig->set($uid, 'system', 'stay_local' , $stay_local); - $this->pConfig->set($uid, 'system', 'show_page_drop' , $show_page_drop); - $this->pConfig->set($uid, 'system', 'display_eventlist' , $display_eventlist); - $this->pConfig->set($uid, 'system', 'preview_mode' , $preview_mode); + $this->pConfig->set($uid, 'system', 'update_content', $update_content); + $this->pConfig->set($uid, 'system', 'no_smilies', !$enable_smile); + $this->pConfig->set($uid, 'system', 'infinite_scroll', $infinite_scroll); + $this->pConfig->set($uid, 'system', 'no_smart_threading', !$enable_smart_threading); + $this->pConfig->set($uid, 'system', 'hide_dislike', !$enable_dislike); + $this->pConfig->set($uid, 'system', 'display_resharer', $display_resharer); + $this->pConfig->set($uid, 'system', 'stay_local', $stay_local); + $this->pConfig->set($uid, 'system', 'show_page_drop', $show_page_drop); + $this->pConfig->set($uid, 'system', 'display_eventlist', $display_eventlist); + $this->pConfig->set($uid, 'system', 'preview_mode', $preview_mode); - $this->pConfig->set($uid, 'system', 'network_timelines' , $network_timelines); - $this->pConfig->set($uid, 'system', 'enabled_timelines' , $enabled_timelines); - $this->pConfig->set($uid, 'channel', 'languages' , $channel_languages); + $this->pConfig->set($uid, 'system', 'network_timelines', $network_timelines); + $this->pConfig->set($uid, 'system', 'enabled_timelines', $enabled_timelines); + $this->pConfig->set($uid, 'channel', 'languages', $channel_languages); - $this->pConfig->set($uid, 'calendar', 'first_day_of_week' , $first_day_of_week); - $this->pConfig->set($uid, 'calendar', 'default_view' , $calendar_default_view); + $this->pConfig->set($uid, 'accessibility', 'hide_empty_descriptions', $hide_empty_descriptions); + $this->pConfig->set($uid, 'accessibility', 'hide_custom_emojis', $hide_custom_emojis); + $this->pConfig->set($uid, 'accessibility', 'platform_icon_style', $platform_icon_style); + + $this->pConfig->set($uid, 'calendar', 'first_day_of_week', $first_day_of_week); + $this->pConfig->set($uid, 'calendar', 'default_view', $calendar_default_view); if (in_array($theme, Theme::getAllowedList())) { if ($theme == $user['theme']) { // call theme_post only if theme has not been changed if ($themeconfigfile = Theme::getConfigFile($theme)) { require_once $themeconfigfile; - theme_post($this->app); + theme_post($this->appHelper); } } else { User::update(['theme' => $theme], $uid); @@ -211,7 +202,7 @@ class Display extends BaseSettings $allowed_themes = Theme::getAllowedList(); - $themes = []; + $themes = []; $mobile_themes = ['---' => $this->t('No special theme for mobile devices')]; foreach ($allowed_themes as $theme) { $is_experimental = file_exists('view/theme/' . $theme . '/experimental'); @@ -236,26 +227,33 @@ class Display extends BaseSettings $theme_selected = $user['theme'] ?: $default_theme; $mobile_theme_selected = $this->session->get('mobile-theme', $default_mobile_theme); - $itemspage_network = intval($this->pConfig->get($uid, 'system', 'itemspage_network')); - $itemspage_network = (($itemspage_network > 0 && $itemspage_network < 101) ? $itemspage_network : $this->config->get('system', 'itemspage_network')); + $itemspage_network = intval($this->pConfig->get($uid, 'system', 'itemspage_network')); + $itemspage_network = (($itemspage_network > 0 && $itemspage_network < 101) ? $itemspage_network : $this->config->get('system', 'itemspage_network')); $itemspage_mobile_network = intval($this->pConfig->get($uid, 'system', 'itemspage_mobile_network')); $itemspage_mobile_network = (($itemspage_mobile_network > 0 && $itemspage_mobile_network < 101) ? $itemspage_mobile_network : $this->config->get('system', 'itemspage_network_mobile')); - $browser_update = intval($this->pConfig->get($uid, 'system', 'update_interval')); - if ($browser_update != -1) { - $browser_update = (($browser_update == 0) ? 40 : $browser_update / 1000); // default if not set: 40 seconds - } - + $update_content = $this->pConfig->get($uid, 'system', 'update_content') ?? false; $enable_smile = !$this->pConfig->get($uid, 'system', 'no_smilies', false); - $infinite_scroll = $this->pConfig->get($uid, 'system', 'infinite_scroll', false); + $infinite_scroll = $this->pConfig->get($uid, 'system', 'infinite_scroll', false); $enable_smart_threading = !$this->pConfig->get($uid, 'system', 'no_smart_threading', false); $enable_dislike = !$this->pConfig->get($uid, 'system', 'hide_dislike', false); - $display_resharer = $this->pConfig->get($uid, 'system', 'display_resharer', false); - $stay_local = $this->pConfig->get($uid, 'system', 'stay_local', false); - $show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true); - $display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true); + $display_resharer = $this->pConfig->get($uid, 'system', 'display_resharer', false); + $stay_local = $this->pConfig->get($uid, 'system', 'stay_local', false); + $show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true); + $display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true); - $preview_mode = $this->pConfig->get($uid, 'system', 'preview_mode', BBCode::PREVIEW_LARGE); + $hide_empty_descriptions = $this->pConfig->get($uid, 'accessibility', 'hide_empty_descriptions', false); + $hide_custom_emojis = $this->pConfig->get($uid, 'accessibility', 'hide_custom_emojis', false); + $platform_icon_style = $this->pConfig->get($uid, 'accessibility', 'platform_icon_style', ContactSelector::SVG_COLOR_BLACK); + $platform_icon_styles = [ + ContactSelector::SVG_DISABLED => $this->t('Disabled'), + ContactSelector::SVG_COLOR_BLACK => $this->t('Color/Black'), + ContactSelector::SVG_BLACK => $this->t('Black'), + ContactSelector::SVG_COLOR_WHITE => $this->t('Color/White'), + ContactSelector::SVG_WHITE => $this->t('White'), + ]; + + $preview_mode = $this->pConfig->get($uid, 'system', 'preview_mode', BBCode::PREVIEW_LARGE); $preview_modes = [ BBCode::PREVIEW_NONE => $this->t('No preview'), BBCode::PREVIEW_NO_IMAGE => $this->t('No image'), @@ -265,16 +263,16 @@ class Display extends BaseSettings $bookmarked_timelines = $this->pConfig->get($uid, 'system', 'network_timelines', $this->getAvailableTimelines($uid, true)->column('code')); $enabled_timelines = $this->pConfig->get($uid, 'system', 'enabled_timelines', $this->getAvailableTimelines($uid, false)->column('code')); - $channel_languages = User::getWantedLanguages($uid); - $languages = $this->l10n->getLanguageCodes(true); + $channel_languages = User::getWantedLanguages($uid); + $languages = $this->l10n->getLanguageCodes(true); $timelines = []; foreach ($this->getAvailableTimelines($uid) as $timeline) { $timelines[] = [ - 'label' => $timeline->label, - 'description' => $timeline->description, - 'enable' => ["enable[{$timeline->code}]", '', in_array($timeline->code, $enabled_timelines)], - 'bookmark' => ["bookmark[{$timeline->code}]", '', in_array($timeline->code, $bookmarked_timelines)], + 'label' => $timeline->label, + 'description' => $timeline->description, + 'enable' => ["enable[{$timeline->code}]", '', in_array($timeline->code, $enabled_timelines)], + 'bookmark' => ["bookmark[{$timeline->code}]", '', in_array($timeline->code, $bookmarked_timelines)], ]; } @@ -300,7 +298,7 @@ class Display extends BaseSettings $theme_config = ''; if ($themeconfigfile = Theme::getConfigFile($theme_selected)) { require_once $themeconfigfile; - $theme_config = theme_content($this->app); + $theme_config = theme_content($this->appHelper); } $tpl = Renderer::getMarkupTemplate('settings/display.tpl'); @@ -318,22 +316,25 @@ class Display extends BaseSettings '$form_security_token' => self::getFormSecurityToken('settings_display'), '$uid' => $uid, - '$theme' => ['theme', $this->t('Display Theme:'), $theme_selected, '', $themes, true], - '$mobile_theme' => ['mobile_theme', $this->t('Mobile Theme:'), $mobile_theme_selected, '', $mobile_themes, false], + '$theme' => ['theme', $this->t('Display Theme:'), $theme_selected, '', $themes, true], + '$mobile_theme' => ['mobile_theme', $this->t('Mobile Theme:'), $mobile_theme_selected, '', $mobile_themes, false], '$theme_config' => $theme_config, - '$itemspage_network' => ['itemspage_network' , $this->t('Number of items to display per page:'), $itemspage_network, $this->t('Maximum of 100 items')], + '$itemspage_network' => ['itemspage_network', $this->t('Number of items to display per page:'), $itemspage_network, $this->t('Maximum of 100 items')], '$itemspage_mobile_network' => ['itemspage_mobile_network', $this->t('Number of items to display per page when viewed from mobile device:'), $itemspage_mobile_network, $this->t('Maximum of 100 items')], - '$ajaxint' => ['browser_update' , $this->t('Update browser every xx seconds'), $browser_update, $this->t('Minimum of 10 seconds. Enter -1 to disable it.')], - '$enable_smile' => ['enable_smile' , $this->t('Display emoticons'), $enable_smile, $this->t('When enabled, emoticons are replaced with matching symbols.')], - '$infinite_scroll' => ['infinite_scroll' , $this->t('Infinite scroll'), $infinite_scroll, $this->t('Automatic fetch new items when reaching the page end.')], - '$enable_smart_threading' => ['enable_smart_threading' , $this->t('Enable Smart Threading'), $enable_smart_threading, $this->t('Enable the automatic suppression of extraneous thread indentation.')], - '$enable_dislike' => ['enable_dislike' , $this->t('Display the Dislike feature'), $enable_dislike, $this->t('Display the Dislike button and dislike reactions on posts and comments.')], - '$display_resharer' => ['display_resharer' , $this->t('Display the resharer'), $display_resharer, $this->t('Display the first resharer as icon and text on a reshared item.')], - '$stay_local' => ['stay_local' , $this->t('Stay local'), $stay_local, $this->t("Don't go to a remote system when following a contact link.")], - '$show_page_drop' => ['show_page_drop' , $this->t('Show the post deletion checkbox'), $show_page_drop, $this->t("Display the checkbox for the post deletion on the network page.")], - '$display_eventlist' => ['display_eventlist' , $this->t('DIsplay the event list'), $display_eventlist, $this->t("Display the birthday reminder and event list on the network page.")], - '$preview_mode' => ['preview_mode' , $this->t('Link preview mode'), $preview_mode, $this->t('Appearance of the link preview that is added to each post with a link.'), $preview_modes, false], + '$update_content' => ['update_content', $this->t('Regularly update the page content'), $update_content, $this->t('When enabled, new content on network, community and channels are added on top.')], + '$enable_smile' => ['enable_smile', $this->t('Display emoticons'), $enable_smile, $this->t('When enabled, emoticons are replaced with matching symbols.')], + '$infinite_scroll' => ['infinite_scroll', $this->t('Infinite scroll'), $infinite_scroll, $this->t('Automatic fetch new items when reaching the page end.')], + '$enable_smart_threading' => ['enable_smart_threading', $this->t('Enable Smart Threading'), $enable_smart_threading, $this->t('Enable the automatic suppression of extraneous thread indentation.')], + '$enable_dislike' => ['enable_dislike', $this->t('Display the Dislike feature'), $enable_dislike, $this->t('Display the Dislike button and dislike reactions on posts and comments.')], + '$display_resharer' => ['display_resharer', $this->t('Display the resharer'), $display_resharer, $this->t('Display the first resharer as icon and text on a reshared item.')], + '$stay_local' => ['stay_local', $this->t('Stay local'), $stay_local, $this->t("Don't go to a remote system when following a contact link.")], + '$show_page_drop' => ['show_page_drop', $this->t('Show the post deletion checkbox'), $show_page_drop, $this->t("Display the checkbox for the post deletion on the network page.")], + '$display_eventlist' => ['display_eventlist', $this->t('DIsplay the event list'), $display_eventlist, $this->t("Display the birthday reminder and event list on the network page.")], + '$preview_mode' => ['preview_mode', $this->t('Link preview mode'), $preview_mode, $this->t('Appearance of the link preview that is added to each post with a link.'), $preview_modes, false], + '$hide_empty_descriptions' => ['hide_empty_descriptions', $this->t('Hide pictures with empty alternative text'), $hide_empty_descriptions, $this->t("Don't display pictures that are missing the alternative text.")], + '$hide_custom_emojis' => ['hide_custom_emojis', $this->t('Hide custom emojis'), $hide_custom_emojis, $this->t("Don't display custom emojis.")], + '$platform_icon_style' => ['platform_icon_style', $this->t('Platform icons style'), $platform_icon_style, $this->t('Style of the platform icons'), $platform_icon_styles, false], '$timeline_label' => $this->t('Label'), '$timeline_descriptiom' => $this->t('Description'), @@ -342,9 +343,9 @@ class Display extends BaseSettings '$timelines' => $timelines, '$timeline_explanation' => $this->t('Enable timelines that you want to see in the channels widget. Bookmark timelines that you want to see in the top menu.'), - '$channel_languages' => ['channel_languages[]', $this->t('Channel languages:'), $channel_languages, $this->t('Select all languages that you want to see in your channels.'), $languages, 'multiple'], + '$channel_languages' => ['channel_languages[]', $this->t('Channel languages:'), $channel_languages, $this->t('Select all the languages you want to see in your channels. "Unspecified" describes all posts for which no language information was detected (e.g. posts with just an image or too little text to be sure of the language). If you want to see all languages, you will need to select all items in the list.'), $languages, 'multiple'], - '$first_day_of_week' => ['first_day_of_week' , $this->t('Beginning of week:') , $first_day_of_week , '', $weekdays , false], + '$first_day_of_week' => ['first_day_of_week', $this->t('Beginning of week:'), $first_day_of_week, '', $weekdays, false], '$calendar_default_view' => ['calendar_default_view', $this->t('Default calendar view:'), $calendar_default_view, '', $calendarViews, false], ]); } diff --git a/src/Module/Settings/Features.php b/src/Module/Settings/Features.php index c1766f6b92..3c8d6ba5df 100644 --- a/src/Module/Settings/Features.php +++ b/src/Module/Settings/Features.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; @@ -49,7 +35,7 @@ class Features extends BaseSettings BaseSettings::checkFormSecurityTokenRedirectOnError('/settings/features', 'settings_features'); foreach ($request as $k => $v) { if (strpos($k, 'feature_') === 0) { - $this->pConfig->set($this->session->getLocalUserId(), 'feature', substr($k, 8), ((intval($v)) ? 1 : 0)); + $this->pConfig->set($this->session->getLocalUserId(), 'feature', substr($k, 8), (bool)$v); } } } @@ -58,9 +44,8 @@ class Features extends BaseSettings { parent::content($request); - $arr = []; - $features = Feature::get(); - foreach ($features as $name => $feature) { + $arr = []; + foreach (Feature::get() as $name => $feature) { $arr[$name] = []; $arr[$name][0] = $feature[0]; foreach (array_slice($feature, 1) as $f) { diff --git a/src/Module/Settings/OAuth.php b/src/Module/Settings/OAuth.php index 98c547e1a7..12737599ab 100644 --- a/src/Module/Settings/OAuth.php +++ b/src/Module/Settings/OAuth.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; diff --git a/src/Module/Settings/Profile/Index.php b/src/Module/Settings/Profile/Index.php index 38d816833f..9a342aa3b3 100644 --- a/src/Module/Settings/Profile/Index.php +++ b/src/Module/Settings/Profile/Index.php @@ -1,50 +1,39 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\Profile; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; +use Friendica\App\Page; use Friendica\Core\ACL; -use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Protocol; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\Theme; +use Friendica\Core\Worker; use Friendica\Database\DBA; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; use Friendica\Model\Profile; use Friendica\Module\Response; -use Friendica\Navigation\SystemMessages; -use Friendica\Profile\ProfileField; use Friendica\Model\User; use Friendica\Module\BaseSettings; use Friendica\Module\Security\Login; +use Friendica\Navigation\SystemMessages; use Friendica\Network\HTTPException; +use Friendica\Profile\ProfileField; use Friendica\Security\PermissionSet; use Friendica\Util\ACLFormatter; use Friendica\Util\DateTimeFormat; use Friendica\Util\Profiler; use Friendica\Util\Temporal; -use Friendica\Core\Worker; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; class Index extends BaseSettings @@ -61,9 +50,27 @@ class Index extends BaseSettings private $permissionSetFactory; /** @var ACLFormatter */ private $aclFormatter; + private EventDispatcherInterface $eventDispatcher; - public function __construct(ACLFormatter $aclFormatter, PermissionSet\Factory\PermissionSet $permissionSetFactory, PermissionSet\Repository\PermissionSet $permissionSetRepo, SystemMessages $systemMessages, ProfileField\Factory\ProfileField $profileFieldFactory, ProfileField\Repository\ProfileField $profileFieldRepo, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) - { + public function __construct( + ACLFormatter $aclFormatter, + PermissionSet\Factory\PermissionSet $permissionSetFactory, + PermissionSet\Repository\PermissionSet $permissionSetRepo, + SystemMessages $systemMessages, + ProfileField\Factory\ProfileField $profileFieldFactory, + ProfileField\Repository\ProfileField $profileFieldRepo, + EventDispatcherInterface $eventDispatcher, + IHandleUserSessions $session, + Page $page, + L10n $l10n, + BaseURL $baseUrl, + Arguments $args, + LoggerInterface $logger, + Profiler $profiler, + Response $response, + array $server, + array $parameters = [] + ) { parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); $this->profileFieldRepo = $profileFieldRepo; @@ -72,6 +79,7 @@ class Index extends BaseSettings $this->permissionSetRepo = $permissionSetRepo; $this->permissionSetFactory = $permissionSetFactory; $this->aclFormatter = $aclFormatter; + $this->eventDispatcher = $eventDispatcher; } protected function post(array $request = []) @@ -87,9 +95,11 @@ class Index extends BaseSettings self::checkFormSecurityTokenRedirectOnError('/settings/profile', 'settings_profile'); - Hook::callAll('profile_post', $request); + $request = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::PROFILE_SETTINGS_POST, $request), + )->getArray(); - $dob = trim($request['dob'] ?? ''); + $dob = $this->cleanInput($request['dob'] ?? ''); if ($dob && !in_array($dob, ['0000-00-00', DBA::NULL_DATE])) { $y = substr($dob, 0, 4); @@ -101,7 +111,7 @@ class Index extends BaseSettings if (strpos($dob, '0000-') === 0 || strpos($dob, '0001-') === 0) { $ignore_year = true; - $dob = substr($dob, 5); + $dob = substr($dob, 5); } if ($ignore_year) { @@ -111,32 +121,35 @@ class Index extends BaseSettings } } - $username = trim($request['username'] ?? ''); + $username = $this->cleanInputText($request['username'] ?? ''); if (!$username) { $this->systemMessages->addNotice($this->t('Display Name is required.')); return; } - $about = trim($request['about']); - $address = trim($request['address']); - $locality = trim($request['locality']); - $region = trim($request['region']); - $postal_code = trim($request['postal_code']); - $country_name = trim($request['country_name']); + $about = $this->cleanInputText($request['about']); + $address = $this->cleanInputText($request['address']); + $locality = $this->cleanInputText($request['locality']); + $region = $this->cleanInputText($request['region']); + $postal_code = $this->cleanInputText($request['postal_code']); + $country_name = $this->cleanInputText($request['country_name']); $pub_keywords = self::cleanKeywords(trim($request['pub_keywords'])); $prv_keywords = self::cleanKeywords(trim($request['prv_keywords'])); - $xmpp = trim($request['xmpp']); - $matrix = trim($request['matrix']); - $homepage = trim($request['homepage']); + $xmpp = $this->cleanInput(trim($request['xmpp'])); + $matrix = $this->cleanInput(trim($request['matrix'])); + $homepage = $this->cleanInput(trim($request['homepage'])); if ((strpos($homepage, 'http') !== 0) && (strlen($homepage))) { // neither http nor https in URL, add them $homepage = 'http://' . $homepage; } + $user = User::getById($this->session->getLocalUserId()); + $about = Profile::addResponsibleRelayContact($about, $user['parent-uid'], $user['account-type'], $user['language']); + $profileFieldsNew = $this->getProfileFieldsFromInput( $this->session->getLocalUserId(), - $request['profile_field'], - $request['profile_field_order'] + (array)$request['profile_field'], + (array)$request['profile_field_order'] ); $this->profileFieldRepo->saveCollectionForUser($this->session->getLocalUserId(), $profileFieldsNew); @@ -187,6 +200,8 @@ class Index extends BaseSettings throw new HTTPException\NotFoundException(); } + $owner['about'] = Profile::addResponsibleRelayContact($owner['about'], $owner['parent-uid'], $owner['account-type'], $owner['language']); + $this->page->registerFooterScript('view/asset/es-jquery-sortable/source/js/jquery-sortable-min.js'); $this->page->registerFooterScript(Theme::getPathForFile('js/module/settings/profile/index.js')); @@ -230,7 +245,7 @@ class Index extends BaseSettings $this->session->getLocalUserId(), false, ['allow_cid' => []], - ['network' => Protocol::DFRN], + ['network' => Protocol::DFRN], 'profile_field[new]' ), ], @@ -263,7 +278,8 @@ class Index extends BaseSettings 'miscellaneous_section' => $this->t('Miscellaneous'), 'custom_fields_section' => $this->t('Custom Profile Fields'), 'profile_photo' => $this->t('Upload Profile Photo'), - 'custom_fields_description' => $this->t('

                              Custom fields appear on your profile page.

                              + 'custom_fields_description' => $this->t( + '

                              Custom fields appear on your profile page.

                              You can use BBCodes in the field values.

                              Reorder by dragging the field title.

                              Empty the label field to remove a custom field.

                              @@ -297,8 +313,16 @@ class Index extends BaseSettings '$custom_fields' => $custom_fields, ]); - $arr = ['profile' => $owner, 'entry' => $o]; - Hook::callAll('profile_edit', $arr); + $hook_data = [ + 'profile' => $owner, + 'entry' => $o, + ]; + + $hook_data = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::PROFILE_SETTINGS_FORM, $hook_data), + )->getArray(); + + $o = $hook_data['entry'] ?? $o; return $o; } @@ -353,6 +377,16 @@ class Index extends BaseSettings return $profileFields; } + private function cleanInputText(string $input): string + { + return trim(strip_tags($input)); + } + + private function cleanInput(string $input): string + { + return str_replace(['<', '>', '"', "'", ' '], '', $input); + } + private static function cleanKeywords($keywords): string { $keywords = str_replace(',', ' ', $keywords); @@ -360,7 +394,7 @@ class Index extends BaseSettings $cleaned = []; foreach ($keywords as $keyword) { - $keyword = trim($keyword); + $keyword = trim(str_replace(['<', '>', '"', "'"], '', $keyword)); $keyword = trim($keyword, '#'); if ($keyword != '') { $cleaned[] = $keyword; diff --git a/src/Module/Settings/Profile/Photo/Crop.php b/src/Module/Settings/Profile/Photo/Crop.php index 2929e16dae..53486fe001 100644 --- a/src/Module/Settings/Profile/Photo/Crop.php +++ b/src/Module/Settings/Profile/Photo/Crop.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\Profile\Photo; @@ -55,14 +41,11 @@ class Crop extends BaseSettings $selectionW = intval($_POST['width'] ?? 0); $selectionH = intval($_POST['height'] ?? 0); - $path = 'profile/' . DI::app()->getLoggedInUserNickname(); + $path = 'profile/' . DI::userSession()->getLocalUserNickname(); $base_image = Photo::selectFirst([], ['resource-id' => $resource_id, 'uid' => DI::userSession()->getLocalUserId(), 'scale' => $scale]); if (DBA::isResult($base_image)) { $Image = Photo::getImageForPhoto($base_image); - if (empty($Image)) { - throw new HTTPException\InternalServerErrorException(); - } if ($Image->isValid()) { // If setting for the default profile, unset the profile photo flag from any other photos I own @@ -195,11 +178,11 @@ class Crop extends BaseSettings DI::sysmsg()->addInfo(DI::l10n()->t('Profile picture successfully updated.')); - DI::baseUrl()->redirect('profile/' . DI::app()->getLoggedInUserNickname()); + DI::baseUrl()->redirect('profile/' . DI::userSession()->getLocalUserNickname()); } $Image = Photo::getImageForPhoto($photos[0]); - if (empty($Image)) { + if (!$Image->isValid()) { throw new HTTPException\InternalServerErrorException(); } @@ -213,7 +196,7 @@ class Crop extends BaseSettings DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/photo/crop_head.tpl'), []); - $filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . '.' . $imagecrop['ext']; + $filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . $imagecrop['ext']; $tpl = Renderer::getMarkupTemplate('settings/profile/photo/crop.tpl'); $o = Renderer::replaceMacros($tpl, [ '$filename' => $filename, diff --git a/src/Module/Settings/Profile/Photo/Index.php b/src/Module/Settings/Profile/Photo/Index.php index 47c4455d2e..1041dda71a 100644 --- a/src/Module/Settings/Profile/Photo/Index.php +++ b/src/Module/Settings/Profile/Photo/Index.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\Profile\Photo; @@ -52,8 +38,6 @@ class Index extends BaseSettings $filesize = intval($_FILES['userfile']['size']); $filetype = $_FILES['userfile']['type']; - $filetype = Images::getMimeTypeBySource($src, $filename, $filetype); - $maximagesize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maximagesize', 0)); if ($maximagesize && $filesize > $maximagesize) { @@ -63,7 +47,7 @@ class Index extends BaseSettings } $imagedata = @file_get_contents($src); - $Image = new Image($imagedata, $filetype); + $Image = new Image($imagedata, $filetype, $filename); if (!$Image->isValid()) { DI::sysmsg()->addNotice(DI::l10n()->t('Unable to process image.')); @@ -133,7 +117,7 @@ class Index extends BaseSettings DI::l10n()->t('or'), ($newuser) ? '' . DI::l10n()->t('skip this step') . '' - : '' + : '' . DI::l10n()->t('select a photo from your photo albums') . '' ), ]); diff --git a/src/Module/Settings/RemoveMe.php b/src/Module/Settings/RemoveMe.php index 800c2cd6c8..cae57d2f0f 100644 --- a/src/Module/Settings/RemoveMe.php +++ b/src/Module/Settings/RemoveMe.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; @@ -106,8 +92,6 @@ class RemoveMe extends BaseSettings $this->baseUrl->redirect(); } catch (\RuntimeException $e) { $this->systemMessages->addNotice($e->getMessage()); - } finally { - return; } } diff --git a/src/Module/Settings/Server/Action.php b/src/Module/Settings/Server/Action.php index e06cbf26be..32ed78d2eb 100644 --- a/src/Module/Settings/Server/Action.php +++ b/src/Module/Settings/Server/Action.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\Server; diff --git a/src/Module/Settings/Server/Index.php b/src/Module/Settings/Server/Index.php index f59e23a87b..c32f0bcfe3 100644 --- a/src/Module/Settings/Server/Index.php +++ b/src/Module/Settings/Server/Index.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\Server; diff --git a/src/Module/Settings/TwoFactor/AppSpecific.php b/src/Module/Settings/TwoFactor/AppSpecific.php index c3653d5fd3..94e14c3521 100644 --- a/src/Module/Settings/TwoFactor/AppSpecific.php +++ b/src/Module/Settings/TwoFactor/AppSpecific.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\TwoFactor; @@ -52,7 +38,7 @@ class AppSpecific extends BaseSettings { parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->pConfig = $pConfig; + $this->pConfig = $pConfig; $this->systemMessages = $systemMessages; if (!$this->session->getLocalUserId()) { @@ -95,7 +81,7 @@ class AppSpecific extends BaseSettings } break; - case 'revoke_all' : + case 'revoke_all': AppSpecificPassword::deleteAllForUser($this->session->getLocalUserId()); $this->systemMessages->addInfo($this->t('App-specific passwords successfully revoked.')); $this->baseUrl->redirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password')); @@ -128,22 +114,22 @@ class AppSpecific extends BaseSettings '$form_security_token' => self::getFormSecurityToken('settings_2fa_app_specific'), '$password_security_token' => self::getFormSecurityToken('settings_2fa_password'), - '$title' => $this->t('Two-factor app-specific passwords'), - '$help_label' => $this->t('Help'), - '$message' => $this->t('

                              App-specific passwords are randomly generated passwords used instead your regular password to authenticate your account on third-party applications that don\'t support two-factor authentication.

                              '), - '$generated_message' => $this->t('Make sure to copy your new app-specific password now. You won’t be able to see it again!'), + '$title' => $this->t('Two-factor app-specific passwords'), + '$help_label' => $this->t('Help'), + '$message' => $this->t('

                              App-specific passwords are randomly generated passwords used instead your regular password to authenticate your account on third-party applications that don\'t support two-factor authentication.

                              '), + '$generated_message' => $this->t('Make sure to copy your new app-specific password now. You won’t be able to see it again!'), '$generated_app_specific_password' => $this->appSpecificPassword, - '$description_label' => $this->t('Description'), - '$last_used_label' => $this->t('Last Used'), - '$revoke_label' => $this->t('Revoke'), - '$revoke_all_label' => $this->t('Revoke All'), + '$description_label' => $this->t('Description'), + '$last_used_label' => $this->t('Last Used'), + '$revoke_label' => $this->t('Revoke'), + '$revoke_all_label' => $this->t('Revoke All'), - '$app_specific_passwords' => $appSpecificPasswords, - '$generate_message' => $this->t('When you generate a new app-specific password, you must use it right away, it will be shown to you once after you generate it.'), - '$generate_title' => $this->t('Generate new app-specific password'), + '$app_specific_passwords' => $appSpecificPasswords, + '$generate_message' => $this->t('When you generate a new app-specific password, you must use it right away, it will be shown to you once after you generate it.'), + '$generate_title' => $this->t('Generate new app-specific password'), '$description_placeholder_label' => $this->t('Friendiqa on my Fairphone 2...'), - '$generate_label' => $this->t('Generate'), + '$generate_label' => $this->t('Generate'), ]); } } diff --git a/src/Module/Settings/TwoFactor/Index.php b/src/Module/Settings/TwoFactor/Index.php index 29acacc604..4731b105c0 100644 --- a/src/Module/Settings/TwoFactor/Index.php +++ b/src/Module/Settings/TwoFactor/Index.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\TwoFactor; diff --git a/src/Module/Settings/TwoFactor/Recovery.php b/src/Module/Settings/TwoFactor/Recovery.php index 9c0191b6dd..7dce732ab0 100644 --- a/src/Module/Settings/TwoFactor/Recovery.php +++ b/src/Module/Settings/TwoFactor/Recovery.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\TwoFactor; diff --git a/src/Module/Settings/TwoFactor/Trusted.php b/src/Module/Settings/TwoFactor/Trusted.php index 1cd5e5598a..6a3d8311df 100644 --- a/src/Module/Settings/TwoFactor/Trusted.php +++ b/src/Module/Settings/TwoFactor/Trusted.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\TwoFactor; diff --git a/src/Module/Settings/TwoFactor/Verify.php b/src/Module/Settings/TwoFactor/Verify.php index ca05dbf5e7..db21c92903 100644 --- a/src/Module/Settings/TwoFactor/Verify.php +++ b/src/Module/Settings/TwoFactor/Verify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings\TwoFactor; diff --git a/src/Module/Settings/UserExport.php b/src/Module/Settings/UserExport.php index 7ff08d97b7..9c87452a26 100644 --- a/src/Module/Settings/UserExport.php +++ b/src/Module/Settings/UserExport.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Settings; @@ -29,7 +15,6 @@ use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\Database\Definition\DbaDefinition; -use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\Post; @@ -47,8 +32,7 @@ use Psr\Log\LoggerInterface; **/ class UserExport extends BaseSettings { - /** @var DbaDefinition */ - private $dbaDefinition; + private DbaDefinition $dbaDefinition; public function __construct(DbaDefinition $dbaDefinition, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) { @@ -86,10 +70,12 @@ class UserExport extends BaseSettings * options shown on "Export personal data" page * list of array( 'link url', 'link text', 'help text' ) */ + + $t = self::getFormSecurityToken('userexport'); $options = [ - ['settings/userexport/account', $this->l10n->t('Export account'), $this->l10n->t('Export your account info and contacts. Use this to make a backup of your account and/or to move it to another server.')], - ['settings/userexport/backup', $this->l10n->t('Export all'), $this->l10n->t('Export your account info, contacts and all your items as json. Could be a very big file, and could take a lot of time. Use this to make a full backup of your account (photos are not exported)')], - ['settings/userexport/contact', $this->l10n->t('Export Contacts to CSV'), $this->l10n->t('Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.')], + ['settings/userexport/account?t=' . $t, $this->l10n->t('Export account'), $this->l10n->t('Export your account info and contacts. Use this to make a backup of your account and/or to move it to another server.')], + ['settings/userexport/backup?t=' . $t, $this->l10n->t('Export all'), $this->l10n->t('Export your account info, contacts and all your items as json. Could be a very big file, and could take a lot of time. Use this to make a full backup of your account (photos are not exported)')], + ['settings/userexport/contact?t=' . $t, $this->l10n->t('Export Contacts to CSV'), $this->l10n->t('Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.')], ]; Hook::callAll('uexport_options', $options); @@ -115,20 +101,21 @@ class UserExport extends BaseSettings } if (isset($this->parameters['action'])) { + self::checkFormSecurityTokenForbiddenOnError('userexport', 't'); switch ($this->parameters['action']) { case 'backup': header('Content-type: application/json'); - header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '.' . $this->parameters['action'] . '"'); + header('Content-Disposition: attachment; filename="' . $this->session->getLocalUserNickname() . '.' . $this->parameters['action'] . '"'); $this->echoAll($this->session->getLocalUserId()); break; case 'account': header('Content-type: application/json'); - header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '.' . $this->parameters['action'] . '"'); + header('Content-Disposition: attachment; filename="' . $this->session->getLocalUserNickname() . '.' . $this->parameters['action'] . '"'); $this->echoAccount($this->session->getLocalUserId()); break; case 'contact': header('Content-type: application/csv'); - header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '-contacts.csv' . '"'); + header('Content-Disposition: attachment; filename="' . $this->session->getLocalUserNickname() . '-contacts.csv' . '"'); $this->echoContactsAsCSV($this->session->getLocalUserId()); break; } @@ -156,11 +143,8 @@ class UserExport extends BaseSettings if (!isset($row[$column])) { continue; } - if ($field['type'] == 'datetime') { - $p[$column] = $row[$column] ?? DBA::NULL_DATETIME; - } else { - $p[$column] = $row[$column]; - } + + $p[$column] = $row[$column]; } $result[] = $p; } diff --git a/src/Module/Smilies.php b/src/Module/Smilies.php index 52acb19242..fe577ed575 100644 --- a/src/Module/Smilies.php +++ b/src/Module/Smilies.php @@ -1,31 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Content; -use Friendica\Core\Logger; use Friendica\Core\Renderer; -use Friendica\Core\System; use Friendica\DI; /** @@ -48,7 +32,7 @@ class Smilies extends BaseModule protected function content(array $request = []): string { $smilies = Content\Smilies::getList(); - $count = count($smilies['texts'] ?? []); + $count = count($smilies['texts'] ?? []); $tpl = Renderer::getMarkupTemplate('smilies.tpl'); return Renderer::replaceMacros($tpl, [ diff --git a/src/Module/Special/DisplayNotFound.php b/src/Module/Special/DisplayNotFound.php index 293f40aa06..8af4435cdd 100644 --- a/src/Module/Special/DisplayNotFound.php +++ b/src/Module/Special/DisplayNotFound.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Special; diff --git a/src/Module/Special/HTTPException.php b/src/Module/Special/HTTPException.php index 571c16935c..f92d33c18a 100644 --- a/src/Module/Special/HTTPException.php +++ b/src/Module/Special/HTTPException.php @@ -1,33 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Special; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\Request; use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\Session\Model\UserSession; use Friendica\Core\System; use Friendica\Module\Response; +use Friendica\Network\HTTPException as NetworkHTTPException; use Psr\Log\LoggerInterface; +use Throwable; /** * This special module displays HTTPException when they are thrown in modules. @@ -40,7 +29,7 @@ class HTTPException protected $l10n; /** @var LoggerInterface */ protected $logger; - /** @var App\Arguments */ + /** @var Arguments */ protected $args; /** @var bool */ protected $isSiteAdmin; @@ -49,7 +38,7 @@ class HTTPException /** @var string */ protected $requestId; - public function __construct(L10n $l10n, LoggerInterface $logger, App\Arguments $args, UserSession $session, App\Request $request, array $server = []) + public function __construct(L10n $l10n, LoggerInterface $logger, Arguments $args, UserSession $session, Request $request, array $server = []) { $this->logger = $logger; $this->l10n = $l10n; @@ -64,11 +53,9 @@ class HTTPException * * Fills in the blanks if title or descriptions aren't provided by the exception. * - * @param \Friendica\Network\HTTPException $e - * * @return array ['$title' => ..., '$description' => ...] */ - private function getVars(\Friendica\Network\HTTPException $e) + private function getVars(NetworkHTTPException $e) { // Explanations are mostly taken from https://en.wikipedia.org/wiki/List_of_HTTP_status_codes $vars = [ @@ -90,11 +77,9 @@ class HTTPException /** * Displays a bare message page with no theming at all. * - * @param \Friendica\Network\HTTPException $e - * * @throws \Exception */ - public function rawContent(\Friendica\Network\HTTPException $e) + public function rawContent(NetworkHTTPException $e) { $content = ''; @@ -103,7 +88,7 @@ class HTTPException try { $tpl = Renderer::getMarkupTemplate('http_status.tpl'); $content = Renderer::replaceMacros($tpl, $vars); - } catch (\Exception $e) { + } catch (Throwable $th) { $vars = array_map('htmlentities', $vars); $content = "

                              {$vars['$title']}

                              {$vars['$message']}

                              "; if ($this->isSiteAdmin) { @@ -125,12 +110,10 @@ class HTTPException /** * Returns a content string that can be integrated in the current theme. * - * @param \Friendica\Network\HTTPException $e - * * @return string * @throws \Exception */ - public function content(\Friendica\Network\HTTPException $e): string + public function content(NetworkHTTPException $e): string { if ($e->getCode() >= 400) { $this->logger->debug('Exit with error', diff --git a/src/Module/Special/Options.php b/src/Module/Special/Options.php index 26a21f0482..a785c1abaf 100644 --- a/src/Module/Special/Options.php +++ b/src/Module/Special/Options.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Special; diff --git a/src/Module/Statistics.php b/src/Module/Statistics.php index d0d46a1ab2..6540730e01 100644 --- a/src/Module/Statistics.php +++ b/src/Module/Statistics.php @@ -1,33 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; use Friendica\BaseModule; -use Friendica\Core\Addon; +use Friendica\Core\Addon\AddonHelper; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs; use Friendica\Core\L10n; -use Friendica\Core\System; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; @@ -38,13 +25,27 @@ class Statistics extends BaseModule protected $config; /** @var IManageKeyValuePairs */ protected $keyValue; + private AddonHelper $addonHelper; - public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, IManageConfigValues $config, IManageKeyValuePairs $keyValue, Response $response, array $server, array $parameters = []) - { + public function __construct( + L10n $l10n, + BaseURL $baseUrl, + Arguments $args, + LoggerInterface $logger, + Profiler $profiler, + IManageConfigValues $config, + IManageKeyValuePairs $keyValue, + AddonHelper $addonHelper, + Response $response, + array $server, + array $parameters = [] + ) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->config = $config; - $this->keyValue = $keyValue; + $this->config = $config; + $this->keyValue = $keyValue; + $this->addonHelper = $addonHelper; + if (!$this->config->get("system", "nodeinfo")) { throw new NotFoundException(); } @@ -52,22 +53,21 @@ class Statistics extends BaseModule protected function rawContent(array $request = []) { - $registration_open = - intval($this->config->get('config', 'register_policy')) !== Register::CLOSED + $registration_open = Register::getPolicy() !== Register::CLOSED && !$this->config->get('config', 'invitation_only'); /// @todo mark the "service" addons and load them dynamically here $services = [ - 'appnet' => Addon::isEnabled('appnet'), - 'bluesky' => Addon::isEnabled('bluesky'), - 'dreamwidth' => Addon::isEnabled('dreamwidth'), - 'gnusocial' => Addon::isEnabled('gnusocial'), - 'libertree' => Addon::isEnabled('libertree'), - 'livejournal' => Addon::isEnabled('livejournal'), - 'pumpio' => Addon::isEnabled('pumpio'), - 'twitter' => Addon::isEnabled('twitter'), - 'tumblr' => Addon::isEnabled('tumblr'), - 'wordpress' => Addon::isEnabled('wordpress'), + 'appnet' => $this->addonHelper->isAddonEnabled('appnet'), + 'bluesky' => $this->addonHelper->isAddonEnabled('bluesky'), + 'dreamwidth' => $this->addonHelper->isAddonEnabled('dreamwidth'), + 'gnusocial' => $this->addonHelper->isAddonEnabled('gnusocial'), + 'libertree' => $this->addonHelper->isAddonEnabled('libertree'), + 'livejournal' => $this->addonHelper->isAddonEnabled('livejournal'), + 'pumpio' => $this->addonHelper->isAddonEnabled('pumpio'), + 'twitter' => $this->addonHelper->isAddonEnabled('twitter'), + 'tumblr' => $this->addonHelper->isAddonEnabled('tumblr'), + 'wordpress' => $this->addonHelper->isAddonEnabled('wordpress'), ]; $statistics = array_merge([ diff --git a/src/Module/Stats.php b/src/Module/Stats.php new file mode 100644 index 0000000000..c8122177eb --- /dev/null +++ b/src/Module/Stats.php @@ -0,0 +1,239 @@ +config = $config; + $this->keyValue = $keyValue; + $this->dba = $dba; + $this->addonHelper = $addonHelper; + } + + protected function content(array $request = []): string + { + if (!$this->isAllowed($request)) { + throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); + } + return ''; + } + + protected function rawContent(array $request = []) + { + if (!$this->isAllowed($request)) { + return; + } + + $report = $this->dba->selectFirst('report', ['created'], [], ['order' => ['created' => true]]); + if (!empty($report)) { + $report_datetime = DateTimeFormat::utc($report['created'], DateTimeFormat::JSON); + $report_timestamp = strtotime($report['created']); + } else { + $report_datetime = ''; + $report_timestamp = 0; + } + + $statistics = [ + 'cron' => [ + 'lastExecution' => [ + 'datetime' => date(DateTimeFormat::JSON, (int)$this->keyValue->get('last_cron')), + 'timestamp' => (int)$this->keyValue->get('last_cron'), + ], + ], + 'worker' => [ + 'lastExecution' => [ + 'datetime' => DateTimeFormat::utc($this->keyValue->get('last_worker_execution'), DateTimeFormat::JSON), + 'timestamp' => strtotime($this->keyValue->get('last_worker_execution')), + ], + 'jpm' => [ + 1 => $this->dba->count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - 1 minute')]), + 3 => round($this->dba->count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - 3 minute')]) / 3), + 5 => round($this->dba->count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - 5 minute')]) / 5), + ], + 'active' => [], + 'deferred' => [], + 'total' => [], + ], + 'jetstream' => [ + 'drift' => intval($this->keyValue->get('jetstream_drift')), + 'did_count' => intval($this->keyValue->get('jetstream_did_count')), + 'did_limit' => intval($this->keyValue->get('jetstream_did_limit')), + 'messages' => intval($this->keyValue->get('jetstream_messages')), + 'timestamp' => intval($this->keyValue->get('jetstream_timestamp')), + ], + 'users' => [ + 'total' => intval($this->keyValue->get('nodeinfo_total_users')), + 'activeWeek' => intval($this->keyValue->get('nodeinfo_active_users_weekly')), + 'activeMonth' => intval($this->keyValue->get('nodeinfo_active_users_monthly')), + 'activeHalfyear' => intval($this->keyValue->get('nodeinfo_active_users_halfyear')), + 'pending' => Register::getPendingCount(), + ], + 'posts' => [ + 'inbound' => [ + 'posts' => intval($this->keyValue->get('nodeinfo_total_posts')) - intval($this->keyValue->get('nodeinfo_local_posts')), + 'comments' => intval($this->keyValue->get('nodeinfo_total_comments')) - intval($this->keyValue->get('nodeinfo_local_comments')), + ], + 'outbound' => [ + 'posts' => intval($this->keyValue->get('nodeinfo_local_posts')), + 'comments' => intval($this->keyValue->get('nodeinfo_local_comments')), + ], + ], + 'packets' => [ + 'inbound' => [ + Protocol::ACTIVITYPUB => intval($this->keyValue->get('stats_packets_inbound_' . Protocol::ACTIVITYPUB) ?? 0), + Protocol::DFRN => intval($this->keyValue->get('stats_packets_inbound_' . Protocol::DFRN) ?? 0), + Protocol::DIASPORA => intval($this->keyValue->get('stats_packets_inbound_' . Protocol::DIASPORA) ?? 0), + Protocol::OSTATUS => intval($this->keyValue->get('stats_packets_inbound_' . Protocol::OSTATUS) ?? 0), + Protocol::FEED => intval($this->keyValue->get('stats_packets_inbound_' . Protocol::FEED) ?? 0), + Protocol::MAIL => intval($this->keyValue->get('stats_packets_inbound_' . Protocol::MAIL) ?? 0), + ], + 'outbound' => [ + Protocol::ACTIVITYPUB => intval($this->keyValue->get('stats_packets_outbound_' . Protocol::ACTIVITYPUB) ?? 0), + Protocol::DFRN => intval($this->keyValue->get('stats_packets_outbound_' . Protocol::DFRN) ?? 0), + Protocol::DIASPORA => intval($this->keyValue->get('stats_packets_outbound_' . Protocol::DIASPORA) ?? 0), + Protocol::OSTATUS => intval($this->keyValue->get('stats_packets_outbound_' . Protocol::OSTATUS) ?? 0), + Protocol::FEED => intval($this->keyValue->get('stats_packets_outbound_' . Protocol::FEED) ?? 0), + Protocol::MAIL => intval($this->keyValue->get('stats_packets_outbound_' . Protocol::MAIL) ?? 0), + ] + ], + 'reports' => [ + 'newest' => [ + 'datetime' => $report_datetime, + 'timestamp' => $report_timestamp, + ], + 'open' => $this->dba->count('report', ['status' => Report::STATUS_OPEN]), + 'closed' => $this->dba->count('report', ['status' => Report::STATUS_CLOSED]), + ], + 'update' => [ + 'available' => Update::isAvailable(), + 'available_version' => Update::getAvailableVersion(), + 'status' => Update::getStatus(), + 'db_status' => DBStructure::getUpdateStatus(), + ], + 'server' => [ + 'version' => App::VERSION, + 'php' => [ + 'version' => phpversion(), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size'), + 'memory_limit' => ini_get('memory_limit'), + ], + 'database' => [ + 'max_allowed_packet' => DBA::getVariable('max_allowed_packet'), + ], + ], + ]; + + if ($this->addonHelper->isAddonEnabled('bluesky')) { + $statistics['packets']['inbound'][Protocol::BLUESKY] = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::BLUESKY) ?? 0); + $statistics['packets']['outbound'][Protocol::BLUESKY] = intval($this->keyValue->get('stats_packets_outbound_' . Protocol::BLUESKY) ?? 0); + } + if ($this->addonHelper->isAddonEnabled('tumblr')) { + $statistics['packets']['inbound'][Protocol::TUMBLR] = intval($this->keyValue->get('stats_packets_inbound_' . Protocol::TUMBLR) ?? 0); + $statistics['packets']['outbound'][Protocol::TUMBLR] = intval($this->keyValue->get('stats_packets_outbound_' . Protocol::TUMBLR) ?? 0); + } + + $statistics = $this->getJobsPerPriority($statistics); + + $this->jsonExit($statistics); + } + + private function isAllowed(array $request): bool + { + return empty(!$request['key']) && $request['key'] == $this->config->get('system', 'stats_key'); + } + + private function getJobsPerPriority(array $statistics): array + { + $statistics['worker']['active'] = $statistics['worker']['total'] = [ + Worker::PRIORITY_UNDEFINED => 0, + Worker::PRIORITY_CRITICAL => 0, + Worker::PRIORITY_HIGH => 0, + Worker::PRIORITY_MEDIUM => 0, + Worker::PRIORITY_LOW => 0, + Worker::PRIORITY_NEGLIGIBLE => 0, + 'total' => 0, + ]; + + for ($i = 1; $i <= $this->config->get('system', 'worker_defer_limit'); $i++) { + $statistics['worker']['deferred'][$i] = 0; + } + $statistics['worker']['deferred']['total'] = 0; + + $jobs = $this->dba->p("SELECT COUNT(*) AS `entries`, `priority` FROM `workerqueue` WHERE NOT `done` AND `retrial` = ? GROUP BY `priority`", 0); + while ($entry = $this->dba->fetch($jobs)) { + $running = $this->dba->count('workerqueue-view', ['priority' => $entry['priority']]); + $statistics['worker']['active']['total'] += $running; + $statistics['worker']['active'][$entry['priority']] = $running; + $statistics['worker']['total']['total'] += $entry['entries']; + $statistics['worker']['total'][$entry['priority']] = $entry['entries']; + } + $this->dba->close($jobs); + $statistics['worker']['active'][Worker::PRIORITY_UNDEFINED] = max(0, Worker::activeWorkers() - $statistics['worker']['active']['total']); + + $jobs = $this->dba->p("SELECT COUNT(*) AS `entries`, `retrial` FROM `workerqueue` WHERE NOT `done` AND `retrial` > ? GROUP BY `retrial`", 0); + while ($entry = $this->dba->fetch($jobs)) { + $statistics['worker']['deferred']['total'] += $entry['entries']; + $statistics['worker']['deferred'][$entry['retrial']] = $entry['entries']; + } + $this->dba->close($jobs); + + return $statistics; + } +} diff --git a/src/Module/StatsCaching.php b/src/Module/StatsCaching.php new file mode 100644 index 0000000000..668d26e021 --- /dev/null +++ b/src/Module/StatsCaching.php @@ -0,0 +1,108 @@ +config = $config; + $this->cache = $cache; + $this->lock = $lock; + } + + private function isAllowed(array $request): bool + { + return !empty($request['key']) && $request['key'] == $this->config->get('system', 'stats_key'); + } + + /** + * @throws NotFoundException In case the rquest isn't allowed + */ + protected function content(array $request = []): string + { + if (!$this->isAllowed($request)) { + throw new HTTPException\NotFoundException($this->l10n->t('Page not found.')); + } + return ''; + } + + protected function rawContent(array $request = []) + { + if (!$this->isAllowed($request)) { + return; + } + + $data = []; + + // OPcache + if (function_exists('opcache_get_status')) { + $status = opcache_get_status(false); + $data['opcache'] = [ + 'enabled' => $status['opcache_enabled'] ?? false, + 'hit_rate' => $status['opcache_statistics']['opcache_hit_rate'] ?? null, + 'used_memory' => $status['memory_usage']['used_memory'] ?? null, + 'free_memory' => $status['memory_usage']['free_memory'] ?? null, + 'num_cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? null, + ]; + } else { + $data['opcache'] = [ + 'enabled' => false, + ]; + } + + if ($this->cache instanceof ICanCacheInMemory) { + $data['cache'] = [ + 'type' => $this->cache->getName(), + 'stats' => $this->cache->getStats(), + ]; + } else { + $data['cache'] = [ + 'type' => $this->cache->getName(), + ]; + } + + if ($this->lock instanceof CacheLock) { + $data['lock'] = [ + 'type' => $this->lock->getName(), + 'stats' => $this->lock->getCacheStats(), + ]; + } else { + $data['lock'] = [ + 'type' => $this->lock->getName(), + ]; + } + + $this->response->setType('json', 'application/json; charset=utf-8'); + $this->response->addContent(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } +} diff --git a/src/Module/Theme.php b/src/Module/Theme.php index 5161725d5e..0d0dc8f289 100644 --- a/src/Module/Theme.php +++ b/src/Module/Theme.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; diff --git a/src/Module/ThemeDetails.php b/src/Module/ThemeDetails.php index fed86d1e8b..a8caeb0d83 100644 --- a/src/Module/ThemeDetails.php +++ b/src/Module/ThemeDetails.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; diff --git a/src/Module/ToggleMobile.php b/src/Module/ToggleMobile.php index 193f4566c7..f6df6902b4 100644 --- a/src/Module/ToggleMobile.php +++ b/src/Module/ToggleMobile.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; diff --git a/src/Module/Tos.php b/src/Module/Tos.php index 3b151c31f5..580a869221 100644 --- a/src/Module/Tos.php +++ b/src/Module/Tos.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; @@ -89,7 +75,7 @@ class Tos extends BaseModule $rules = "[ol]"; foreach (explode("\n", $lines) as $line) { if (trim($line)) { - $rules .= "\n[*]" . trim($line); + $rules .= "\n[li]" . trim($line); } } $rules .= "\n[/ol]\n"; diff --git a/src/Module/Update/Channel.php b/src/Module/Update/Channel.php index 8435a7cc1b..f8e1474ad7 100644 --- a/src/Module/Update/Channel.php +++ b/src/Module/Update/Channel.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Update; @@ -39,7 +25,7 @@ class Channel extends ChannelModule $o = ''; if ($this->update || $this->force) { if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) { - $items = $this->getChannelItems(); + $items = $this->getChannelItems($request, $this->session->getLocalUserId()); } else { $items = $this->getCommunityItems(); } diff --git a/src/Module/Update/Community.php b/src/Module/Update/Community.php index 05ea8f828c..d191bf71f1 100644 --- a/src/Module/Update/Community.php +++ b/src/Module/Update/Community.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * See update_profile.php for documentation */ diff --git a/src/Module/Update/Display.php b/src/Module/Update/Display.php index 57f5679790..1ac1770331 100644 --- a/src/Module/Update/Display.php +++ b/src/Module/Update/Display.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Update; @@ -55,23 +41,20 @@ class Display extends DisplayModule throw new HTTPException\NotFoundException($this->t('The requested item doesn\'t exist or has been deleted.')); } - $this->app->setProfileOwner($item['uid'] ?: $profileUid); + $this->appHelper->setProfileOwner($item['uid'] ?: $profileUid); $parentUriId = $item['parent-uri-id']; if (empty($force)) { - $browserUpdate = intval($this->pConfig->get($profileUid, 'system', 'update_interval') ?? 40000); - if ($browserUpdate >= 1000) { - $updateDate = date(DateTimeFormat::MYSQL, time() - ($browserUpdate * 2 / 1000)); + if ($this->pConfig->get($profileUid, 'system', 'update_content')) { + $updateDate = date(DateTimeFormat::MYSQL, time() - 120); if (!Post::exists([ "`parent-uri-id` = ? AND `uid` IN (?, ?) AND `received` > ?", $parentUriId, 0, $profileUid, $updateDate])) { - $this->logger->debug('No updated content. Ending process', - ['uri-id' => $uriId, 'uid' => $profileUid, 'updated' => $updateDate]); + $this->logger->debug('No updated content. Ending process', ['uri-id' => $uriId, 'uid' => $profileUid, 'updated' => $updateDate]); return ''; } else { - $this->logger->debug('Updated content found.', - ['uri-id' => $uriId, 'uid' => $profileUid, 'updated' => $updateDate]); + $this->logger->debug('Updated content found.', ['uri-id' => $uriId, 'uid' => $profileUid, 'updated' => $updateDate]); } } } else { diff --git a/src/Module/Update/Network.php b/src/Module/Update/Network.php index 983680ac0b..4c04beecb4 100644 --- a/src/Module/Update/Network.php +++ b/src/Module/Update/Network.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Update; @@ -41,12 +27,17 @@ class Network extends NetworkModule System::htmlUpdateExit($o); } - if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) { - $items = $this->getChannelItems(); - } elseif ($this->community->isTimeline($this->selectedTab)) { - $items = $this->getCommunityItems(); - } else { - $items = $this->getItems(); + try { + if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) { + $items = $this->getChannelItems($request, $this->session->getLocalUserId()); + } elseif ($this->community->isTimeline($this->selectedTab)) { + $items = $this->getCommunityItems(); + } else { + $items = $this->getItems(); + } + } catch (\Exception $e) { + $this->logger->error('Exception when fetching items', ['code' => $e->getCode(), 'message' => $e->getMessage()]); + $items = []; } $o = $this->conversation->render($items, Conversation::MODE_NETWORK, true, false, $this->getOrder(), $this->session->getLocalUserId()); diff --git a/src/Module/Update/Profile.php b/src/Module/Update/Profile.php index 4453a36ede..b9360d7360 100644 --- a/src/Module/Update/Profile.php +++ b/src/Module/Update/Profile.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\Update; @@ -37,21 +23,21 @@ class Profile extends BaseModule { protected function rawContent(array $request = []) { - $a = DI::app(); + $appHelper = DI::appHelper(); // Ensure we've got a profile owner if updating. - $a->setProfileOwner((int)($request['p'] ?? 0)); + $appHelper->setProfileOwner((int)($request['p'] ?? 0)); - if (DI::config()->get('system', 'block_public') && !DI::userSession()->getLocalUserId() && !DI::userSession()->getRemoteContactID($a->getProfileOwner())) { + if (DI::config()->get('system', 'block_public') && !DI::userSession()->getLocalUserId() && !DI::userSession()->getRemoteContactID($appHelper->getProfileOwner())) { throw new ForbiddenException(); } - $remote_contact = DI::userSession()->getRemoteContactID($a->getProfileOwner()); - $is_owner = DI::userSession()->getLocalUserId() == $a->getProfileOwner(); - $last_updated_key = "profile:" . $a->getProfileOwner() . ":" . DI::userSession()->getLocalUserId() . ":" . $remote_contact; + $remote_contact = DI::userSession()->getRemoteContactID($appHelper->getProfileOwner()); + $is_owner = DI::userSession()->getLocalUserId() == $appHelper->getProfileOwner(); + $last_updated_key = "profile:" . $appHelper->getProfileOwner() . ":" . DI::userSession()->getLocalUserId() . ":" . $remote_contact; if (!DI::userSession()->isAuthenticated()) { - $user = User::getById($a->getProfileOwner(), ['hidewall']); + $user = User::getById($appHelper->getProfileOwner(), ['hidewall']); if ($user['hidewall']) { throw new ForbiddenException(DI::l10n()->t('Access to this profile has been restricted.')); } @@ -64,7 +50,7 @@ class Profile extends BaseModule } // Get permissions SQL - if $remote_contact is true, our remote user has been pre-verified and we already have fetched their circles - $sql_extra = Item::getPermissionsSQLByUserId($a->getProfileOwner()); + $sql_extra = Item::getPermissionsSQLByUserId($appHelper->getProfileOwner()); $last_updated_array = DI::session()->get('last_updated', []); @@ -72,7 +58,7 @@ class Profile extends BaseModule $condition = ["`uid` = ? AND NOT `contact-blocked` AND NOT `contact-pending` AND `visible` AND (NOT `deleted` OR `gravity` = ?) - AND `wall` " . $sql_extra, $a->getProfileOwner(), Item::GRAVITY_ACTIVITY]; + AND `wall` " . $sql_extra, $appHelper->getProfileOwner(), Item::GRAVITY_ACTIVITY]; if ($request['force'] && !empty($request['item'])) { // When the parent is provided, we only fetch this @@ -104,7 +90,7 @@ class Profile extends BaseModule $last_updated_array[$last_updated_key] = time(); DI::session()->set('last_updated', $last_updated_array); - if ($is_owner && !$a->getProfileOwner() && ProfileModel::shouldDisplayEventList(DI::userSession()->getLocalUserId(), DI::mode())) { + if ($is_owner && !$appHelper->getProfileOwner() && ProfileModel::shouldDisplayEventList(DI::userSession()->getLocalUserId(), DI::mode())) { $o .= ProfileModel::getBirthdays(DI::userSession()->getLocalUserId()); $o .= ProfileModel::getEventsReminderHTML(DI::userSession()->getLocalUserId(), DI::userSession()->getPublicContactId()); } @@ -116,7 +102,7 @@ class Profile extends BaseModule } } - $o .= DI::conversation()->render($items, Conversation::MODE_PROFILE, $a->getProfileOwner(), false, 'received', $a->getProfileOwner()); + $o .= DI::conversation()->render($items, Conversation::MODE_PROFILE, $appHelper->getProfileOwner(), false, 'received', $appHelper->getProfileOwner()); System::htmlUpdateExit($o); } diff --git a/src/Module/User/Delegation.php b/src/Module/User/Delegation.php index 6f20b9facb..935ac83a6b 100644 --- a/src/Module/User/Delegation.php +++ b/src/Module/User/Delegation.php @@ -1,34 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\User; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; +use Friendica\AppHelper; use Friendica\BaseModule; use Friendica\Contact\Introduction\Repository\Introduction; -use Friendica\Core\Hook; use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\Database; +use Friendica\Event\Event; use Friendica\Model\Notification; use Friendica\Model\User; use Friendica\Module\Response; @@ -37,6 +25,7 @@ use Friendica\Navigation\SystemMessages; use Friendica\Network\HTTPException\ForbiddenException; use Friendica\Security\Authentication; use Friendica\Util; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; /** @@ -56,20 +45,22 @@ class Delegation extends BaseModule private $notify; /** @var Introduction */ private $intro; - /** @var App */ - private $app; + /** @var AppHelper */ + private $appHelper; + private EventDispatcherInterface $eventDispatcher; - public function __construct(App $app, Introduction $intro, Notify $notify, SystemMessages $systemMessages, Authentication $auth, Database $db, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Util\Profiler $profiler, Response $response, array $server, array $parameters = []) + public function __construct(EventDispatcherInterface $eventDispatcher, AppHelper $appHelper, Introduction $intro, Notify $notify, SystemMessages $systemMessages, Authentication $auth, Database $db, IHandleUserSessions $session, L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Util\Profiler $profiler, Response $response, array $server, array $parameters = []) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->session = $session; - $this->db = $db; - $this->auth = $auth; - $this->systemMessages = $systemMessages; - $this->notify = $notify; - $this->intro = $intro; - $this->app = $app; + $this->session = $session; + $this->db = $db; + $this->auth = $auth; + $this->systemMessages = $systemMessages; + $this->notify = $notify; + $this->intro = $intro; + $this->appHelper = $appHelper; + $this->eventDispatcher = $eventDispatcher; } protected function post(array $request = []) @@ -134,14 +125,15 @@ class Delegation extends BaseModule $this->session->clear(); - $this->auth->setForUser($this->app, $user, true, true); + $this->auth->setForUser($user, true, true); if ($limited_id) { $this->session->setSubManagedUserId($original_id); } - $ret = []; - Hook::callAll('home_init', $ret); + $this->eventDispatcher->dispatch( + new Event(Event::HOME_INIT) + ); $this->systemMessages->addNotice($this->t('You are now logged in as %s', $user['username'])); diff --git a/src/Module/User/Import.php b/src/Module/User/Import.php index 33db88da9a..74f2f9b6ad 100644 --- a/src/Module/User/Import.php +++ b/src/Module/User/Import.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\User; @@ -27,6 +13,7 @@ use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Protocol; use Friendica\Core\Renderer; +use Friendica\Core\Session\Model\UserSession; use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\Database; @@ -48,9 +35,6 @@ class Import extends \Friendica\BaseModule { const IMPORT_DEBUG = false; - /** @var App */ - private $app; - /** @var IManageConfigValues */ private $config; @@ -66,21 +50,24 @@ class Import extends \Friendica\BaseModule /** @var PermissionSet */ private $permissionSet; - public function __construct(PermissionSet $permissionSet, IManagePersonalConfigValues $pconfig, Database $database, SystemMessages $systemMessages, IManageConfigValues $config, App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + /** @var UserSession */ + private $session; + + public function __construct(UserSession $session, PermissionSet $permissionSet, IManagePersonalConfigValues $pconfig, Database $database, SystemMessages $systemMessages, IManageConfigValues $config, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); - $this->app = $app; $this->config = $config; $this->pconfig = $pconfig; $this->systemMessages = $systemMessages; $this->database = $database; $this->permissionSet = $permissionSet; + $this->session = $session; } protected function post(array $request = []) { - if ($this->config->get('config', 'register_policy') != \Friendica\Module\Register::OPEN && !$this->app->isSiteAdmin()) { + if (\Friendica\Module\Register::getPolicy() !== \Friendica\Module\Register::OPEN && !$this->session->isSiteAdmin()) { throw new HttpException\ForbiddenException($this->t('Permission denied.')); } @@ -99,7 +86,7 @@ class Import extends \Friendica\BaseModule protected function content(array $request = []): string { - if (($this->config->get('config', 'register_policy') != \Friendica\Module\Register::OPEN) && !$this->app->isSiteAdmin()) { + if ((\Friendica\Module\Register::getPolicy() !== \Friendica\Module\Register::OPEN) && !$this->session->isSiteAdmin()) { $this->systemMessages->addNotice($this->t('User imports on closed servers can only be done by an administrator.')); } @@ -392,7 +379,7 @@ class Import extends \Friendica\BaseModule $photo['data'] = hex2bin($photo['data']); $r = Photo::store( - new Image($photo['data'], $photo['type']), + new Image($photo['data'], $photo['type'], $photo['filename']), $photo['uid'], $photo['contact-id'], //0 $photo['resource-id'], $photo['filename'], $photo['album'], $photo['scale'], $photo['profile'], //1 $photo['allow_cid'], $photo['allow_gid'], $photo['deny_cid'], $photo['deny_gid'] diff --git a/src/Module/User/PortableContacts.php b/src/Module/User/PortableContacts.php index b409bd1dc3..cd96b7eb94 100644 --- a/src/Module/User/PortableContacts.php +++ b/src/Module/User/PortableContacts.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * @see https://web.archive.org/web/20160405005550/http://portablecontacts.net/draft-spec.html */ diff --git a/src/Module/Welcome.php b/src/Module/Welcome.php index ad35f31e89..de3f2310cf 100644 --- a/src/Module/Welcome.php +++ b/src/Module/Welcome.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; diff --git a/src/Module/WellKnown/HostMeta.php b/src/Module/WellKnown/HostMeta.php index d787f10986..938d31a03e 100644 --- a/src/Module/WellKnown/HostMeta.php +++ b/src/Module/WellKnown/HostMeta.php @@ -1,31 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\WellKnown; use Friendica\BaseModule; -use Friendica\Core\System; use Friendica\DI; use Friendica\Module\Response; -use Friendica\Protocol\Salmon; use Friendica\Util\Crypto; use Friendica\Util\XML; @@ -80,15 +64,8 @@ class HostMeta extends BaseModule 'href' => $domain . '/amcd' ] ], - 'Property' => [ - '@attributes' => [ - 'type' => 'http://salmon-protocol.org/ns/magic-key', - 'mk:key_id' => '1' - ], - Salmon::salmonKey($config->get('system', 'site_pubkey')) - ] ], - ], $xml, false, ['hm' => 'http://host-meta.net/xrd/1.0', 'mk' => 'http://salmon-protocol.org/ns/magic-key']); + ], $xml, false, ['hm' => 'http://host-meta.net/xrd/1.0']); $this->httpExit($xml->saveXML(), Response::TYPE_XML, 'application/xrd+xml'); } diff --git a/src/Module/WellKnown/NodeInfo.php b/src/Module/WellKnown/NodeInfo.php index 07a2a76199..4e1d3d99a0 100644 --- a/src/Module/WellKnown/NodeInfo.php +++ b/src/Module/WellKnown/NodeInfo.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\WellKnown; use Friendica\BaseModule; -use Friendica\Core\System; use Friendica\DI; /** @@ -35,10 +20,22 @@ class NodeInfo extends BaseModule { $nodeinfo = [ 'links' => [ - ['rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', - 'href' => DI::baseUrl() . '/nodeinfo/1.0'], - ['rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => DI::baseUrl() . '/nodeinfo/2.0'], + [ + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/1.0', + 'href' => DI::baseUrl() . '/nodeinfo/1.0' + ], + [ + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'href' => DI::baseUrl() . '/nodeinfo/2.0' + ], + [ + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.1', + 'href' => DI::baseUrl() . '/nodeinfo/2.1' + ], + [ + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.2', + 'href' => DI::baseUrl() . '/nodeinfo/2.2' + ], ] ]; diff --git a/src/Module/WellKnown/SecurityTxt.php b/src/Module/WellKnown/SecurityTxt.php index 0e8e552178..0a0f40eef1 100644 --- a/src/Module/WellKnown/SecurityTxt.php +++ b/src/Module/WellKnown/SecurityTxt.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\WellKnown; diff --git a/src/Module/WellKnown/XSocialRelay.php b/src/Module/WellKnown/XSocialRelay.php index 4ed1103913..8c6585b2ff 100644 --- a/src/Module/WellKnown/XSocialRelay.php +++ b/src/Module/WellKnown/XSocialRelay.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module\WellKnown; use Friendica\BaseModule; -use Friendica\Core\System; use Friendica\DI; use Friendica\Model\Search; use Friendica\Protocol\Relay; +use Friendica\Util\Strings; /** * Node subscription preferences for social relay systems @@ -43,13 +29,7 @@ class XSocialRelay extends BaseModule $userTags = []; if ($scope == Relay::SCOPE_TAGS) { - $server_tags = $config->get('system', 'relay_server_tags'); - $tagitems = explode(',', $server_tags); - - /// @todo Check if it was better to use "strtolower" on the tags - foreach ($tagitems as $tag) { - $systemTags[] = trim($tag, '# '); - } + $systemTags = Strings::getTagArrayByString($config->get('system', 'relay_server_tags')); if ($config->get('system', 'relay_user_tags')) { $userTags = Search::getUserTags(); diff --git a/src/Module/Xrd.php b/src/Module/Xrd.php index 097ec3c322..39b4e2538c 100644 --- a/src/Module/Xrd.php +++ b/src/Module/Xrd.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Module; use Friendica\BaseModule; -use Friendica\Core\System; use Friendica\DI; use Friendica\Model\Photo; use Friendica\Model\User; +use Friendica\Network\HTTPException\BadRequestException; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Protocol\ActivityNamespace; -use Friendica\Protocol\Salmon; +use Friendica\Util\Network; use Friendica\Util\XML; /** @@ -43,47 +29,43 @@ class Xrd extends BaseModule // @TODO: Replace with parameter from router if (DI::args()->getArgv()[0] == 'xrd') { if (empty($_GET['uri'])) { - return; + throw new BadRequestException(); } $uri = urldecode(trim($_GET['uri'])); - if (strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/jrd+json') !== false) { - $mode = Response::TYPE_JSON; - } else { - $mode = Response::TYPE_XML; - } + $mode = self::getAcceptedContentType($_SERVER['HTTP_ACCEPT'] ?? '', Response::TYPE_XML); } else { if (empty($_GET['resource'])) { - return; + throw new BadRequestException(); } $uri = urldecode(trim($_GET['resource'])); - if (strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/xrd+xml') !== false) { - $mode = Response::TYPE_XML; - } else { - $mode = Response::TYPE_JSON; - } + $mode = self::getAcceptedContentType($_SERVER['HTTP_ACCEPT'] ?? '', Response::TYPE_JSON); } - if (substr($uri, 0, 4) === 'http') { + if (Network::isValidHttpUrl($uri)) { $name = ltrim(basename($uri), '~'); $host = parse_url($uri, PHP_URL_HOST); - } else { + } else if (preg_match('/^[[:alpha:]][[:alnum:]+-.]+:/', $uri)) { $local = str_replace('acct:', '', $uri); if (substr($local, 0, 2) == '//') { $local = substr($local, 2); } list($name, $host) = explode('@', $local); + } else { + throw new BadRequestException(); } if (!empty($host) && $host !== DI::baseUrl()->getHost()) { - DI::logger()->notice('Invalid host name for xrd query',['host' => $host, 'uri' => $uri]); + DI::logger()->notice('Invalid host name for xrd query', ['host' => $host, 'uri' => $uri]); throw new NotFoundException('Invalid host name for xrd query: ' . $host); } header('Vary: Accept', false); + $alias = ''; + if ($name == User::getActorName()) { $owner = User::getSystemAccount(); if (empty($owner)) { @@ -113,6 +95,36 @@ class Xrd extends BaseModule } } + /** + * Detect the accepted content type. + * @todo Handle priorities (see "application/xrd+xml,text/xml;q=0.9") + * + * @param string $accept + * @param string $default + * @return string + */ + private function getAcceptedContentType(string $accept, string $default): string + { + $parts = []; + foreach (explode(',', $accept) as $part) { + $parts[] = current(explode(';', $part)); + } + + if ($parts === []) { + return $default; + } elseif (in_array('application/jrd+json', $parts) && !in_array('application/xrd+xml', $parts)) { + return Response::TYPE_JSON; + } elseif (!in_array('application/jrd+json', $parts) && in_array('application/xrd+xml', $parts)) { + return Response::TYPE_XML; + } elseif (in_array('application/json', $parts) && !in_array('text/xml', $parts)) { + return Response::TYPE_JSON; + } elseif (!in_array('application/json', $parts) && in_array('text/xml', $parts)) { + return Response::TYPE_XML; + } else { + return $default; + } + } + private function printSystemJSON(array $owner) { $baseURL = (string)$this->baseUrl; @@ -121,7 +133,12 @@ class Xrd extends BaseModule 'aliases' => [$owner['url']], 'links' => [ [ - 'rel' => 'http://webfinger.net/rel/profile-page', + 'rel' => ActivityNamespace::FEED, + 'type' => 'application/atom+xml', + 'href' => $owner['poll'] ?? $baseURL, + ], + [ + 'rel' => ActivityNamespace::WEBFINGERPROFILE, 'type' => 'text/html', 'href' => $owner['url'], ], @@ -131,28 +148,23 @@ class Xrd extends BaseModule 'href' => $owner['url'], ], [ - 'rel' => 'http://ostatus.org/schema/1.0/subscribe', - 'template' => $baseURL . '/contact/follow?url={uri}', - ], - [ - 'rel' => ActivityNamespace::FEED, - 'type' => 'application/atom+xml', - 'href' => $owner['poll'] ?? $baseURL, - ], - [ - 'rel' => 'salmon', - 'href' => $baseURL . '/salmon/' . $owner['nickname'], - ], - [ - 'rel' => 'http://microformats.org/profile/hcard', + 'rel' => ActivityNamespace::HCARD, 'type' => 'text/html', 'href' => $baseURL . '/hcard/' . $owner['nickname'], ], [ - 'rel' => 'http://joindiaspora.com/seed_location', + 'rel' => ActivityNamespace::DIASPORA_SEED, 'type' => 'text/html', 'href' => $baseURL, ], + [ + 'rel' => 'salmon', + 'href' => $baseURL . '/receive/users/' . $owner['guid'], + ], + [ + 'rel' => ActivityNamespace::OSTATUSSUB, + 'template' => $baseURL . '/contact/follow?url={uri}', + ], ] ]; header('Access-Control-Allow-Origin: *'); @@ -171,7 +183,7 @@ class Xrd extends BaseModule ], 'links' => [ [ - 'rel' => ActivityNamespace::DFRN , + 'rel' => ActivityNamespace::DFRN, 'href' => $owner['url'], ], [ @@ -180,7 +192,7 @@ class Xrd extends BaseModule 'href' => $owner['poll'], ], [ - 'rel' => 'http://webfinger.net/rel/profile-page', + 'rel' => ActivityNamespace::WEBFINGERPROFILE, 'type' => 'text/html', 'href' => $owner['url'], ], @@ -190,42 +202,30 @@ class Xrd extends BaseModule 'href' => $owner['url'], ], [ - 'rel' => 'http://microformats.org/profile/hcard', + 'rel' => ActivityNamespace::HCARD, 'type' => 'text/html', 'href' => $baseURL . '/hcard/' . $owner['nickname'], ], [ - 'rel' => 'http://webfinger.net/rel/avatar', + 'rel' => ActivityNamespace::WEBFINGERAVATAR, 'type' => $avatar['type'], 'href' => User::getAvatarUrl($owner), ], [ - 'rel' => 'http://joindiaspora.com/seed_location', + 'rel' => ActivityNamespace::DIASPORA_SEED, 'type' => 'text/html', 'href' => $baseURL, ], [ 'rel' => 'salmon', - 'href' => $baseURL . '/salmon/' . $owner['nickname'], + 'href' => $baseURL . '/receive/users/' . $owner['guid'], ], [ - 'rel' => 'http://salmon-protocol.org/ns/salmon-replies', - 'href' => $baseURL . '/salmon/' . $owner['nickname'], - ], - [ - 'rel' => 'http://salmon-protocol.org/ns/salmon-mention', - 'href' => $baseURL . '/salmon/' . $owner['nickname'] . '/mention', - ], - [ - 'rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'rel' => ActivityNamespace::OSTATUSSUB, 'template' => $baseURL . '/contact/follow?url={uri}', ], [ - 'rel' => 'magic-public-key', - 'href' => 'data:application/magic-public-key,' . Salmon::salmonKey($owner['spubkey']), - ], - [ - 'rel' => 'http://purl.org/openwebauth/v1', + 'rel' => ActivityNamespace::OPENWEBAUTH, 'type' => 'application/x-zot+json', 'href' => $baseURL . '/owa', ], @@ -250,78 +250,67 @@ class Xrd extends BaseModule '2:Alias' => $alias, '1:link' => [ '@attributes' => [ - 'rel' => 'http://purl.org/macgirvin/dfrn/1.0', + 'rel' => ActivityNamespace::DFRN, 'href' => $owner['url'] ] ], '2:link' => [ '@attributes' => [ - 'rel' => 'http://schemas.google.com/g/2010#updates-from', + 'rel' => ActivityNamespace::FEED, 'type' => 'application/atom+xml', 'href' => $owner['poll'] ] ], '3:link' => [ '@attributes' => [ - 'rel' => 'http://webfinger.net/rel/profile-page', + 'rel' => ActivityNamespace::WEBFINGERPROFILE, 'type' => 'text/html', 'href' => $owner['url'] ] ], '4:link' => [ '@attributes' => [ - 'rel' => 'http://microformats.org/profile/hcard', - 'type' => 'text/html', - 'href' => $baseURL . '/hcard/' . $owner['nickname'] + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $owner['url'] ] ], '5:link' => [ '@attributes' => [ - 'rel' => 'http://webfinger.net/rel/avatar', - 'type' => $avatar['type'], - 'href' => User::getAvatarUrl($owner) + 'rel' => ActivityNamespace::HCARD, + 'type' => 'text/html', + 'href' => $baseURL . '/hcard/' . $owner['nickname'] ] ], '6:link' => [ '@attributes' => [ - 'rel' => 'http://joindiaspora.com/seed_location', - 'type' => 'text/html', - 'href' => $baseURL + 'rel' => ActivityNamespace::WEBFINGERAVATAR, + 'type' => $avatar['type'], + 'href' => User::getAvatarUrl($owner) ] ], '7:link' => [ '@attributes' => [ - 'rel' => 'salmon', - 'href' => $baseURL . '/salmon/' . $owner['nickname'] + 'rel' => ActivityNamespace::DIASPORA_SEED, + 'type' => 'text/html', + 'href' => $baseURL ] ], '8:link' => [ '@attributes' => [ - 'rel' => 'http://salmon-protocol.org/ns/salmon-replies', - 'href' => $baseURL . '/salmon/' . $owner['nickname'] + 'rel' => 'salmon', + 'href' => $baseURL . '/receive/users/' . $owner['guid'] ] ], '9:link' => [ '@attributes' => [ - 'rel' => 'http://salmon-protocol.org/ns/salmon-mention', - 'href' => $baseURL . '/salmon/' . $owner['nickname'] . '/mention' + 'rel' => ActivityNamespace::OSTATUSSUB, + 'template' => $baseURL . '/contact/follow?url={uri}' ] ], '10:link' => [ '@attributes' => [ - 'rel' => 'http://ostatus.org/schema/1.0/subscribe', - 'template' => $baseURL . '/contact/follow?url={uri}' - ] - ], - '11:link' => [ - '@attributes' => [ - 'rel' => 'magic-public-key', - 'href' => 'data:application/magic-public-key,' . Salmon::salmonKey($owner['spubkey']) - ] - ], - '12:link' => [ - '@attributes' => [ - 'rel' => 'http://purl.org/openwebauth/v1', + 'rel' => ActivityNamespace::OPENWEBAUTH, 'type' => 'application/x-zot+json', 'href' => $baseURL . '/owa' ] diff --git a/src/Navigation/Notifications/Collection/FormattedNotifies.php b/src/Navigation/Notifications/Collection/FormattedNotifies.php index ffed8c9158..22e7a3d32c 100644 --- a/src/Navigation/Notifications/Collection/FormattedNotifies.php +++ b/src/Navigation/Notifications/Collection/FormattedNotifies.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Collection; diff --git a/src/Navigation/Notifications/Collection/Notifications.php b/src/Navigation/Notifications/Collection/Notifications.php index e9e568f67f..e422283891 100644 --- a/src/Navigation/Notifications/Collection/Notifications.php +++ b/src/Navigation/Notifications/Collection/Notifications.php @@ -1,57 +1,62 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Collection; use Friendica\BaseCollection; -use Friendica\Navigation\Notifications\Entity; +use Friendica\Navigation\Notifications\Entity\Notification as NotificationEntity; class Notifications extends BaseCollection { - /** - * @return Entity\Notification - */ - public function current(): Entity\Notification + public function current(): NotificationEntity { return parent::current(); } public function setSeen(): Notifications { - return $this->map(function (Entity\Notification $Notification) { - $Notification->setSeen(); + $notifications = $this->map(function (NotificationEntity $notification) { + $notification->setSeen(); }); + + if (!$notifications instanceof Notifications) { + // Show the possible error explicitly + throw new \Exception(sprintf( + 'BaseCollection::map() should return instance of %s, but returns %s instead.', + Notifications::class, + get_class($notifications), + )); + } + + return $notifications; } public function setDismissed(): Notifications { - return $this->map(function (Entity\Notification $Notification) { - $Notification->setDismissed(); + $notifications = $this->map(function (NotificationEntity $notification) { + $notification->setDismissed(); }); + + if (!$notifications instanceof Notifications) { + // Show the possible error explicitly + throw new \Exception(sprintf( + 'BaseCollection::map() should return instance of %s, but returns %s instead.', + Notifications::class, + get_class($notifications), + )); + } + + return $notifications; } public function countUnseen(): int { - return array_reduce($this->getArrayCopy(), function (int $carry, Entity\Notification $Notification) { - return $carry + ($Notification->seen ? 0 : 1); + return array_reduce($this->getArrayCopy(), function (int $carry, NotificationEntity $notification) { + return $carry + ($notification->seen ? 0 : 1); }, 0); } } diff --git a/src/Navigation/Notifications/Collection/Notifies.php b/src/Navigation/Notifications/Collection/Notifies.php index 8605bd8a49..9c2f5dcc74 100644 --- a/src/Navigation/Notifications/Collection/Notifies.php +++ b/src/Navigation/Notifications/Collection/Notifies.php @@ -1,43 +1,37 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Collection; use Friendica\BaseCollection; -use Friendica\Navigation\Notifications\Entity; +use Friendica\Navigation\Notifications\Entity\Notify as NotifyEntity; class Notifies extends BaseCollection { - /** - * @return Entity\Notify - */ - public function current(): Entity\Notify + public function current(): NotifyEntity { return parent::current(); } public function setSeen(): Notifies { - return $this->map(function (Entity\Notify $Notify) { - $Notify->setSeen(); + $notifies = $this->map(function (NotifyEntity $notify) { + $notify->setSeen(); }); + + if (!$notifies instanceof Notifies) { + // Show the possible error explicitly + throw new \Exception(sprintf( + 'BaseCollection::map() should return instance of %s, but returns %s instead.', + Notifies::class, + get_class($notifies), + )); + } + + return $notifies; } } diff --git a/src/Navigation/Notifications/Entity/Notification.php b/src/Navigation/Notifications/Entity/Notification.php index 5fd9499c12..a75b55eea4 100644 --- a/src/Navigation/Notifications/Entity/Notification.php +++ b/src/Navigation/Notifications/Entity/Notification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Entity; @@ -25,16 +11,16 @@ use DateTime; use Friendica\BaseEntity; /** - * @property-read $id - * @property-read $uid - * @property-read $verb - * @property-read $type - * @property-read $actorId - * @property-read $targetUriId - * @property-read $parentUriId - * @property-read $created - * @property-read $seen - * @property-read $dismissed + * @property-read int $id + * @property-read int $uid + * @property-read string $verb + * @property-read int $type + * @property-read int $actorId + * @property-read int $targetUriId + * @property-read int $parentUriId + * @property-read DateTime $created + * @property-read bool $seen + * @property-read bool $dismissed */ class Notification extends BaseEntity { @@ -45,7 +31,7 @@ class Notification extends BaseEntity /** @var string */ protected $verb; /** - * @var int One of the \Friendica\Model\Post\UserNotification::TYPE_* constant values + * @var int $type One of the \Friendica\Model\Post\UserNotification::TYPE_* constant values * @see \Friendica\Model\Post\UserNotification */ protected $type; diff --git a/src/Navigation/Notifications/Entity/Notify.php b/src/Navigation/Notifications/Entity/Notify.php index f24ea16ce6..71308772a9 100644 --- a/src/Navigation/Notifications/Entity/Notify.php +++ b/src/Navigation/Notifications/Entity/Notify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Entity; @@ -28,26 +14,27 @@ use Friendica\Core\Renderer; use Psr\Http\Message\UriInterface; /** - * @property-read $type - * @property-read $name - * @property-read $url - * @property-read $photo - * @property-read $date - * @property-read $msg - * @property-read $uid - * @property-read $link - * @property-read $itemId - * @property-read $parent - * @property-read $seen - * @property-read $verb - * @property-read $otype - * @property-read $name_cache - * @property-read $msg_cache - * @property-read $uriId - * @property-read $parentUriId - * @property-read $id + * @property-read int $type + * @property-read string $name + * @property-read UriInterface $url + * @property-read UriInterface $photo + * @property-read DateTime $date + * @property-read string|null $msg + * @property-read int $uid + * @property-read UriInterface $link + * @property-read int|null $itemId + * @property-read int|null $parent + * @property-read bool $seen + * @property-read string $verb + * @property-read string|null $otype + * @property-read string|null $name_cache + * @property-read string|null $msg_cache + * @property-read int|null $uriId + * @property-read int|null $parentUriId + * @property-read int|null $id * - * @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Entity\Notification instead + * @deprecated 2022.05 Use \Friendica\Navigation\Notifications\Entity\Notification instead + * @see \Friendica\Navigation\Notifications\Entity\Notification */ class Notify extends BaseEntity { @@ -77,7 +64,7 @@ class Notify extends BaseEntity protected $verb; /** @var string */ protected $otype; - /** @var string */ + /** @var string|null */ protected $name_cache; /** @var string|null */ protected $msg_cache; @@ -88,7 +75,7 @@ class Notify extends BaseEntity /** @var int|null */ protected $id; - public function __construct(int $type, string $name, UriInterface $url, UriInterface $photo, DateTime $date, int $uid, UriInterface $link, bool $seen, string $verb, string $otype, string $name_cache, string $msg = null, string $msg_cache = null, int $itemId = null, int $uriId = null, int $parent = null, ?int $parentUriId = null, ?int $id = null) + public function __construct(int $type, string $name, UriInterface $url, UriInterface $photo, DateTime $date, int $uid, UriInterface $link, bool $seen, string $verb, string $otype, string $name_cache = null, string $msg = null, string $msg_cache = null, int $itemId = null, int $uriId = null, int $parent = null, ?int $parentUriId = null, ?int $id = null) { $this->type = $type; $this->name = $name; @@ -118,7 +105,7 @@ class Notify extends BaseEntity public function updateMsgFromPreamble($epreamble) { $this->msg = Renderer::replaceMacros($epreamble, ['$itemlink' => $this->link->__toString()]); - $this->msg_cache = self::formatMessage($this->name_cache, BBCode::toPlaintext($this->msg, false)); + $this->msg_cache = self::formatMessage($this->name_cache ?? $this->name, BBCode::toPlaintext($this->msg, false)); } /** diff --git a/src/Navigation/Notifications/Exception/NoMessageException.php b/src/Navigation/Notifications/Exception/NoMessageException.php index f1f93162c6..27c9699067 100644 --- a/src/Navigation/Notifications/Exception/NoMessageException.php +++ b/src/Navigation/Notifications/Exception/NoMessageException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Exception; diff --git a/src/Navigation/Notifications/Exception/NotificationCreationInterceptedException.php b/src/Navigation/Notifications/Exception/NotificationCreationInterceptedException.php index 629217de6e..05f75045ee 100644 --- a/src/Navigation/Notifications/Exception/NotificationCreationInterceptedException.php +++ b/src/Navigation/Notifications/Exception/NotificationCreationInterceptedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Exception; diff --git a/src/Navigation/Notifications/Exception/UnexpectedNotificationTypeException.php b/src/Navigation/Notifications/Exception/UnexpectedNotificationTypeException.php index bf1cb38dc2..9a4203bcf4 100644 --- a/src/Navigation/Notifications/Exception/UnexpectedNotificationTypeException.php +++ b/src/Navigation/Notifications/Exception/UnexpectedNotificationTypeException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Exception; diff --git a/src/Navigation/Notifications/Factory/FormattedNavNotification.php b/src/Navigation/Notifications/Factory/FormattedNavNotification.php index 0a7054b8e7..b34e157121 100644 --- a/src/Navigation/Notifications/Factory/FormattedNavNotification.php +++ b/src/Navigation/Notifications/Factory/FormattedNavNotification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Factory; diff --git a/src/Navigation/Notifications/Factory/FormattedNotify.php b/src/Navigation/Notifications/Factory/FormattedNotify.php index 9b200dea5a..8d005c3aa3 100644 --- a/src/Navigation/Notifications/Factory/FormattedNotify.php +++ b/src/Navigation/Notifications/Factory/FormattedNotify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Factory; diff --git a/src/Navigation/Notifications/Factory/Introduction.php b/src/Navigation/Notifications/Factory/Introduction.php index 5078cac3ca..2763c6ecc0 100644 --- a/src/Navigation/Notifications/Factory/Introduction.php +++ b/src/Navigation/Notifications/Factory/Introduction.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Factory; use Exception; -use Friendica\App; use Friendica\App\BaseURL; use Friendica\BaseFactory; use Friendica\Content\Text\BBCode; @@ -58,7 +43,7 @@ class Introduction extends BaseFactory /** @var string */ private $nick; - public function __construct(LoggerInterface $logger, Database $dba, BaseURL $baseUrl, L10n $l10n, App $app, IManagePersonalConfigValues $pConfig, IHandleUserSessions $session) + public function __construct(LoggerInterface $logger, Database $dba, BaseURL $baseUrl, L10n $l10n, IManagePersonalConfigValues $pConfig, IHandleUserSessions $session) { parent::__construct($logger); @@ -67,7 +52,7 @@ class Introduction extends BaseFactory $this->l10n = $l10n; $this->pConfig = $pConfig; $this->session = $session; - $this->nick = $app->getLoggedInUserNickname() ?? ''; + $this->nick = $session->getLocalUserNickname() ?? ''; } /** @@ -155,8 +140,8 @@ class Introduction extends BaseFactory } $formattedIntroductions[] = new ValueObject\Introduction([ - 'label' => (($intro['network'] !== Protocol::OSTATUS) ? 'friend_request' : 'follower'), - 'str_type' => (($intro['network'] !== Protocol::OSTATUS) ? $this->l10n->t('Friend/Connect Request') : $this->l10n->t('New Follower')), + 'label' => 'friend_request', + 'str_type' => $this->l10n->t('Friend/Connect Request'), 'dfrn_id' => $intro['issued-id'], 'uid' => $this->session->getLocalUserId(), 'intro_id' => $intro['intro_id'], diff --git a/src/Navigation/Notifications/Factory/Notification.php b/src/Navigation/Notifications/Factory/Notification.php index e6bbc18c58..9547a19033 100644 --- a/src/Navigation/Notifications/Factory/Notification.php +++ b/src/Navigation/Notifications/Factory/Notification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Factory; diff --git a/src/Navigation/Notifications/Factory/Notify.php b/src/Navigation/Notifications/Factory/Notify.php index 40c77c84de..613703ecb8 100644 --- a/src/Navigation/Notifications/Factory/Notify.php +++ b/src/Navigation/Notifications/Factory/Notify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Factory; diff --git a/src/Navigation/Notifications/Repository/Notification.php b/src/Navigation/Notifications/Repository/Notification.php index 777832de6d..85c8895135 100644 --- a/src/Navigation/Notifications/Repository/Notification.php +++ b/src/Navigation/Notifications/Repository/Notification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Repository; @@ -29,8 +15,8 @@ use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Model\Post\UserNotification; use Friendica\Model\Verb; -use Friendica\Navigation\Notifications\Collection; -use Friendica\Navigation\Notifications\Entity; +use Friendica\Navigation\Notifications\Collection\Notifications as NotificationsCollection; +use Friendica\Navigation\Notifications\Entity\Notification as NotificationEntity; use Friendica\Navigation\Notifications\Factory; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Protocol\Activity; @@ -55,19 +41,18 @@ class Notification extends BaseRepository } /** - * @param array $condition - * @param array $params - * @return Entity\Notification * @throws NotFoundException */ - private function selectOne(array $condition, array $params = []): Entity\Notification + private function selectOne(array $condition, array $params = []): NotificationEntity { - return parent::_selectOne($condition, $params); + $fields = $this->_selectFirstRowAsArray( $condition, $params); + + return $this->factory->createFromTableRow($fields); } - private function select(array $condition, array $params = []): Collection\Notifications + private function select(array $condition, array $params = []): NotificationsCollection { - return new Collection\Notifications(parent::_select($condition, $params)->getArrayCopy()); + return new NotificationsCollection(parent::_select($condition, $params)->getArrayCopy()); } public function countForUser($uid, array $condition, array $params = []): int @@ -85,23 +70,21 @@ class Notification extends BaseRepository } /** - * @param int $id - * @return Entity\Notification * @throws NotFoundException */ - public function selectOneById(int $id): Entity\Notification + public function selectOneById(int $id): NotificationEntity { return $this->selectOne(['id' => $id]); } - public function selectOneForUser(int $uid, array $condition, array $params = []): Entity\Notification + public function selectOneForUser(int $uid, array $condition, array $params = []): NotificationEntity { $condition = DBA::mergeConditions($condition, ['uid' => $uid]); return $this->selectOne($condition, $params); } - public function selectForUser(int $uid, array $condition = [], array $params = []): Collection\Notifications + public function selectForUser(int $uid, array $condition = [], array $params = []): NotificationsCollection { $condition = DBA::mergeConditions($condition, ['uid' => $uid]); @@ -112,12 +95,9 @@ class Notification extends BaseRepository /** * Returns only the most recent notifications for the same conversation or contact * - * @param int $uid - * - * @return Collection\Notifications * @throws Exception */ - public function selectDetailedForUser(int $uid): Collection\Notifications + public function selectDetailedForUser(int $uid): NotificationsCollection { $notify_type = $this->pconfig->get($uid, 'system', 'notify_type'); if (!is_null($notify_type)) { @@ -127,11 +107,11 @@ class Notification extends BaseRepository } if (!$this->pconfig->get($uid, 'system', 'notify_like')) { - $condition = DBA::mergeConditions($condition, ['NOT `vid` IN (?, ?)', Verb::getID(\Friendica\Protocol\Activity::LIKE), Verb::getID(\Friendica\Protocol\Activity::DISLIKE)]); + $condition = DBA::mergeConditions($condition, ['NOT `vid` IN (?, ?)', Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE)]); } if (!$this->pconfig->get($uid, 'system', 'notify_announce')) { - $condition = DBA::mergeConditions($condition, ['`vid` != ?', Verb::getID(\Friendica\Protocol\Activity::ANNOUNCE)]); + $condition = DBA::mergeConditions($condition, ['`vid` != ?', Verb::getID(Activity::ANNOUNCE)]); } return $this->selectForUser($uid, $condition, ['limit' => 50, 'order' => ['id' => true]]); @@ -140,33 +120,30 @@ class Notification extends BaseRepository /** * Returns only the most recent notifications for the same conversation or contact * - * @param int $uid - * - * @return Collection\Notifications * @throws Exception */ - public function selectDigestForUser(int $uid): Collection\Notifications + public function selectDigestForUser(int $uid): NotificationsCollection { $values = [$uid]; $type_condition = ''; - $notify_type = $this->pconfig->get($uid, 'system', 'notify_type'); + $notify_type = $this->pconfig->get($uid, 'system', 'notify_type'); if (!is_null($notify_type)) { $type_condition = 'AND `type` & ? != 0'; - $values[] = $notify_type | UserNotification::TYPE_SHARED | UserNotification::TYPE_FOLLOW; + $values[] = $notify_type | UserNotification::TYPE_SHARED | UserNotification::TYPE_FOLLOW; } $like_condition = ''; if (!$this->pconfig->get($uid, 'system', 'notify_like')) { $like_condition = 'AND NOT `vid` IN (?, ?)'; - $values[] = Verb::getID(\Friendica\Protocol\Activity::LIKE); - $values[] = Verb::getID(\Friendica\Protocol\Activity::DISLIKE); + $values[] = Verb::getID(Activity::LIKE); + $values[] = Verb::getID(Activity::DISLIKE); } $announce_condition = ''; if (!$this->pconfig->get($uid, 'system', 'notify_announce')) { $announce_condition = 'AND vid != ?'; - $values[] = Verb::getID(\Friendica\Protocol\Activity::ANNOUNCE); + $values[] = Verb::getID(Activity::ANNOUNCE); } $rows = $this->db->p(" @@ -185,15 +162,20 @@ class Notification extends BaseRepository LIMIT 50 ", ...$values); - $Entities = new Collection\Notifications(); - foreach ($rows as $fields) { - $Entities[] = $this->factory->createFromTableRow($fields); + $entities = new NotificationsCollection(); + + if (!is_iterable($rows)) { + return $entities; } - return $Entities; + foreach ($rows as $fields) { + $entities[] = $this->factory->createFromTableRow($fields); + } + + return $entities; } - public function selectAllForUser(int $uid): Collection\Notifications + public function selectAllForUser(int $uid): NotificationsCollection { return $this->selectForUser($uid); } @@ -213,7 +195,7 @@ class Notification extends BaseRepository { $BaseCollection = parent::_selectByBoundaries($condition, $params, $min_id, $max_id, $limit); - return new Collection\Notifications($BaseCollection->getArrayCopy(), $BaseCollection->getTotalCount()); + return new NotificationsCollection($BaseCollection->getArrayCopy(), $BaseCollection->getTotalCount()); } public function setAllSeenForUser(int $uid, array $condition = []): bool @@ -231,11 +213,9 @@ class Notification extends BaseRepository } /** - * @param Entity\Notification $Notification - * @return Entity\Notification * @throws Exception */ - public function save(Entity\Notification $Notification): Entity\Notification + public function save(NotificationEntity $Notification): NotificationEntity { $fields = [ 'uid' => $Notification->uid, @@ -273,12 +253,12 @@ class Notification extends BaseRepository public function deleteForItem(int $itemUriId): bool { $conditionTarget = [ - 'vid' => Verb::getID(Activity::POST), + 'vid' => Verb::getID(Activity::POST), 'target-uri-id' => $itemUriId, ]; $conditionParent = [ - 'vid' => Verb::getID(Activity::POST), + 'vid' => Verb::getID(Activity::POST), 'parent-uri-id' => $itemUriId, ]; diff --git a/src/Navigation/Notifications/Repository/Notify.php b/src/Navigation/Notifications/Repository/Notify.php index b7bf818b52..40caa29437 100644 --- a/src/Navigation/Notifications/Repository/Notify.php +++ b/src/Navigation/Notifications/Repository/Notify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\Repository; @@ -27,15 +13,15 @@ use Friendica\Content\Text\BBCode; use Friendica\Content\Text\Plaintext; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; -use Friendica\Core\Hook; use Friendica\Core\L10n; -use Friendica\Core\System; use Friendica\Database\Database; use Friendica\Database\DBA; +use Friendica\Event\ArrayFilterEvent; use Friendica\Factory\Api\Mastodon\Notification as NotificationFactory; use Friendica\Model; use Friendica\Navigation\Notifications\Collection; -use Friendica\Navigation\Notifications\Entity; +use Friendica\Navigation\Notifications\Entity\Notification as NotificationEntity; +use Friendica\Navigation\Notifications\Entity\Notify as NotifyEntity; use Friendica\Navigation\Notifications\Exception; use Friendica\Navigation\Notifications\Factory; use Friendica\Network\HTTPException; @@ -43,10 +29,11 @@ use Friendica\Object\Api\Mastodon\Notification; use Friendica\Protocol\Activity; use Friendica\Util\DateTimeFormat; use Friendica\Util\Emailer; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; /** - * @deprecated since 2022.05 Use \Friendica\Navigation\Notifications\Repository\Notification instead + * @deprecated 2022.05 Use `\Friendica\Navigation\Notifications\Repository\Notification` instead */ class Notify extends BaseRepository { @@ -71,30 +58,41 @@ class Notify extends BaseRepository /** @var Factory\Notification */ protected $notification; + private EventDispatcherInterface $eventDispatcher; + protected static $table_name = 'notify'; - public function __construct(Database $database, LoggerInterface $logger, L10n $l10n, BaseURL $baseUrl, IManageConfigValues $config, IManagePersonalConfigValues $pConfig, Emailer $emailer, Factory\Notification $notification, Factory\Notify $factory = null) - { - $this->l10n = $l10n; - $this->baseUrl = $baseUrl; - $this->config = $config; - $this->pConfig = $pConfig; - $this->emailer = $emailer; - $this->notification = $notification; + public function __construct( + Database $database, + LoggerInterface $logger, + L10n $l10n, + BaseURL $baseUrl, + IManageConfigValues $config, + IManagePersonalConfigValues $pConfig, + Emailer $emailer, + Factory\Notification $notification, + EventDispatcherInterface $eventDispatcher, + Factory\Notify $factory = null + ) { + $this->l10n = $l10n; + $this->baseUrl = $baseUrl; + $this->config = $config; + $this->pConfig = $pConfig; + $this->emailer = $emailer; + $this->notification = $notification; + $this->eventDispatcher = $eventDispatcher; parent::__construct($database, $logger, $factory ?? new Factory\Notify($logger)); } /** - * @param array $condition - * @param array $params - * - * @return Entity\Notify * @throws HTTPException\NotFoundException */ - private function selectOne(array $condition, array $params = []): Entity\Notify + private function selectOne(array $condition, array $params = []): NotifyEntity { - return parent::_selectOne($condition, $params); + $fields = $this->_selectFirstRowAsArray( $condition, $params); + + return $this->factory->createFromTableRow($fields); } private function select(array $condition, array $params = []): Collection\Notifies @@ -119,10 +117,9 @@ class Notify extends BaseRepository /** * @param int $id * - * @return Entity\Notify * @throws HTTPException\NotFoundException */ - public function selectOneById(int $id): Entity\Notify + public function selectOneById(int $id): NotifyEntity { return $this->selectOne(['id' => $id]); } @@ -154,14 +151,11 @@ class Notify extends BaseRepository } /** - * @param Entity\Notify $Notify - * - * @return Entity\Notify * @throws HTTPException\NotFoundException * @throws HTTPException\InternalServerErrorException * @throws Exception\NotificationCreationInterceptedException */ - public function save(Entity\Notify $Notify): Entity\Notify + public function save(NotifyEntity $Notify): NotifyEntity { $fields = [ 'type' => $Notify->type, @@ -186,7 +180,10 @@ class Notify extends BaseRepository $this->db->update(self::$table_name, $fields, ['id' => $Notify->id]); } else { $fields['date'] = DateTimeFormat::utcNow(); - Hook::callAll('enotify_store', $fields); + + $fields = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ENOTIFY_STORE, $fields), + )->getArray(); $this->db->insert(self::$table_name, $fields); @@ -196,7 +193,7 @@ class Notify extends BaseRepository return $Notify; } - public function setAllSeenForRelatedNotify(Entity\Notify $Notify): bool + public function setAllSeenForRelatedNotify(NotifyEntity $Notify): bool { $condition = [ '(`link` = ? OR (`parent` != 0 AND `parent` = ? AND `otype` = ?)) AND `uid` = ?', @@ -219,7 +216,7 @@ class Notify extends BaseRepository * @return bool * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - function createFromArray($params) + public function createFromArray($params) { /** @var string the common prefix of a notification subject */ $subjectPrefix = $this->l10n->t('[Friendica:Notify]'); @@ -231,10 +228,10 @@ class Notify extends BaseRepository // Ensure that the important fields are set at any time $fields = ['nickname', 'account-type', 'notify-flags', 'language', 'username', 'email', 'account_removed', 'account_expired']; - $user = DBA::selectFirst('user', $fields, ['uid' => $params['uid']]); + $user = DBA::selectFirst('user', $fields, ['uid' => $params['uid']]); if (!DBA::isResult($user)) { - $this->logger->error('Unknown user', ['uid' => $params['uid']]); + $this->logger->error('Unknown user', ['uid' => $params['uid']]); return false; } @@ -258,13 +255,13 @@ class Notify extends BaseRepository if (!empty($params['cid'])) { $contact = Model\Contact::getById($params['cid'], ['url', 'name', 'photo']); if (DBA::isResult($contact)) { - $params['source_link'] = $contact['url']; - $params['source_name'] = $contact['name']; + $params['source_link'] = $contact['url']; + $params['source_name'] = $contact['name']; $params['source_photo'] = $contact['photo']; } } - $siteurl = (string)$this->baseUrl; + $siteurl = (string)$this->baseUrl; $sitename = $this->config->get('config', 'sitename'); // with $params['show_in_notification_page'] == false, the notification isn't inserted into @@ -273,9 +270,9 @@ class Notify extends BaseRepository $show_in_notification_page = isset($params['show_in_notification_page']) ? $params['show_in_notification_page'] : true; $title = $params['item']['title'] ?? ''; - $body = $params['item']['body'] ?? ''; + $body = $params['item']['body'] ?? ''; - $parent_id = $params['item']['parent'] ?? 0; + $parent_id = $params['item']['parent'] ?? 0; $parent_uri_id = $params['item']['parent-uri-id'] ?? 0; $epreamble = ''; @@ -292,10 +289,10 @@ class Notify extends BaseRepository $subject = $l10n->t('%s New mail received at %s', $subjectPrefix, $sitename); - $preamble = $l10n->t('%1$s sent you a new private message at %2$s.', $params['source_name'], $sitename); + $preamble = $l10n->t('%1$s sent you a new private message at %2$s.', $params['source_name'], $sitename); $epreamble = $l10n->t('%1$s sent you %2$s.', '[url='.$params['source_link'].']'.$params['source_name'].'[/url]', '[url=' . $itemlink . ']' . $l10n->t('a private message').'[/url]'); - $sitelink = $l10n->t('Please visit %s to view and/or reply to your private messages.'); + $sitelink = $l10n->t('Please visit %s to view and/or reply to your private messages.'); $tsitelink = sprintf($sitelink, $itemlink); $hsitelink = sprintf($sitelink, '' . $sitename . ''); @@ -316,7 +313,7 @@ class Notify extends BaseRepository $item_post_type = Model\Item::postType($item, $l10n); - $body = BBCode::toPlaintext($item['body'], false); + $body = BBCode::toPlaintext($item['body'], false); $title = Plaintext::shorten($body, 70); if (!empty($title)) { $title = '"' . trim(str_replace("\n", " ", $title)) . '"'; @@ -325,16 +322,16 @@ class Notify extends BaseRepository // First go for the general message // "George Bull's post" - $message = $l10n->t('%1$s commented on %2$s\'s %3$s %4$s'); + $message = $l10n->t('%1$s commented on %2$s\'s %3$s %4$s'); $dest_str = sprintf($message, $params['source_name'], $item['author-name'], $item_post_type, $title); // "your post" if ($item['wall']) { - $message = $l10n->t('%1$s commented on your %2$s %3$s'); + $message = $l10n->t('%1$s commented on your %2$s %3$s'); $dest_str = sprintf($message, $params['source_name'], $item_post_type, $title); - // "their post" + // "their post" } elseif ($item['author-link'] == $params['source_link']) { - $message = $l10n->t('%1$s commented on their %2$s %3$s'); + $message = $l10n->t('%1$s commented on their %2$s %3$s'); $dest_str = sprintf($message, $params['source_name'], $item_post_type, $title); } @@ -344,40 +341,42 @@ class Notify extends BaseRepository $epreamble = $dest_str; - $sitelink = $l10n->t('Please visit %s to view and/or reply to the conversation.'); + $sitelink = $l10n->t('Please visit %s to view and/or reply to the conversation.'); $tsitelink = sprintf($sitelink, $siteurl); $hsitelink = sprintf($sitelink, '' . $sitename . ''); - $itemlink = $params['link']; + $itemlink = $params['link']; break; case Model\Notification\Type::WALL: $subject = $l10n->t('%s %s posted to your profile wall', $subjectPrefix, $params['source_name']); - $preamble = $l10n->t('%1$s posted to your profile wall at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('%1$s posted to [url=%2$s]your wall[/url]', + $preamble = $l10n->t('%1$s posted to your profile wall at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + '%1$s posted to [url=%2$s]your wall[/url]', '[url='.$params['source_link'].']'.$params['source_name'].'[/url]', $params['link'] ); - $sitelink = $l10n->t('Please visit %s to view and/or reply to the conversation.'); + $sitelink = $l10n->t('Please visit %s to view and/or reply to the conversation.'); $tsitelink = sprintf($sitelink, $siteurl); $hsitelink = sprintf($sitelink, ''.$sitename.''); - $itemlink = $params['link']; + $itemlink = $params['link']; break; case Model\Notification\Type::INTRO: $itemlink = $params['link']; - $subject = $l10n->t('%s Introduction received', $subjectPrefix); + $subject = $l10n->t('%s Introduction received', $subjectPrefix); - $preamble = $l10n->t('You\'ve received an introduction from \'%1$s\' at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('You\'ve received [url=%1$s]an introduction[/url] from %2$s.', + $preamble = $l10n->t('You\'ve received an introduction from \'%1$s\' at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + 'You\'ve received [url=%1$s]an introduction[/url] from %2$s.', $itemlink, '[url='.$params['source_link'].']'.$params['source_name'].'[/url]' ); $body = $l10n->t('You may visit their profile at %s', $params['source_link']); - $sitelink = $l10n->t('Please visit %s to approve or reject the introduction.'); + $sitelink = $l10n->t('Please visit %s to approve or reject the introduction.'); $tsitelink = sprintf($sitelink, $siteurl); $hsitelink = sprintf($sitelink, ''.$sitename.''); @@ -386,8 +385,9 @@ class Notify extends BaseRepository // someone started to share with user (mostly OStatus) $subject = $l10n->t('%s A new person is sharing with you', $subjectPrefix); - $preamble = $l10n->t('%1$s is sharing with you at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('%1$s is sharing with you at %2$s', + $preamble = $l10n->t('%1$s is sharing with you at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + '%1$s is sharing with you at %2$s', '[url='.$params['source_link'].']'.$params['source_name'].'[/url]', $sitename ); @@ -396,8 +396,9 @@ class Notify extends BaseRepository // someone started to follow the user (mostly OStatus) $subject = $l10n->t('%s You have a new follower', $subjectPrefix); - $preamble = $l10n->t('You have a new follower at %2$s : %1$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('You have a new follower at %2$s : %1$s', + $preamble = $l10n->t('You have a new follower at %2$s : %1$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + 'You have a new follower at %2$s : %1$s', '[url='.$params['source_link'].']'.$params['source_name'].'[/url]', $sitename ); @@ -409,11 +410,12 @@ class Notify extends BaseRepository break; case Model\Notification\Type::SUGGEST: - $itemlink = $params['link']; - $subject = $l10n->t('%s Friend suggestion received', $subjectPrefix); + $itemlink = $params['link']; + $subject = $l10n->t('%s Friend suggestion received', $subjectPrefix); - $preamble = $l10n->t('You\'ve received a friend suggestion from \'%1$s\' at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('You\'ve received [url=%1$s]a friend suggestion[/url] for %2$s from %3$s.', + $preamble = $l10n->t('You\'ve received a friend suggestion from \'%1$s\' at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + 'You\'ve received [url=%1$s]a friend suggestion[/url] for %2$s from %3$s.', $itemlink, '[url='.$params['item']['url'].']'.$params['item']['name'].'[/url]', '[url='.$params['source_link'].']'.$params['source_name'].'[/url]' @@ -423,42 +425,44 @@ class Notify extends BaseRepository $body .= $l10n->t('Photo:').' '.$params['item']['photo']."\n"; $body .= $l10n->t('You may visit their profile at %s', $params['item']['url']); - $sitelink = $l10n->t('Please visit %s to approve or reject the suggestion.'); + $sitelink = $l10n->t('Please visit %s to approve or reject the suggestion.'); $tsitelink = sprintf($sitelink, $siteurl); $hsitelink = sprintf($sitelink, ''.$sitename.''); break; case Model\Notification\Type::CONFIRM: if ($params['verb'] == Activity::FRIEND) { // mutual connection - $itemlink = $params['link']; - $subject = $l10n->t('%s Connection accepted', $subjectPrefix); + $itemlink = $params['link']; + $subject = $l10n->t('%s Connection accepted', $subjectPrefix); - $preamble = $l10n->t('\'%1$s\' has accepted your connection request at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('%2$s has accepted your [url=%1$s]connection request[/url].', + $preamble = $l10n->t('\'%1$s\' has accepted your connection request at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + '%2$s has accepted your [url=%1$s]connection request[/url].', $itemlink, '[url='.$params['source_link'].']'.$params['source_name'].'[/url]' ); - $body = $l10n->t('You are now mutual friends and may exchange status updates, photos, and email without restriction.'); + $body = $l10n->t('You are now mutual friends and may exchange status updates, photos, and email without restriction.'); - $sitelink = $l10n->t('Please visit %s if you wish to make any changes to this relationship.'); + $sitelink = $l10n->t('Please visit %s if you wish to make any changes to this relationship.'); $tsitelink = sprintf($sitelink, $siteurl); $hsitelink = sprintf($sitelink, ''.$sitename.''); } else { // ACTIVITY_FOLLOW - $itemlink = $params['link']; - $subject = $l10n->t('%s Connection accepted', $subjectPrefix); + $itemlink = $params['link']; + $subject = $l10n->t('%s Connection accepted', $subjectPrefix); - $preamble = $l10n->t('\'%1$s\' has accepted your connection request at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('%2$s has accepted your [url=%1$s]connection request[/url].', + $preamble = $l10n->t('\'%1$s\' has accepted your connection request at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + '%2$s has accepted your [url=%1$s]connection request[/url].', $itemlink, '[url='.$params['source_link'].']'.$params['source_name'].'[/url]' ); - $body = $l10n->t('\'%1$s\' has chosen to accept you a fan, which restricts some forms of communication - such as private messaging and some profile interactions. If this is a celebrity or community page, these settings were applied automatically.', $params['source_name']); + $body = $l10n->t('\'%1$s\' has chosen to accept you a fan, which restricts some forms of communication - such as private messaging and some profile interactions. If this is a celebrity or community page, these settings were applied automatically.', $params['source_name']); $body .= "\n\n"; $body .= $l10n->t('\'%1$s\' may choose to extend this into a two-way or more permissive relationship in the future.', $params['source_name']); - $sitelink = $l10n->t('Please visit %s if you wish to make any changes to this relationship.'); + $sitelink = $l10n->t('Please visit %s if you wish to make any changes to this relationship.'); $tsitelink = sprintf($sitelink, $siteurl); $hsitelink = sprintf($sitelink, ''.$sitename.''); } @@ -467,43 +471,49 @@ class Notify extends BaseRepository case Model\Notification\Type::SYSTEM: switch($params['event']) { case 'SYSTEM_REGISTER_REQUEST': - $itemlink = $params['link']; - $subject = $l10n->t('[Friendica System Notify]') . ' ' . $l10n->t('registration request'); + $itemlink = $params['link']; + $subject = $l10n->t('[Friendica System Notify]') . ' ' . $l10n->t('registration request'); - $preamble = $l10n->t('You\'ve received a registration request from \'%1$s\' at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('You\'ve received a [url=%1$s]registration request[/url] from %2$s.', + $preamble = $l10n->t('You\'ve received a registration request from \'%1$s\' at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + 'You\'ve received a [url=%1$s]registration request[/url] from %2$s.', $itemlink, '[url='.$params['source_link'].']'.$params['source_name'].'[/url]' ); - $body = $l10n->t("Display Name: %s\nSite Location: %s\nLogin Name: %s (%s)", + $body = $l10n->t( + "Display Name: %s\nSite Location: %s\nLogin Name: %s (%s)", $params['source_name'], - $siteurl, $params['source_mail'], + $siteurl, + $params['source_mail'], $params['source_nick'] ); - $sitelink = $l10n->t('Please visit %s to approve or reject the request.'); + $sitelink = $l10n->t('Please visit %s to approve or reject the request.'); $tsitelink = sprintf($sitelink, $params['link']); $hsitelink = sprintf($sitelink, '' . $sitename . '

                              '); break; case 'SYSTEM_REGISTER_NEW': - $itemlink = $params['link']; - $subject = $l10n->t('[Friendica System Notify]') . ' ' . $l10n->t('new registration'); + $itemlink = $params['link']; + $subject = $l10n->t('[Friendica System Notify]') . ' ' . $l10n->t('new registration'); - $preamble = $l10n->t('You\'ve received a new registration from \'%1$s\' at %2$s', $params['source_name'], $sitename); - $epreamble = $l10n->t('You\'ve received a [url=%1$s]new registration[/url] from %2$s.', + $preamble = $l10n->t('You\'ve received a new registration from \'%1$s\' at %2$s', $params['source_name'], $sitename); + $epreamble = $l10n->t( + 'You\'ve received a [url=%1$s]new registration[/url] from %2$s.', $itemlink, '[url='.$params['source_link'].']'.$params['source_name'].'[/url]' ); - $body = $l10n->t("Display Name: %s\nSite Location: %s\nLogin Name: %s (%s)", + $body = $l10n->t( + "Display Name: %s\nSite Location: %s\nLogin Name: %s (%s)", $params['source_name'], - $siteurl, $params['source_mail'], + $siteurl, + $params['source_mail'], $params['source_nick'] ); - $sitelink = $l10n->t('Please visit %s to have a look at the new registration.'); + $sitelink = $l10n->t('Please visit %s to have a look at the new registration.'); $tsitelink = sprintf($sitelink, $params['link']); $hsitelink = sprintf($sitelink, '' . $sitename . '

                              '); break; @@ -523,14 +533,14 @@ class Notify extends BaseRepository private function storeAndSend(array $params, string $sitelink, string $tsitelink, string $hsitelink, string $title, string $subject, string $preamble, string $epreamble, string $body, string $itemlink, bool $show_in_notification_page): bool { - $item_id = $params['item']['id'] ?? 0; - $uri_id = $params['item']['uri-id'] ?? null; - $parent_id = $params['item']['parent'] ?? 0; + $item_id = $params['item']['id'] ?? 0; + $uri_id = $params['item']['uri-id'] ?? null; + $parent_id = $params['item']['parent'] ?? 0; $parent_uri_id = $params['item']['parent-uri-id'] ?? null; // Ensure that the important fields are set at any time $fields = ['nickname', 'account_removed', 'account_expired']; - $user = Model\User::getById($params['uid'], $fields); + $user = Model\User::getById($params['uid'], $fields); if ($user['account_removed'] || $user['account_expired']) { return false; } @@ -551,7 +561,7 @@ class Notify extends BaseRepository $subject .= " (".$nickname."@".$hostname.")"; - $h = [ + $hook_data = [ 'params' => $params, 'subject' => $subject, 'preamble' => $preamble, @@ -563,18 +573,20 @@ class Notify extends BaseRepository 'itemlink' => $itemlink ]; - Hook::callAll('enotify', $h); + $hook_data = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ENOTIFY, $hook_data), + )->getArray(); - $subject = $h['subject']; + $subject = $hook_data['subject']; - $preamble = $h['preamble']; - $epreamble = $h['epreamble']; + $preamble = $hook_data['preamble']; + $epreamble = $hook_data['epreamble']; - $body = $h['body']; + $body = $hook_data['body']; - $tsitelink = $h['tsitelink']; - $hsitelink = $h['hsitelink']; - $itemlink = $h['itemlink']; + $tsitelink = $hook_data['tsitelink']; + $hsitelink = $hook_data['hsitelink']; + $itemlink = $hook_data['itemlink']; $notify_id = 0; @@ -601,7 +613,7 @@ class Notify extends BaseRepository $this->logger->notice('sending notification email'); if (isset($params['parent']) && (intval($params['parent']) != 0)) { - $parent = Model\Post::selectFirst(['guid'], ['id' => $params['parent']]); + $parent = Model\Post::selectFirst(['guid'], ['id' => $params['parent']]); $message_id = "<" . $parent['guid'] . "@" . gethostname() . ">"; // Is this the first email notification for this parent item and user? @@ -609,13 +621,11 @@ class Notify extends BaseRepository $this->logger->info("notify_id:" . intval($notify_id) . ", parent: " . intval($params['parent']) . "uid: " . intval($params['uid'])); $fields = ['notify-id' => $notify_id, 'master-parent-uri-id' => $parent_uri_id, - 'receiver-uid' => $params['uid'], 'parent-item' => 0]; + 'receiver-uid' => $params['uid'], 'parent-item' => 0]; DBA::insert('notify-threads', $fields); $emailBuilder->setHeader('Message-ID', $message_id); - $log_msg = "No previous notification found for this parent:\n" . - " parent: {$params['parent']}\n" . " uid : {$params['uid']}\n"; - $this->logger->info($log_msg); + $this->logger->info('No previous notification found for this parent', ['parent' => $params['parent'], 'uid' => $params['uid']]); } else { // If not, just "follow" the thread. $emailBuilder->setHeader('References', $message_id); @@ -624,12 +634,12 @@ class Notify extends BaseRepository } } - $datarray = [ + $hook_data = [ 'preamble' => $preamble, 'type' => $params['type'], 'parent' => $parent_id, - 'source_name' => $params['source_name'] ?? null, - 'source_link' => $params['source_link'] ?? null, + 'source_name' => $params['source_name'] ?? null, + 'source_link' => $params['source_link'] ?? null, 'source_photo' => $params['source_photo'] ?? null, 'uid' => $params['uid'], 'hsitelink' => $hsitelink, @@ -641,30 +651,33 @@ class Notify extends BaseRepository 'headers' => $emailBuilder->getHeaders(), ]; - Hook::callAll('enotify_mail', $datarray); + $hook_data = $this->eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ENOTIFY_MAIL, $hook_data), + )->getArray(); $emailBuilder - ->withHeaders($datarray['headers']) + ->withHeaders($hook_data['headers']) ->withRecipient($params['to_email']) ->forUser([ - 'uid' => $datarray['uid'], + 'uid' => $hook_data['uid'], 'language' => $params['language'], ]) - ->withNotification($datarray['subject'], $datarray['preamble'], $datarray['title'], $datarray['body']) - ->withSiteLink($datarray['tsitelink'], $datarray['hsitelink']) - ->withItemLink($datarray['itemlink']); + ->withNotification($hook_data['subject'], $hook_data['preamble'], $hook_data['title'], $hook_data['body']) + ->withSiteLink($hook_data['tsitelink'], $hook_data['hsitelink']) + ->withItemLink($hook_data['itemlink']); // If a photo is present, add it to the email - if (!empty($datarray['source_photo'])) { + if (!empty($hook_data['source_photo'])) { $emailBuilder->withPhoto( - $datarray['source_photo'], - $datarray['source_link'] ?? $sitelink, - $datarray['source_name'] ?? $sitename); + $hook_data['source_photo'], + $hook_data['source_link'] ?? $sitelink, + $hook_data['source_name'] ?? $sitename + ); } $email = $emailBuilder->build(); - $this->logger->debug('Send mail', $datarray); + $this->logger->debug('Send mail', $hook_data); // use the Emailer class to send the message return $this->emailer->send($email); @@ -673,7 +686,7 @@ class Notify extends BaseRepository return false; } - public function shouldShowOnDesktop(Entity\Notification $Notification, string $type = null): bool + public function shouldShowOnDesktop(NotificationEntity $Notification, string $type = null): bool { if (is_null($type)) { $type = NotificationFactory::getType($Notification); @@ -695,7 +708,7 @@ class Notify extends BaseRepository // Fallback for the case when the notify type isn't set at all if (is_null($notify_type) && !in_array($type, [Notification::TYPE_RESHARE, Notification::TYPE_LIKE])) { - return true; + return true; } if (!is_null($notify_type) && ($notify_type & $Notification->type)) { @@ -705,7 +718,7 @@ class Notify extends BaseRepository return false; } - public function createFromNotification(Entity\Notification $Notification): bool + public function createFromNotification(NotificationEntity $Notification): bool { $this->logger->info('Start', ['uid' => $Notification->uid, 'id' => $Notification->id, 'type' => $Notification->type]); @@ -714,7 +727,7 @@ class Notify extends BaseRepository return false; } - $params = []; + $params = []; $params['verb'] = $Notification->verb; $params['uid'] = $Notification->uid; $params['otype'] = Model\Notification\ObjectType::ITEM; @@ -739,9 +752,12 @@ class Notify extends BaseRepository $params['source_photo'] = $contact['photo']; } - $item = Model\Post::selectFirstForUser($Notification->uid, Model\Item::ITEM_FIELDLIST, - ['uid' => [0, $Notification->uid], 'uri-id' => $Notification->targetUriId, 'deleted' => false], - ['order' => ['uid' => true]]); + $item = Model\Post::selectFirstForUser( + $Notification->uid, + Model\Item::ITEM_FIELDLIST, + ['uid' => [0, $Notification->uid], 'uri-id' => $Notification->targetUriId, 'deleted' => false], + ['order' => ['uid' => true]] + ); if (empty($item)) { $this->logger->info('Item not found', ['uri-id' => $Notification->targetUriId, 'type' => $Notification->type]); return false; @@ -761,13 +777,13 @@ class Notify extends BaseRepository // Check to see if there was already a tag notify or comment notify for this post. // If so don't create a second notification $condition = ['type' => [Model\Notification\Type::TAG_SELF, Model\Notification\Type::COMMENT, Model\Notification\Type::SHARE], - 'link' => $params['link'], 'verb' => Activity::POST]; + 'link' => $params['link'], 'verb' => Activity::POST]; if ($this->existsForUser($Notification->uid, $condition)) { $this->logger->info('Duplicate found, quitting', $condition + ['uid' => $Notification->uid]); return false; } - $body = BBCode::toPlaintext($item['body'], false); + $body = BBCode::toPlaintext($item['body'], false); $title = Plaintext::shorten($body, 70); if (!empty($title)) { $title = '"' . trim(str_replace("\n", " ", $title)) . '"'; diff --git a/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php b/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php index 8f64bf25ad..16543f9ac1 100644 --- a/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php +++ b/src/Navigation/Notifications/ValueObject/FormattedNavNotification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\ValueObject; @@ -25,6 +11,8 @@ use Friendica\BaseEntity; /** * A view-only object for printing item notifications to the frontend + * + * @property-read bool $seen */ class FormattedNavNotification extends BaseEntity { diff --git a/src/Navigation/Notifications/ValueObject/FormattedNotify.php b/src/Navigation/Notifications/ValueObject/FormattedNotify.php index 389db3cbe2..d92d39e426 100644 --- a/src/Navigation/Notifications/ValueObject/FormattedNotify.php +++ b/src/Navigation/Notifications/ValueObject/FormattedNotify.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\ValueObject; @@ -55,13 +41,13 @@ class FormattedNotify extends BaseDataTransferObject public function __construct(string $label, string $link, string $image, string $url, string $text, string $when, string $ago, bool $seen) { - $this->label = $label ?? ''; - $this->link = $link ?? ''; - $this->image = $image ?? ''; - $this->url = $url ?? ''; - $this->text = $text ?? ''; - $this->when = $when ?? ''; - $this->ago = $ago ?? ''; - $this->seen = $seen ?? false; + $this->label = $label; + $this->link = $link; + $this->image = $image; + $this->url = $url; + $this->text = $text; + $this->when = $when; + $this->ago = $ago; + $this->seen = $seen; } } diff --git a/src/Navigation/Notifications/ValueObject/Introduction.php b/src/Navigation/Notifications/ValueObject/Introduction.php index bd753870b2..36e490d0be 100644 --- a/src/Navigation/Notifications/ValueObject/Introduction.php +++ b/src/Navigation/Notifications/ValueObject/Introduction.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Navigation\Notifications\ValueObject; @@ -75,6 +61,34 @@ class Introduction implements \JsonSerializable /** @var string */ private $about; + public function __construct(array $data = []) + { + $this->label = $data['label'] ?? ''; + $this->type = $data['str_type'] ?? ''; + $this->intro_id = $data['intro_id'] ?? -1; + $this->madeBy = $data['madeBy'] ?? ''; + $this->madeByUrl = $data['madeByUrl'] ?? ''; + $this->madeByZrl = $data['madeByZrl'] ?? ''; + $this->madeByAddr = $data['madeByAddr'] ?? ''; + $this->contactId = $data['contactId'] ?? -1; + $this->photo = $data['photo'] ?? ''; + $this->name = $data['name'] ?? ''; + $this->url = $data['url'] ?? ''; + $this->zrl = $data['zrl'] ?? ''; + $this->hidden = $data['hidden'] ?? false; + $this->postNewFriend = $data['postNewFriend'] ?? ''; + $this->knowYou = $data['knowYou'] ?? false; + $this->note = $data['note'] ?? ''; + $this->request = $data['request'] ?? ''; + $this->dfrnId = -1; + $this->addr = $data['addr'] ?? ''; + $this->network = $data['network'] ?? ''; + $this->uid = $data['uid'] ?? -1; + $this->keywords = $data['keywords'] ?? ''; + $this->location = $data['location'] ?? ''; + $this->about = $data['about'] ?? ''; + } + public function getLabel(): string { return $this->label; @@ -145,7 +159,7 @@ class Introduction implements \JsonSerializable return $this->postNewFriend; } - public function getKnowYou(): string + public function getKnowYou(): bool { return $this->knowYou; } @@ -195,34 +209,6 @@ class Introduction implements \JsonSerializable return $this->about; } - public function __construct(array $data = []) - { - $this->label = $data['label'] ?? ''; - $this->type = $data['str_type'] ?? ''; - $this->intro_id = $data['intro_id'] ?? -1; - $this->madeBy = $data['madeBy'] ?? ''; - $this->madeByUrl = $data['madeByUrl'] ?? ''; - $this->madeByZrl = $data['madeByZrl'] ?? ''; - $this->madeByAddr = $data['madeByAddr'] ?? ''; - $this->contactId = $data['contactId'] ?? -1; - $this->photo = $data['photo'] ?? ''; - $this->name = $data['name'] ?? ''; - $this->url = $data['url'] ?? ''; - $this->zrl = $data['zrl'] ?? ''; - $this->hidden = $data['hidden'] ?? false; - $this->postNewFriend = $data['postNewFriend'] ?? ''; - $this->knowYou = $data['knowYou'] ?? false; - $this->note = $data['note'] ?? ''; - $this->request = $data['request'] ?? ''; - $this->dfrnId = -1; - $this->addr = $data['addr'] ?? ''; - $this->network = $data['network'] ?? ''; - $this->uid = $data['uid'] ?? -1; - $this->keywords = $data['keywords'] ?? ''; - $this->location = $data['location'] ?? ''; - $this->about = $data['about'] ?? ''; - } - /** * @inheritDoc */ diff --git a/src/Navigation/SystemMessages.php b/src/Navigation/SystemMessages.php index 139990988a..b3c7ccf96b 100644 --- a/src/Navigation/SystemMessages.php +++ b/src/Navigation/SystemMessages.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Friendica is a communications platform for integrated social communications * utilising decentralised communications and linkage to several indie social diff --git a/src/Network/Entity/MimeType.php b/src/Network/Entity/MimeType.php index 4400a9df60..a522183ce6 100644 --- a/src/Network/Entity/MimeType.php +++ b/src/Network/Entity/MimeType.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\Entity; diff --git a/src/Network/Factory/MimeType.php b/src/Network/Factory/MimeType.php index ec76eb8440..eceb76e261 100644 --- a/src/Network/Factory/MimeType.php +++ b/src/Network/Factory/MimeType.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\Factory; diff --git a/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php b/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php index 7c4670e811..c609c544b4 100644 --- a/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php +++ b/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Capability; @@ -84,23 +70,39 @@ interface ICanHandleHttpResponses */ public function isSuccess(): bool; + /** + * Returns if the URL is permanently gone (return code 410) + * + * @return bool + */ + public function isGone(): bool; + /** * @return string */ public function getUrl(): string; /** + * If the request was redirected to another URL, gets the final URL requested * @return string */ public function getRedirectUrl(): string; + /** + * If the request was redirected to another URL, indicates if the redirect is permanent. + * If the request was not redirected, returns false. + * If the request was redirected multiple times, returns true only if all of the redirects were permanent. + * + * @return bool True if the redirect is permanent + */ + public function redirectIsPermanent(): bool; + /** * Getter for body * - * @see MessageInterface::getBody() * @return string */ - public function getBody(); + public function getBodyString(); /** * @return boolean diff --git a/src/Network/HTTPClient/Capability/ICanSendHttpRequests.php b/src/Network/HTTPClient/Capability/ICanSendHttpRequests.php index 4009b57436..8e1f5bbe57 100644 --- a/src/Network/HTTPClient/Capability/ICanSendHttpRequests.php +++ b/src/Network/HTTPClient/Capability/ICanSendHttpRequests.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Capability; @@ -39,25 +25,11 @@ interface ICanSendHttpRequests * @param string $accept_content supply Accept: header with 'accept_content' as the value * @param int $timeout Timeout in seconds, default system config value or 60 seconds * @param string $cookiejar Path to cookie jar file + * @param string $request Request Type * * @return string The fetched content */ - public function fetch(string $url, string $accept_content = HttpClientAccept::DEFAULT, int $timeout = 0, string $cookiejar = ''): string; - - /** - * Fetches the whole response of an URL. - * - * Inner workings and parameters are the same as @ref fetchUrl but returns an array with - * all the information collected during the fetch. - * - * @param string $url URL to fetch - * @param string $accept_content supply Accept: header with 'accept_content' as the value - * @param int $timeout Timeout in seconds, default system config value or 60 seconds - * @param string $cookiejar Path to cookie jar file - * - * @return ICanHandleHttpResponses With all relevant information, 'body' contains the actual fetched content. - */ - public function fetchFull(string $url, string $accept_content = HttpClientAccept::DEFAULT, int $timeout = 0, string $cookiejar = ''): ICanHandleHttpResponses; + public function fetch(string $url, string $accept_content = HttpClientAccept::DEFAULT, int $timeout = 0, string $cookiejar = '', string $request = ''): string; /** * Send a GET to a URL. @@ -95,10 +67,11 @@ interface ICanSendHttpRequests * @param mixed $params POST variables (if an array is passed, it will automatically set as formular parameters) * @param array $headers HTTP headers * @param int $timeout The timeout in seconds, default system config value or 60 seconds + * @param string $request The type of the request. This is set in the user agent string * * @return ICanHandleHttpResponses The content */ - public function post(string $url, $params, array $headers = [], int $timeout = 0): ICanHandleHttpResponses; + public function post(string $url, $params, array $headers = [], int $timeout = 0, string $request = ''): ICanHandleHttpResponses; /** * Sends an HTTP request to a given url diff --git a/src/Network/HTTPClient/Client/HttpClient.php b/src/Network/HTTPClient/Client/HttpClient.php index bc22aded8b..113facc04b 100644 --- a/src/Network/HTTPClient/Client/HttpClient.php +++ b/src/Network/HTTPClient/Client/HttpClient.php @@ -1,26 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Client; +use Friendica\App; use Friendica\Core\System; use Friendica\Network\HTTPClient\Response\CurlResult; use Friendica\Network\HTTPClient\Response\GuzzleResponse; @@ -32,6 +19,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Cookie\FileCookieJar; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\TransferException; +use GuzzleHttp\Psr7\Uri; use GuzzleHttp\RequestOptions; use mattwright\URLResolver; use Psr\Http\Message\ResponseInterface; @@ -51,13 +39,16 @@ class HttpClient implements ICanSendHttpRequests private $client; /** @var URLResolver */ private $resolver; + /** @var App\BaseURL */ + private $baseUrl; - public function __construct(LoggerInterface $logger, Profiler $profiler, Client $client, URLResolver $resolver) + public function __construct(LoggerInterface $logger, Profiler $profiler, Client $client, URLResolver $resolver, App\BaseURL $baseUrl) { $this->logger = $logger; $this->profiler = $profiler; $this->client = $client; $this->resolver = $resolver; + $this->baseUrl = $baseUrl; } /** @@ -70,21 +61,23 @@ class HttpClient implements ICanSendHttpRequests $host = parse_url($url, PHP_URL_HOST); if (empty($host)) { - throw new \InvalidArgumentException('Unable to retrieve the host in URL: ' . $url); - } - - if(!filter_var($host, FILTER_VALIDATE_IP) && !@dns_get_record($host . '.', DNS_A + DNS_AAAA)) { - $this->logger->debug('URL cannot be resolved.', ['url' => $url]); + $this->logger->notice('Unable to retrieve the host in URL', ['url' => $url]); $this->profiler->stopRecording(); return CurlResult::createErrorCurl($this->logger, $url); } - if (Network::isLocalLink($url)) { + if (!filter_var($host, FILTER_VALIDATE_IP) && !@dns_get_record($host . '.', DNS_A) && !@dns_get_record($host . '.', DNS_AAAA)) { + $this->logger->info('URL cannot be resolved.', ['url' => $url]); + $this->profiler->stopRecording(); + return CurlResult::createErrorCurl($this->logger, $url); + } + + if ($this->baseUrl->isLocalUrl($url)) { $this->logger->info('Local link', ['url' => $url]); } if (strlen($url) > 1000) { - $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url]); + $this->logger->info('URL is longer than 1000 characters.', ['url' => $url]); $this->profiler->stopRecording(); return CurlResult::createErrorCurl($this->logger, substr($url, 0, 200)); } @@ -100,7 +93,7 @@ class HttpClient implements ICanSendHttpRequests } } $parts['path'] = implode('/', $parts2); - $url = Network::unparseURL($parts); + $url = (string)Uri::fromParts((array)$parts); if (Network::isUrlBlocked($url)) { $this->logger->info('Domain is blocked.', ['url' => $url]); @@ -115,7 +108,7 @@ class HttpClient implements ICanSendHttpRequests $conf[RequestOptions::COOKIES] = $jar; } - $headers = []; + $headers = ['User-Agent' => $this->getUserAgent($opts[HttpClientOptions::REQUEST] ?? '')]; if (!empty($opts[HttpClientOptions::ACCEPT_CONTENT])) { $headers['Accept'] = $opts[HttpClientOptions::ACCEPT_CONTENT]; @@ -153,8 +146,10 @@ class HttpClient implements ICanSendHttpRequests } $conf[RequestOptions::ON_HEADERS] = function (ResponseInterface $response) use ($opts) { - if (!empty($opts[HttpClientOptions::CONTENT_LENGTH]) && - (int)$response->getHeaderLine('Content-Length') > $opts[HttpClientOptions::CONTENT_LENGTH]) { + if ( + !empty($opts[HttpClientOptions::CONTENT_LENGTH]) && + (int)$response->getHeaderLine('Content-Length') > $opts[HttpClientOptions::CONTENT_LENGTH] + ) { throw new TransferException('The file is too big!'); } }; @@ -172,8 +167,10 @@ class HttpClient implements ICanSendHttpRequests $response = $this->client->request($method, $url, $conf); return new GuzzleResponse($response, $url); } catch (TransferException $exception) { - if ($exception instanceof RequestException && - $exception->hasResponse()) { + if ( + $exception instanceof RequestException && + $exception->hasResponse() + ) { return new GuzzleResponse($exception->getResponse(), $url, $exception->getCode(), ''); } else { return new CurlResult($this->logger, $url, '', ['http_code' => 500], $exception->getCode(), ''); @@ -209,7 +206,7 @@ class HttpClient implements ICanSendHttpRequests /** * {@inheritDoc} */ - public function post(string $url, $params, array $headers = [], int $timeout = 0): ICanHandleHttpResponses + public function post(string $url, $params, array $headers = [], int $timeout = 0, string $request = ''): ICanHandleHttpResponses { $opts = []; @@ -227,6 +224,10 @@ class HttpClient implements ICanSendHttpRequests $opts[HttpClientOptions::TIMEOUT] = $timeout; } + if (!empty($request)) { + $opts[HttpClientOptions::REQUEST] = $request; + } + return $this->request('post', $url, $opts); } @@ -237,7 +238,7 @@ class HttpClient implements ICanSendHttpRequests { $this->profiler->startRecording('network'); - if (Network::isLocalLink($url)) { + if ($this->baseUrl->isLocalUrl($url)) { $this->logger->debug('Local link', ['url' => $url]); } @@ -255,6 +256,7 @@ class HttpClient implements ICanSendHttpRequests $url = trim($url, "'"); + $this->resolver->setUserAgent($this->getUserAgent(HttpClientRequest::URLRESOLVER)); $urlResult = $this->resolver->resolveURL($url); if ($urlResult->didErrorOccur()) { @@ -267,25 +269,33 @@ class HttpClient implements ICanSendHttpRequests /** * {@inheritDoc} */ - public function fetch(string $url, string $accept_content = HttpClientAccept::DEFAULT, int $timeout = 0, string $cookiejar = ''): string + public function fetch(string $url, string $accept_content = HttpClientAccept::DEFAULT, int $timeout = 0, string $cookiejar = '', string $request = ''): string { - $ret = $this->fetchFull($url, $accept_content, $timeout, $cookiejar); - - return $ret->getBody(); + try { + $ret = $this->get( + $url, + $accept_content, + [ + HttpClientOptions::TIMEOUT => $timeout, + HttpClientOptions::COOKIEJAR => $cookiejar, + HttpClientOptions::REQUEST => $request, + ] + ); + return $ret->getBodyString(); + } catch (\Throwable $th) { + $this->logger->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return ''; + } } - /** - * {@inheritDoc} - */ - public function fetchFull(string $url, string $accept_content = HttpClientAccept::DEFAULT, int $timeout = 0, string $cookiejar = ''): ICanHandleHttpResponses + private function getUserAgent(string $type = ''): string { - return $this->get( - $url, - $accept_content, - [ - HttpClientOptions::TIMEOUT => $timeout, - HttpClientOptions::COOKIEJAR => $cookiejar - ] - ); + // @see https://developers.whatismybrowser.com/learn/browser-detection/user-agents/user-agent-best-practices + $userAgent = App::PLATFORM . '/' . App::VERSION . ' DatabaseVersion/' . DB_UPDATE_VERSION; + if ($type != '') { + $userAgent .= ' Request/' . $type; + } + $userAgent .= ' +' . $this->baseUrl; + return $userAgent; } } diff --git a/src/Network/HTTPClient/Client/HttpClientAccept.php b/src/Network/HTTPClient/Client/HttpClientAccept.php index c1ff55364c..2432633032 100644 --- a/src/Network/HTTPClient/Client/HttpClientAccept.php +++ b/src/Network/HTTPClient/Client/HttpClientAccept.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Client; @@ -29,10 +15,13 @@ class HttpClientAccept /** @var string Default value for "Accept" header */ public const DEFAULT = '*/*'; + /** @var string Accept all types with a preferences of ActivityStream content */ + public const AS_DEFAULT = 'application/activity+json,application/ld+json; profile="https://www.w3.org/ns/activitystreams",*/*;q=0.9'; + public const ATOM_XML = 'application/atom+xml,text/xml;q=0.9,*/*;q=0.8'; public const FEED_XML = 'application/atom+xml,application/rss+xml;q=0.9,application/rdf+xml;q=0.8,text/xml;q=0.7,*/*;q=0.6'; public const HTML = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; - public const IMAGE = 'image/png,image/jpeg,image/gif,image/*;q=0.9,*/*;q=0.8'; + public const IMAGE = 'image/webp,image/png,image/jpeg,image/gif,image/*;q=0.9,*/*;q=0.8'; // @todo add image/avif once our minimal supported PHP version is 8.1.0 public const JRD_JSON = 'application/jrd+json,application/json;q=0.9'; public const JSON = 'application/json,*/*;q=0.9'; public const JSON_AS = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; diff --git a/src/Network/HTTPClient/Client/HttpClientOptions.php b/src/Network/HTTPClient/Client/HttpClientOptions.php index 85f60d9fa4..e34954dfc0 100644 --- a/src/Network/HTTPClient/Client/HttpClientOptions.php +++ b/src/Network/HTTPClient/Client/HttpClientOptions.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Client; @@ -52,7 +38,10 @@ class HttpClientOptions * content_length: (int) maximum File content length */ const CONTENT_LENGTH = 'content_length'; - + /** + * Request: (string) Type of request (ActivityPub, Diaspora, server discovery, ...) + */ + const REQUEST = 'request'; /** * verify: (bool|string, default=true) Describes the SSL certificate */ diff --git a/src/Network/HTTPClient/Client/HttpClientRequest.php b/src/Network/HTTPClient/Client/HttpClientRequest.php new file mode 100644 index 0000000000..46dc05cfd5 --- /dev/null +++ b/src/Network/HTTPClient/Client/HttpClientRequest.php @@ -0,0 +1,33 @@ +. - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Factory; @@ -86,12 +72,6 @@ class HttpClient extends BaseFactory $logger->info('Curl redirect.', ['url' => $request->getUri(), 'to' => $uri, 'method' => $request->getMethod()]); }; - $userAgent = App::PLATFORM . " '" . - App::CODENAME . "' " . - App::VERSION . '-' . - DB_UPDATE_VERSION . '; ' . - $this->baseUrl; - $guzzle = new GuzzleHttp\Client([ RequestOptions::ALLOW_REDIRECTS => [ 'max' => 8, @@ -104,7 +84,9 @@ class HttpClient extends BaseFactory // Without this setting it seems as if some webservers send compressed content // This seems to confuse curl so that it shows this uncompressed. /// @todo We could possibly set this value to "gzip" or something similar - RequestOptions::DECODE_CONTENT => '', + //RequestOptions::DECODE_CONTENT => '', + // Fixes Issue 14451 - [Bluesky] Unexpected GZIP response from getTimeline endpoint + RequestOptions::DECODE_CONTENT => true, RequestOptions::FORCE_IP_RESOLVE => ($this->config->get('system', 'ipv4_resolve') ? 'v4' : null), RequestOptions::CONNECT_TIMEOUT => 10, RequestOptions::TIMEOUT => $this->config->get('system', 'curl_timeout', 60), @@ -112,22 +94,19 @@ class HttpClient extends BaseFactory // but it can be overridden RequestOptions::VERIFY => (bool)$this->config->get('system', 'verifyssl'), RequestOptions::PROXY => $proxy, - RequestOptions::HEADERS => [ - 'User-Agent' => $userAgent, - ], + RequestOptions::HEADERS => [], 'handler' => $handlerStack ?? HandlerStack::create(), ]); $resolver = new URLResolver(); - $resolver->setUserAgent($userAgent); $resolver->setMaxRedirects(10); $resolver->setRequestTimeout(10); // if the file is too large then exit - $resolver->setMaxResponseDataSize(1000000); + $resolver->setMaxResponseDataSize($this->config->get('performance', 'max_response_data_size', 1000000)); // Designate a temporary file that will store cookies during the session. // Some websites test the browser for cookie support, so this enhances results. - $resolver->setCookieJar(System::getTempPath() .'/resolver-cookie-' . Strings::getRandomName(10)); + $resolver->setCookieJar(System::getTempPath() . '/resolver-cookie-' . Strings::getRandomName(10)); - return new Client\HttpClient($logger, $this->profiler, $guzzle, $resolver); + return new Client\HttpClient($logger, $this->profiler, $guzzle, $resolver, $this->baseUrl); } } diff --git a/src/Network/HTTPClient/Response/CurlResult.php b/src/Network/HTTPClient/Response/CurlResult.php index 9db2f1304f..a38de01336 100644 --- a/src/Network/HTTPClient/Response/CurlResult.php +++ b/src/Network/HTTPClient/Response/CurlResult.php @@ -1,30 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Response; -use Friendica\Core\Logger; use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; use Friendica\Network\HTTPException\UnprocessableEntityException; -use Friendica\Util\Network; +use GuzzleHttp\Psr7\Uri; use Psr\Log\LoggerInterface; /** @@ -57,6 +42,11 @@ class CurlResult implements ICanHandleHttpResponses */ private $isSuccess; + /** + * @var boolean true (if HTTP 410 result) or false + */ + private $isGone; + /** * @var string the URL which was called */ @@ -82,6 +72,11 @@ class CurlResult implements ICanHandleHttpResponses */ private $isRedirectUrl; + /** + * @var boolean true if the URL has a permanent redirect + */ + private $redirectIsPermanent; + /** * @var boolean true if the curl request timed out */ @@ -144,6 +139,7 @@ class CurlResult implements ICanHandleHttpResponses $this->parseBodyHeader($result); $this->checkSuccess(); + $this->checkGone(); $this->checkRedirect(); $this->checkInfo(); } @@ -190,6 +186,11 @@ class CurlResult implements ICanHandleHttpResponses } } + private function checkGone() + { + $this->isGone = $this->returnCode == 410; + } + private function checkRedirect() { if (!array_key_exists('url', $this->info)) { @@ -198,7 +199,7 @@ class CurlResult implements ICanHandleHttpResponses $this->redirectUrl = $this->info['url']; } - if ($this->returnCode == 301 || $this->returnCode == 302 || $this->returnCode == 303 || $this->returnCode == 307) { + if ($this->returnCode == 301 || $this->returnCode == 302 || $this->returnCode == 303 || $this->returnCode == 307 || $this->returnCode == 308) { $redirect_parts = parse_url($this->info['redirect_url'] ?? ''); if (empty($redirect_parts)) { $redirect_parts = []; @@ -224,11 +225,12 @@ class CurlResult implements ICanHandleHttpResponses } } - $this->redirectUrl = Network::unparseURL($redirect_parts); - - $this->isRedirectUrl = true; + $this->redirectUrl = (string)Uri::fromParts((array)$redirect_parts); + $this->isRedirectUrl = true; + $this->redirectIsPermanent = $this->returnCode == 301 || $this->returnCode == 308; } else { - $this->isRedirectUrl = false; + $this->isRedirectUrl = false; + $this->redirectIsPermanent = false; } } @@ -244,7 +246,7 @@ class CurlResult implements ICanHandleHttpResponses /** {@inheritDoc} */ public function getReturnCode(): string { - return $this->returnCode; + return (string) $this->returnCode; } /** {@inheritDoc} */ @@ -317,6 +319,12 @@ class CurlResult implements ICanHandleHttpResponses return $this->isSuccess; } + /** {@inheritDoc} */ + public function isGone(): bool + { + return $this->isSuccess; + } + /** {@inheritDoc} */ public function getUrl(): string { @@ -330,7 +338,7 @@ class CurlResult implements ICanHandleHttpResponses } /** {@inheritDoc} */ - public function getBody(): string + public function getBodyString(): string { return $this->body; } @@ -341,6 +349,12 @@ class CurlResult implements ICanHandleHttpResponses return $this->isRedirectUrl; } + /** {@inheritDoc} */ + public function redirectIsPermanent(): bool + { + return $this->redirectIsPermanent; + } + /** {@inheritDoc} */ public function getErrorNumber(): int { diff --git a/src/Network/HTTPClient/Response/GuzzleResponse.php b/src/Network/HTTPClient/Response/GuzzleResponse.php index 468ab996f3..fde67cdd8c 100644 --- a/src/Network/HTTPClient/Response/GuzzleResponse.php +++ b/src/Network/HTTPClient/Response/GuzzleResponse.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPClient\Response; -use Friendica\Core\Logger; +use Friendica\DI; use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; use GuzzleHttp\Psr7\Response; use GuzzleHttp\RedirectMiddleware; @@ -38,6 +24,8 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon private $isTimeout; /** @var boolean */ private $isSuccess; + /** @var boolean */ + private $isGone; /** * @var int the error number or 0 (zero) if no error */ @@ -52,6 +40,8 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon private $redirectUrl = ''; /** @var bool */ private $isRedirectUrl = false; + /** @var bool */ + private $redirectIsPermanent = false; public function __construct(ResponseInterface $response, string $url, $errorNumber = 0, $error = '') { @@ -61,6 +51,7 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon $this->errorNumber = $errorNumber; $this->checkSuccess(); + $this->checkGone(); $this->checkRedirect($response); } @@ -74,7 +65,7 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon } if (!$this->isSuccess) { - Logger::debug('debug', ['info' => $this->getHeaders()]); + DI::logger()->debug('debug', ['info' => $this->getHeaders()]); } if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) { @@ -84,20 +75,32 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon } } + private function checkGone() + { + $this->isGone = $this->getStatusCode() == 410; + } + private function checkRedirect(ResponseInterface $response) { $headersRedirect = $response->getHeader(RedirectMiddleware::HISTORY_HEADER) ?? []; if (count($headersRedirect) > 0) { - $this->redirectUrl = $headersRedirect[0]; + $this->redirectUrl = end($headersRedirect); $this->isRedirectUrl = true; + + $this->redirectIsPermanent = true; + foreach (($response->getHeader(RedirectMiddleware::STATUS_HISTORY_HEADER) ?? []) as $history) { + if (preg_match('/30(2|3|4|7)/', $history)) { + $this->redirectIsPermanent = false; + } + } } } /** {@inheritDoc} */ public function getReturnCode(): string { - return $this->getStatusCode(); + return (string) $this->getStatusCode(); } /** {@inheritDoc} */ @@ -126,6 +129,12 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon return $this->isSuccess; } + /** {@inheritDoc} */ + public function isGone(): bool + { + return $this->isGone; + } + /** {@inheritDoc} */ public function getUrl(): string { @@ -145,6 +154,12 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon return $this->isRedirectUrl; } + /** {@inheritDoc} */ + public function redirectIsPermanent(): bool + { + return $this->redirectIsPermanent; + } + /** {@inheritDoc} */ public function getErrorNumber(): int { @@ -163,8 +178,7 @@ class GuzzleResponse extends Response implements ICanHandleHttpResponses, Respon return $this->isTimeout; } - /// @todo - fix mismatching use of "getBody()" as string here and parent "getBody()" as streaminterface - public function getBody(): string + public function getBodyString(): string { return (string) parent::getBody(); } diff --git a/src/Network/HTTPException.php b/src/Network/HTTPException.php index c2f5c4867d..5b818ef86e 100644 --- a/src/Network/HTTPException.php +++ b/src/Network/HTTPException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network; diff --git a/src/Network/HTTPException/AcceptedException.php b/src/Network/HTTPException/AcceptedException.php index d53ccdd2ea..dfa56ed8dd 100644 --- a/src/Network/HTTPException/AcceptedException.php +++ b/src/Network/HTTPException/AcceptedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/BadGatewayException.php b/src/Network/HTTPException/BadGatewayException.php index 7ec93614f1..57cce6a1dd 100644 --- a/src/Network/HTTPException/BadGatewayException.php +++ b/src/Network/HTTPException/BadGatewayException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/BadRequestException.php b/src/Network/HTTPException/BadRequestException.php index 41b7714613..01490aa340 100644 --- a/src/Network/HTTPException/BadRequestException.php +++ b/src/Network/HTTPException/BadRequestException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/ConflictException.php b/src/Network/HTTPException/ConflictException.php index 915f6b1834..ae93d0fc5e 100644 --- a/src/Network/HTTPException/ConflictException.php +++ b/src/Network/HTTPException/ConflictException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/ExpectationFailedException.php b/src/Network/HTTPException/ExpectationFailedException.php index df3ff97d6d..e5c67d748c 100644 --- a/src/Network/HTTPException/ExpectationFailedException.php +++ b/src/Network/HTTPException/ExpectationFailedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/ForbiddenException.php b/src/Network/HTTPException/ForbiddenException.php index 5fc584507c..0cf6af722c 100644 --- a/src/Network/HTTPException/ForbiddenException.php +++ b/src/Network/HTTPException/ForbiddenException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/FoundException.php b/src/Network/HTTPException/FoundException.php index 53e6ef2db7..52800e636a 100644 --- a/src/Network/HTTPException/FoundException.php +++ b/src/Network/HTTPException/FoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/GatewayTimeoutException.php b/src/Network/HTTPException/GatewayTimeoutException.php index 71141f1f86..29cfa19c6e 100644 --- a/src/Network/HTTPException/GatewayTimeoutException.php +++ b/src/Network/HTTPException/GatewayTimeoutException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/GoneException.php b/src/Network/HTTPException/GoneException.php index 9cab0f22f7..d640dd0b9f 100644 --- a/src/Network/HTTPException/GoneException.php +++ b/src/Network/HTTPException/GoneException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/ImATeapotException.php b/src/Network/HTTPException/ImATeapotException.php index 238b8d1e94..47144d8dd9 100644 --- a/src/Network/HTTPException/ImATeapotException.php +++ b/src/Network/HTTPException/ImATeapotException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/InternalServerErrorException.php b/src/Network/HTTPException/InternalServerErrorException.php index 9d671cb2d5..ee13b0c297 100644 --- a/src/Network/HTTPException/InternalServerErrorException.php +++ b/src/Network/HTTPException/InternalServerErrorException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/LengthRequiredException.php b/src/Network/HTTPException/LengthRequiredException.php index 0dd1896939..6d44ef159d 100644 --- a/src/Network/HTTPException/LengthRequiredException.php +++ b/src/Network/HTTPException/LengthRequiredException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/MethodNotAllowedException.php b/src/Network/HTTPException/MethodNotAllowedException.php index 1c8451cb3a..d6fa1110b8 100644 --- a/src/Network/HTTPException/MethodNotAllowedException.php +++ b/src/Network/HTTPException/MethodNotAllowedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/MovedPermanentlyException.php b/src/Network/HTTPException/MovedPermanentlyException.php index 7ea089f5d3..3f2c34a1d6 100644 --- a/src/Network/HTTPException/MovedPermanentlyException.php +++ b/src/Network/HTTPException/MovedPermanentlyException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/NoContentException.php b/src/Network/HTTPException/NoContentException.php index e251d6d108..5c306f948b 100644 --- a/src/Network/HTTPException/NoContentException.php +++ b/src/Network/HTTPException/NoContentException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/NonAcceptableException.php b/src/Network/HTTPException/NonAcceptableException.php index 65803d19c3..9e531ff534 100644 --- a/src/Network/HTTPException/NonAcceptableException.php +++ b/src/Network/HTTPException/NonAcceptableException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/NotFoundException.php b/src/Network/HTTPException/NotFoundException.php index 6300d7acf3..bafe877cda 100644 --- a/src/Network/HTTPException/NotFoundException.php +++ b/src/Network/HTTPException/NotFoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/NotImplementedException.php b/src/Network/HTTPException/NotImplementedException.php index bb63681616..4b559196e5 100644 --- a/src/Network/HTTPException/NotImplementedException.php +++ b/src/Network/HTTPException/NotImplementedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/NotModifiedException.php b/src/Network/HTTPException/NotModifiedException.php index a0f46c9833..829c73b04f 100644 --- a/src/Network/HTTPException/NotModifiedException.php +++ b/src/Network/HTTPException/NotModifiedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/OKException.php b/src/Network/HTTPException/OKException.php index 0a557c94ee..e566886b72 100644 --- a/src/Network/HTTPException/OKException.php +++ b/src/Network/HTTPException/OKException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/PreconditionFailedException.php b/src/Network/HTTPException/PreconditionFailedException.php index 1a8f3f71f4..84df4276d1 100644 --- a/src/Network/HTTPException/PreconditionFailedException.php +++ b/src/Network/HTTPException/PreconditionFailedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/ServiceUnavailableException.php b/src/Network/HTTPException/ServiceUnavailableException.php index 81cfa3c4e6..a2e875b74a 100644 --- a/src/Network/HTTPException/ServiceUnavailableException.php +++ b/src/Network/HTTPException/ServiceUnavailableException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/TemporaryRedirectException.php b/src/Network/HTTPException/TemporaryRedirectException.php index fe75a9db8e..4105b7bbf0 100644 --- a/src/Network/HTTPException/TemporaryRedirectException.php +++ b/src/Network/HTTPException/TemporaryRedirectException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/TooManyRequestsException.php b/src/Network/HTTPException/TooManyRequestsException.php index 0facc627a4..ef94ebe686 100644 --- a/src/Network/HTTPException/TooManyRequestsException.php +++ b/src/Network/HTTPException/TooManyRequestsException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/UnauthorizedException.php b/src/Network/HTTPException/UnauthorizedException.php index dfe8719f9f..5a3e9c78c5 100644 --- a/src/Network/HTTPException/UnauthorizedException.php +++ b/src/Network/HTTPException/UnauthorizedException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/UnprocessableEntityException.php b/src/Network/HTTPException/UnprocessableEntityException.php index 2df52d4ff4..992c40d1b9 100644 --- a/src/Network/HTTPException/UnprocessableEntityException.php +++ b/src/Network/HTTPException/UnprocessableEntityException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/HTTPException/UnsupportedMediaTypeException.php b/src/Network/HTTPException/UnsupportedMediaTypeException.php index c14044f746..6398d5a8a2 100644 --- a/src/Network/HTTPException/UnsupportedMediaTypeException.php +++ b/src/Network/HTTPException/UnsupportedMediaTypeException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network\HTTPException; diff --git a/src/Network/Probe.php b/src/Network/Probe.php index ba2ea4ab4c..471f6e313a 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Network; use DOMDocument; use DomXPath; use Exception; +use Friendica\Content\Text\HTML; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Database\DBA; use Friendica\DI; @@ -35,8 +21,10 @@ use Friendica\Model\Profile; use Friendica\Model\User; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityPub; +use Friendica\Protocol\ATProtocol; use Friendica\Protocol\Diaspora; use Friendica\Protocol\Email; use Friendica\Protocol\Feed; @@ -111,20 +99,23 @@ class Probe */ private static function rearrangeData(array $data): array { - $fields = ['name', 'given_name', 'family_name', 'nick', 'guid', 'url', 'addr', 'alias', - 'photo', 'photo_medium', 'photo_small', 'header', - 'account-type', 'community', 'keywords', 'location', 'about', 'xmpp', 'matrix', - 'hide', 'batch', 'notify', 'poll', 'request', 'confirm', 'subscribe', 'poco', - 'following', 'followers', 'inbox', 'outbox', 'sharedinbox', - 'priority', 'network', 'pubkey', 'manually-approve', 'baseurl', 'gsid']; + $fields = [ + 'name', 'given_name', 'family_name', 'nick', 'guid', 'url', 'addr', 'alias', + 'photo', 'photo_medium', 'photo_small', 'header', + 'account-type', 'community', 'keywords', 'location', 'about', 'xmpp', 'matrix', + 'hide', 'batch', 'notify', 'poll', 'request', 'confirm', 'subscribe', 'poco', + 'openwebauth', 'following', 'followers', 'inbox', 'outbox', 'sharedinbox', + 'priority', 'network', 'pubkey', 'manually-approve', 'baseurl', 'gsid' + ]; - $numeric_fields = ['gsid', 'hide', 'account-type', 'manually-approve']; + $numeric_fields = ['gsid', 'account-type']; + $boolean_fields = ['hide', 'manually-approve']; if (!empty($data['photo'])) { $data['photo'] = Network::addBasePath($data['photo'], $data['url']); if (!Network::isValidHttpUrl($data['photo'])) { - Logger::warning('Invalid URL for photo', ['url' => $data['url'], 'photo' => $data['photo']]); + DI::logger()->warning('Invalid URL for photo', ['url' => $data['url'], 'photo' => $data['photo']]); unset($data['photo']); } } @@ -134,10 +125,12 @@ class Probe if (isset($data[$field])) { if (in_array($field, $numeric_fields)) { $newdata[$field] = (int)$data[$field]; + } elseif (in_array($field, $boolean_fields)) { + $newdata[$field] = (bool)$data[$field]; } else { $newdata[$field] = trim($data[$field]); } - } elseif (!in_array($field, $numeric_fields)) { + } elseif (!in_array($field, $numeric_fields) && !in_array($field, $boolean_fields)) { $newdata[$field] = ''; } else { $newdata[$field] = null; @@ -145,7 +138,7 @@ class Probe } $newdata['networks'] = []; - foreach ([Protocol::DIASPORA, Protocol::OSTATUS] as $network) { + foreach ([Protocol::DIASPORA] as $network) { if (!empty($data['networks'][$network])) { $data['networks'][$network]['subscribe'] = $newdata['subscribe'] ?? ''; if (empty($data['networks'][$network]['baseurl'])) { @@ -154,7 +147,7 @@ class Probe $newdata['baseurl'] = $data['networks'][$network]['baseurl']; } if (!empty($newdata['baseurl'])) { - $newdata['gsid'] = $data['networks'][$network]['gsid'] = GServer::getID($newdata['baseurl']); + $newdata['gsid'] = $data['networks'][$network]['gsid'] = GServer::getRealID($newdata['baseurl']); } else { $newdata['gsid'] = $data['networks'][$network]['gsid'] = null; } @@ -211,38 +204,50 @@ class Probe // Handles the case when the hostname contains the scheme if (!parse_url($host, PHP_URL_SCHEME)) { $ssl_url = 'https://' . $host . self::HOST_META; - $url = 'http://' . $host . self::HOST_META; + $url = 'http://' . $host . self::HOST_META; } else { $ssl_url = $host . self::HOST_META; - $url = ''; + $url = ''; } $xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20); - Logger::info('Probing', ['host' => $host, 'ssl_url' => $ssl_url, 'url' => $url]); + DI::logger()->info('Probing', ['host' => $host, 'ssl_url' => $ssl_url, 'url' => $url]); $xrd = null; - $curlResult = DI::httpClient()->get($ssl_url, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout]); + try { + $curlResult = DI::httpClient()->get($ssl_url, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return []; + } + $ssl_connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); + + $host_url = $host; + if ($curlResult->isSuccess()) { - $xml = $curlResult->getBody(); + $xml = $curlResult->getBodyString(); $xrd = XML::parseString($xml, true); if (!empty($url)) { $host_url = 'https://' . $host; - } else { - $host_url = $host; } } elseif ($curlResult->isTimeout()) { - Logger::info('Probing timeout', ['url' => $ssl_url]); + DI::logger()->info('Probing timeout', ['url' => $ssl_url]); self::$isTimeout = true; return []; } - if (!is_object($xrd) && !empty($url)) { - $curlResult = DI::httpClient()->get($url, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout]); + if ($ssl_connection_error && !is_object($xrd) && !empty($url)) { + try { + $curlResult = DI::httpClient()->get($url, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return []; + } $connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isTimeout()) { - Logger::info('Probing timeout', ['url' => $url]); + DI::logger()->info('Probing timeout', ['url' => $url]); self::$isTimeout = true; return []; } elseif ($connection_error && $ssl_connection_error) { @@ -250,18 +255,18 @@ class Probe return []; } - $xml = $curlResult->getBody(); - $xrd = XML::parseString($xml, true); - $host_url = 'http://'.$host; + $xml = $curlResult->getBodyString(); + $xrd = XML::parseString($xml, true); + $host_url = 'http://' . $host; } if (!is_object($xrd)) { - Logger::info('No xrd object found', ['host' => $host]); + DI::logger()->info('No xrd object found', ['host' => $host]); return []; } $links = XML::elementToArray($xrd); if (!isset($links['xrd']['link'])) { - Logger::info('No xrd data found', ['host' => $host]); + DI::logger()->info('No xrd data found', ['host' => $host]); return []; } @@ -284,13 +289,13 @@ class Probe } if (Network::isUrlBlocked($host_url)) { - Logger::info('Domain is blocked', ['url' => $host]); + DI::logger()->info('Domain is blocked', ['url' => $host]); return []; } self::$baseurl = $host_url; - Logger::info('Probing successful', ['host' => $host]); + DI::logger()->info('Probing successful', ['host' => $host]); return $lrdd; } @@ -311,7 +316,7 @@ class Probe $webfinger = $data['webfinger']; if (empty($webfinger['links'])) { - Logger::info('No webfinger links found', ['uri' => $uri]); + DI::logger()->info('No webfinger links found', ['uri' => $uri]); return []; } @@ -325,7 +330,7 @@ class Probe foreach ($webfinger['aliases'] as $alias) { $data[] = [ '@attributes' => [ - 'rel' => 'alias', + 'rel' => 'alias', 'href' => $alias, ] ]; @@ -378,12 +383,18 @@ class Probe unset($data['networks']); if (!empty($data['network'])) { $networks[$data['network']] = $data; + $ap_profile['guid'] = $ap_profile['guid'] ?? $data['guid'] ?? null; + $ap_profile['about'] = $ap_profile['about'] ?? $data['about'] ?? null; + $ap_profile['keywords'] = $data['keywords'] ?? null; + $ap_profile['location'] = $data['location'] ?? null; + $ap_profile['poco'] = $data['poco'] ?? null; + $ap_profile['openwebauth'] = $data['openwebauth'] ?? null; } - $data = $ap_profile; + $data = $ap_profile; $data['networks'] = $networks; } elseif (!empty($ap_profile)) { $ap_profile['batch'] = ''; - $data = array_merge($ap_profile, $data); + $data = array_merge($ap_profile, $data); } } else { $data = $ap_profile; @@ -425,8 +436,8 @@ class Probe } if (!empty($data['baseurl']) && empty($data['gsid'])) { - $data['gsid'] = GServer::getID($data['baseurl']); - } + $data['gsid'] = GServer::getRealID($data['baseurl']); + } // Ensure that local connections always are DFRN if (($network == '') && ($data['network'] != Protocol::PHANTOM) && (self::ownHost($data['baseurl'] ?? '') || self::ownHost($data['url']))) { @@ -449,7 +460,12 @@ class Probe */ private static function getHideStatus(string $url): bool { - $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::CONTENT_LENGTH => 1000000]); + try { + $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::CONTENT_LENGTH => 1000000, HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return false; + } if (!$curlResult->isSuccess()) { return false; } @@ -459,7 +475,7 @@ class Probe return false; } - $body = $curlResult->getBody(); + $body = $curlResult->getBodyString(); if (empty($body)) { return false; } @@ -518,6 +534,8 @@ class Probe foreach ($webfinger['links'] as $link) { if (!empty($link['template']) && ($link['rel'] === ActivityNamespace::OSTATUSSUB)) { $result['subscribe'] = $link['template']; + } elseif (!empty($link['href']) && ($link['rel'] === ActivityNamespace::OPENWEBAUTH) && ($link['type'] === 'application/x-zot+json')) { + $result['openwebauth'] = $link['href']; } } @@ -535,6 +553,7 @@ class Probe public static function getWebfingerArray(string $uri): array { $parts = parse_url($uri); + $lrdd = []; if (!empty($parts['scheme']) && !empty($parts['host'])) { $host = $parts['host']; @@ -547,20 +566,23 @@ class Probe $nick = ''; $addr = ''; - $path_parts = explode('/', trim($parts['path'] ?? '', '/')); - if (!empty($path_parts)) { + $path_parts = []; + + if (array_key_exists('path', $parts) && trim(strval($parts['path']), '/') !== '') { + $path_parts = explode('/', trim($parts['path'], '/')); + $nick = ltrim(end($path_parts), '@'); $addr = $nick . '@' . $host; } $webfinger = self::getWebfinger($parts['scheme'] . '://' . $host . self::WEBFINGER, HttpClientAccept::JRD_JSON, $uri, $addr); - if (empty($webfinger)) { + if (empty($webfinger) && !is_null($webfinger)) { $lrdd = self::hostMeta($host); } if (empty($webfinger) && empty($lrdd)) { - while (empty($lrdd) && empty($webfinger) && (sizeof($path_parts) > 1)) { - $host .= '/' . array_shift($path_parts); + while (empty($lrdd) && empty($webfinger) && (count($path_parts) > 1)) { + $host .= '/' . array_shift($path_parts); $baseurl = $parts['scheme'] . '://' . $host; if (!empty($nick)) { @@ -568,7 +590,7 @@ class Probe } $webfinger = self::getWebfinger($parts['scheme'] . '://' . $host . self::WEBFINGER, HttpClientAccept::JRD_JSON, $uri, $addr); - if (empty($webfinger)) { + if (empty($webfinger) && !is_null($webfinger)) { $lrdd = self::hostMeta($host); } } @@ -590,11 +612,12 @@ class Probe return []; } - if (empty($webfinger)) { + if (is_null($webfinger)) { $webfinger = self::getWebfinger('http://' . $host . self::WEBFINGER, HttpClientAccept::JRD_JSON, $uri, $addr); - if (self::$isTimeout) { + if (self::$isTimeout || is_null($webfinger)) { return []; } + $baseurl = 'http://' . $host; } else { $baseurl = 'https://' . $host; } @@ -605,11 +628,9 @@ class Probe return []; } $baseurl = self::$baseurl; - } else { - $baseurl = 'http://' . $host; } } else { - Logger::info('URI was not detectable', ['uri' => $uri]); + DI::logger()->info('URI was not detectable', ['uri' => $uri]); return []; } @@ -647,30 +668,32 @@ class Probe * * @return array webfinger results */ - private static function getWebfinger(string $template, string $type, string $uri, string $addr): array + private static function getWebfinger(string $template, string $type, string $uri, string $addr): ?array { if (Network::isUrlBlocked($template)) { - Logger::info('Domain is blocked', ['url' => $template]); - return []; + DI::logger()->info('Domain is blocked', ['url' => $template]); + return null; } + $detected = ''; + // First try the address because this is the primary purpose of webfinger - if (!empty($addr)) { - $detected = $addr; - $path = str_replace('{uri}', urlencode('acct:' . $addr), $template); + if ($addr !== '') { + $detected = $addr; + $path = str_replace('{uri}', urlencode('acct:' . $addr), $template); $webfinger = self::webfinger($path, $type); - if (self::$isTimeout) { - return []; + if (is_null($webfinger)) { + return null; } } // Then try the URI if (empty($webfinger) && $uri != $addr) { - $detected = $uri; - $path = str_replace('{uri}', urlencode($uri), $template); + $detected = $uri; + $path = str_replace('{uri}', urlencode($uri), $template); $webfinger = self::webfinger($path, $type); - if (self::$isTimeout) { - return []; + if (is_null($webfinger)) { + return null; } } @@ -710,8 +733,8 @@ class Probe $parts = parse_url($uri); if (empty($parts['scheme']) && empty($parts['host']) && (empty($parts['path']) || strpos($parts['path'], '@') === false)) { - Logger::info('URI was not detectable', ['uri' => $uri]); - return []; + DI::logger()->info('URI was not detectable, probe for AT Protocol now', ['uri' => $uri]); + return self::atProtocol($uri); } // If the URI starts with "mailto:" then jump directly to the mail detection @@ -724,7 +747,7 @@ class Probe return self::mail($uri, $uid); } - Logger::info('Probing start', ['uri' => $uri]); + DI::logger()->info('Probing start', ['uri' => $uri]); if (!empty($ap_profile['addr']) && ($ap_profile['addr'] != $uri)) { $data = self::getWebfingerArray($ap_profile['addr']); @@ -735,6 +758,10 @@ class Probe } if (empty($data)) { + $data = self::atProtocol($uri); + if (!empty($data)) { + return $data; + } if (!empty($parts['scheme'])) { return self::feed($uri); } elseif (!empty($uid)) { @@ -745,9 +772,9 @@ class Probe } $webfinger = $data['webfinger']; - $nick = $data['nick'] ?? ''; - $addr = $data['addr'] ?? ''; - $baseurl = $data['baseurl'] ?? ''; + $nick = $data['nick'] ?? ''; + $addr = $data['addr'] ?? ''; + $baseurl = $data['baseurl'] ?? ''; $result = []; @@ -761,14 +788,9 @@ class Probe } if ((!$result && ($network == '')) || ($network == Protocol::OSTATUS)) { $result = self::ostatus($webfinger); - } else { - $result['networks'][Protocol::OSTATUS] = self::ostatus($webfinger); } if (in_array($network, ['', Protocol::ZOT])) { - $result = self::zot($webfinger, $result, $baseurl); - } - if ((!$result && ($network == '')) || ($network == Protocol::PUMPIO)) { - $result = self::pumpio($webfinger, $addr, $baseurl); + $result = self::zot($webfinger, $result); } if (empty($result['network']) && empty($ap_profile['network']) || ($network == Protocol::FEED)) { $result = self::feed($uri); @@ -798,7 +820,7 @@ class Probe $result['url'] = $uri; } - Logger::info('Probing done', ['uri' => $uri, 'network' => $result['network']]); + DI::logger()->info('Probing done', ['uri' => $uri, 'network' => $result['network']]); return $result; } @@ -808,18 +830,27 @@ class Probe * * @param array $webfinger Webfinger data * @param array $data previously probed data - * @param string $baseUrl Base URL * * @return array Zot data * @throws HTTPException\InternalServerErrorException */ - private static function zot(array $webfinger, array $data, string $baseurl): array + private static function zot(array $webfinger, array $data): array { - if (!empty($webfinger['aliases']) && is_array($webfinger['aliases'])) { - foreach ($webfinger['aliases'] as $alias) { - if (substr($alias, 0, 5) == 'acct:') { - $data['addr'] = substr($alias, 5); - } + $zot_url = ''; + + foreach ($webfinger['links'] as $link) { + if (($link['rel'] == 'http://purl.org/zot/protocol/6.0') && !empty($link['href'])) { + $zot_url = $link['href']; + } + } + + if ($zot_url === '') { + return $data; + } + + foreach ($webfinger['aliases'] as $alias) { + if (substr($alias, 0, 5) == 'acct:') { + $data['addr'] = substr($alias, 5); } } @@ -827,30 +858,40 @@ class Probe $data['addr'] = substr($webfinger['subject'], 5); } - $zot_url = ''; + if (!empty($webfinger['properties'])) { + if (!empty($webfinger['properties']['http://webfinger.net/ns/name'])) { + $data['name'] = $webfinger['properties']['http://webfinger.net/ns/name']; + } + if (!empty($webfinger['properties']['http://xmlns.com/foaf/0.1/name'])) { + $data['name'] = $webfinger['properties']['http://xmlns.com/foaf/0.1/name']; + } + if (!empty($webfinger['properties']['https://w3id.org/security/v1#publicKeyPem'])) { + $data['pubkey'] = $webfinger['properties']['https://w3id.org/security/v1#publicKeyPem']; + } + + if (empty($data['network']) && !empty($webfinger['properties']['http://purl.org/zot/federation'])) { + $networks = explode(',', $webfinger['properties']['http://purl.org/zot/federation']); + if (in_array('zot6', $networks)) { + $data['network'] = Protocol::ZOT; + } + } + } + foreach ($webfinger['links'] as $link) { - if (($link['rel'] == 'http://purl.org/zot/protocol') && !empty($link['href'])) { - $zot_url = $link['href']; + if (($link['rel'] == ActivityNamespace::WEBFINGERAVATAR) && !empty($link['href'])) { + $data['photo'] = $link['href']; + } elseif (($link['rel'] == 'http://openid.net/specs/connect/1.0/issuer') && !empty($link['href'])) { + $data['baseurl'] = trim($link['href'], '/'); + } elseif (($link['rel'] == 'http://webfinger.net/rel/blog') && !empty($link['href'])) { + $data['url'] = $link['href']; } } - if (empty($zot_url) && !empty($data['addr']) && !empty($baseurl)) { - $condition = ['nurl' => Strings::normaliseLink($baseurl), 'platform' => ['hubzilla']]; - if (!DBA::exists('gserver', $condition)) { - return $data; - } - $zot_url = $baseurl . '/.well-known/zot-info?address=' . $data['addr']; - } - - if (empty($zot_url)) { - return $data; - } - $data = self::pollZot($zot_url, $data); if (!empty($data['url']) && !empty($webfinger['aliases']) && is_array($webfinger['aliases'])) { foreach ($webfinger['aliases'] as $alias) { - if (!strstr($alias, '@') && Strings::normaliseLink($alias) != Strings::normaliseLink($data['url'])) { + if (Network::isValidHttpUrl($alias) && !Strings::compareLink($alias, $data['url'])) { $data['alias'] = $alias; } } @@ -859,13 +900,18 @@ class Probe return $data; } - public static function pollZot(string $url, array $data): array + private static function pollZot(string $url, array $data): array { - $curlResult = DI::httpClient()->get($url, HttpClientAccept::JSON); + try { + $curlResult = DI::httpClient()->get($url, 'application/x-zot+json', [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return $data; + } if ($curlResult->isTimeout()) { return $data; } - $content = $curlResult->getBody(); + $content = $curlResult->getBodyString(); if (!$content) { return $data; } @@ -876,43 +922,50 @@ class Probe } if (empty($data['network'])) { - if (!empty($json['protocols']) && in_array('zot', $json['protocols'])) { - $data['network'] = Protocol::ZOT; - } elseif (!isset($json['protocols'])) { + if (!empty($json['protocols']) && in_array('zot6', $json['protocols'])) { $data['network'] = Protocol::ZOT; } } - if (!empty($json['guid']) && empty($data['guid'])) { - $data['guid'] = $json['guid']; - } - if (!empty($json['key']) && empty($data['pubkey'])) { - $data['pubkey'] = $json['key']; + if (!empty($json['public_key'])) { + $data['pubkey'] = $json['public_key']; } if (!empty($json['name'])) { $data['name'] = $json['name']; } - if (!empty($json['photo'])) { - $data['photo'] = $json['photo']; - if (!empty($json['photo_updated'])) { - $data['photo'] .= '?rev=' . urlencode($json['photo_updated']); + if (!empty($json['username'])) { + $data['nick'] = $json['username']; + } + if (!empty($json['photo']) && !empty($json['photo']['url'])) { + $data['photo'] = $json['photo']['url']; + } + if (!empty($json['locations'])) { + foreach ($json['locations'] as $location) { + if ($location['deleted'] || (parse_url($url, PHP_URL_HOST) != $location['host'])) { + continue; + } + if (!empty($location['address'])) { + $data['addr'] = $location['address']; + } + if (!empty($location['id_url'])) { + $data['url'] = $location['id_url']; + } + if (!empty($location['callback'])) { + $data['confirm'] = $location['callback']; + } } } - if (!empty($json['address'])) { - $data['addr'] = $json['address']; - } - if (!empty($json['url'])) { - $data['url'] = $json['url']; - } - if (!empty($json['connections_url'])) { - $data['poco'] = $json['connections_url']; + if (!empty($json['primary_location']) && !empty($json['primary_location']['connections_url'])) { + $data['poco'] = $json['primary_location']['connections_url']; } if (isset($json['searchable'])) { $data['hide'] = !$json['searchable']; } if (!empty($json['public_forum'])) { - $data['community'] = $json['public_forum']; - $data['account-type'] = User::PAGE_FLAGS_COMMUNITY; + $data['community'] = $json['public_forum']; + $data['account-type'] = User::ACCOUNT_TYPE_COMMUNITY; + } elseif (($json['channel_type'] ?? '') == 'normal') { + $data['account-type'] = User::ACCOUNT_TYPE_PERSON; } if (!empty($json['profile'])) { @@ -954,29 +1007,29 @@ class Probe * @return array webfinger data * @throws HTTPException\InternalServerErrorException */ - public static function webfinger(string $url, string $type): array + private static function webfinger(string $url, string $type): ?array { try { $curlResult = DI::httpClient()->get( $url, $type, - [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout', 20)] + [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout', 20), HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO] ); } catch (\Throwable $e) { - Logger::notice($e->getMessage(), ['url' => $url, 'type' => $type, 'class' => get_class($e)]); - return []; + DI::logger()->notice($e->getMessage(), ['url' => $url, 'type' => $type, 'class' => get_class($e)]); + return null; } if ($curlResult->isTimeout()) { self::$isTimeout = true; - return []; + return null; } - $data = $curlResult->getBody(); + $data = $curlResult->getBodyString(); $webfinger = json_decode($data, true); if (!empty($webfinger)) { if (!isset($webfinger['links'])) { - Logger::info('No json webfinger links', ['url' => $url]); + DI::logger()->info('No json webfinger links', ['url' => $url]); return []; } return $webfinger; @@ -985,13 +1038,13 @@ class Probe // If it is not JSON, maybe it is XML $xrd = XML::parseString($data, true); if (!is_object($xrd)) { - Logger::info('No webfinger data retrievable', ['url' => $url]); + DI::logger()->info('No webfinger data retrievable', ['url' => $url]); return []; } $xrd_arr = XML::elementToArray($xrd); if (!isset($xrd_arr['xrd']['link'])) { - Logger::info('No XML webfinger links', ['url' => $url]); + DI::logger()->info('No XML webfinger links', ['url' => $url]); return []; } @@ -1035,20 +1088,25 @@ class Probe */ private static function pollNoscrape(string $noscrape_url, array $data): array { - $curlResult = DI::httpClient()->get($noscrape_url, HttpClientAccept::JSON); + try { + $curlResult = DI::httpClient()->get($noscrape_url, HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return $data; + } if ($curlResult->isTimeout()) { self::$isTimeout = true; return $data; } - $content = $curlResult->getBody(); + $content = $curlResult->getBodyString(); if (!$content) { - Logger::info('Empty body', ['url' => $noscrape_url]); + DI::logger()->info('Empty body', ['url' => $noscrape_url]); return $data; } $json = json_decode($content, true); if (!is_array($json)) { - Logger::info('No json data', ['url' => $noscrape_url]); + DI::logger()->info('No json data', ['url' => $noscrape_url]); return $data; } @@ -1104,14 +1162,6 @@ class Probe $data['photo'] = $json['photo']; } - if (!empty($json['dfrn-request'])) { - $data['request'] = $json['dfrn-request']; - } - - if (!empty($json['dfrn-confirm'])) { - $data['confirm'] = $json['dfrn-confirm']; - } - if (!empty($json['dfrn-notify'])) { $data['notify'] = $json['dfrn-notify']; } @@ -1129,86 +1179,6 @@ class Probe return $data; } - /** - * Check for valid DFRN data - * - * @param array $data DFRN data - * - * @return int Number of errors - */ - public static function validDfrn(array $data): int - { - $errors = 0; - if (!isset($data['key'])) { - $errors ++; - } - if (!isset($data['dfrn-request'])) { - $errors ++; - } - if (!isset($data['dfrn-confirm'])) { - $errors ++; - } - if (!isset($data['dfrn-notify'])) { - $errors ++; - } - if (!isset($data['dfrn-poll'])) { - $errors ++; - } - return $errors; - } - - /** - * Fetch data from a DFRN profile page and via "noscrape" - * - * @param string $profile_link Link to the profile page - * @return array profile data - * @throws HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function profile(string $profile_link): array - { - $data = []; - - Logger::info('Check profile', ['link' => $profile_link]); - - // Fetch data via noscrape - this is faster - $noscrape_url = str_replace(['/hcard/', '/profile/'], '/noscrape/', $profile_link); - $data = self::pollNoscrape($noscrape_url, $data); - - if (!isset($data['notify']) - || !isset($data['confirm']) - || !isset($data['request']) - || !isset($data['poll']) - || !isset($data['name']) - || !isset($data['photo']) - ) { - $data = self::pollHcard($profile_link, $data, true); - } - - - if (empty($data['addr']) || empty($data['nick'])) { - $probe_data = self::uri($profile_link); - $data['addr'] = ($data['addr'] ?? '') ?: $probe_data['addr']; - $data['nick'] = ($data['nick'] ?? '') ?: $probe_data['nick']; - } - - $prof_data = [ - 'addr' => $data['addr'], - 'nick' => $data['nick'], - 'dfrn-request' => $data['request'] ?? null, - 'dfrn-confirm' => $data['confirm'] ?? null, - 'dfrn-notify' => $data['notify'] ?? null, - 'dfrn-poll' => $data['poll'] ?? null, - 'photo' => $data['photo'] ?? null, - 'fn' => $data['name'] ?? null, - 'key' => $data['pubkey'] ?? null, - ]; - - Logger::debug('Result', ['link' => $profile_link, 'data' => $prof_data]); - - return $prof_data; - } - /** * Check for DFRN contact * @@ -1219,7 +1189,7 @@ class Probe private static function dfrn(array $webfinger): array { $hcard_url = ''; - $data = []; + $data = []; // The array is reversed to take into account the order of preference for same-rel links // See: https://tools.ietf.org/html/rfc7033#section-4.4.4 foreach (array_reverse($webfinger['links']) as $link) { @@ -1227,17 +1197,17 @@ class Probe $data['network'] = Protocol::DFRN; } elseif (($link['rel'] == ActivityNamespace::FEED) && !empty($link['href'])) { $data['poll'] = $link['href']; - } elseif (($link['rel'] == 'http://webfinger.net/rel/profile-page') && (($link['type'] ?? '') == 'text/html') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && (($link['type'] ?? '') == 'text/html') && !empty($link['href'])) { $data['url'] = $link['href']; - } elseif (($link['rel'] == 'http://microformats.org/profile/hcard') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::HCARD) && !empty($link['href'])) { $hcard_url = $link['href']; } elseif (($link['rel'] == ActivityNamespace::POCO) && !empty($link['href'])) { $data['poco'] = $link['href']; - } elseif (($link['rel'] == 'http://webfinger.net/rel/avatar') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::WEBFINGERAVATAR) && !empty($link['href'])) { $data['photo'] = $link['href']; - } elseif (($link['rel'] == 'http://joindiaspora.com/seed_location') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::DIASPORA_SEED) && !empty($link['href'])) { $data['baseurl'] = trim($link['href'], '/'); - } elseif (($link['rel'] == 'http://joindiaspora.com/guid') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::DIASPORA_GUID) && !empty($link['href'])) { $data['guid'] = $link['href']; } elseif (($link['rel'] == 'diaspora-public-key') && !empty($link['href'])) { $data['pubkey'] = base64_decode($link['href']); @@ -1250,9 +1220,9 @@ class Probe if (!empty($webfinger['aliases']) && is_array($webfinger['aliases'])) { foreach ($webfinger['aliases'] as $alias) { - if (empty($data['url']) && !strstr($alias, '@')) { + if (empty($data['url']) && Network::isValidHttpUrl($alias)) { $data['url'] = $alias; - } elseif (!strstr($alias, '@') && Strings::normaliseLink($alias) != Strings::normaliseLink($data['url'])) { + } elseif (Network::isValidHttpUrl($alias) && !Strings::compareLink($alias, $data['url'])) { $data['alias'] = $alias; } elseif (substr($alias, 0, 5) == 'acct:') { $data['addr'] = substr($alias, 5); @@ -1270,21 +1240,7 @@ class Probe // Fetch data via noscrape - this is faster $noscrape_url = str_replace('/hcard/', '/noscrape/', $hcard_url); - $data = self::pollNoscrape($noscrape_url, $data); - - if (isset($data['notify']) - && isset($data['confirm']) - && isset($data['request']) - && isset($data['poll']) - && isset($data['name']) - && isset($data['photo']) - ) { - return $data; - } - - $data = self::pollHcard($hcard_url, $data, true); - - return $data; + return self::pollNoscrape($noscrape_url, $data); } /** @@ -1292,18 +1248,22 @@ class Probe * * @param string $hcard_url Link to the hcard page * @param array $data The already fetched data - * @param boolean $dfrn Poll DFRN specific data * @return array hcard data * @throws HTTPException\InternalServerErrorException */ - private static function pollHcard(string $hcard_url, array $data, bool $dfrn = false): array + private static function pollHcard(string $hcard_url, array $data): array { - $curlResult = DI::httpClient()->get($hcard_url, HttpClientAccept::HTML); + try { + $curlResult = DI::httpClient()->get($hcard_url, HttpClientAccept::HTML, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return []; + } if ($curlResult->isTimeout()) { self::$isTimeout = true; return []; } - $content = $curlResult->getBody(); + $content = $curlResult->getBodyString(); if (empty($content)) { return []; } @@ -1406,27 +1366,6 @@ class Probe } } - if ($dfrn) { - // Poll DFRN specific data - $search = $xpath->query("//link[contains(concat(' ', @rel), ' dfrn-')]"); - if ($search->length > 0) { - foreach ($search as $link) { - //$data['request'] = $search->item(0)->nodeValue; - $attr = []; - foreach ($link->attributes as $attribute) { - $attr[$attribute->name] = trim($attribute->value); - } - - $data[substr($attr['rel'], 5)] = $attr['href']; - } - } - - // Older Friendica versions had used the "uid" field differently than newer versions - if (!empty($data['nick']) && !empty($data['guid']) && ($data['nick'] == $data['guid'])) { - unset($data['guid']); - } - } - return $data; } @@ -1441,20 +1380,20 @@ class Probe private static function diaspora(array $webfinger): array { $hcard_url = ''; - $data = []; + $data = []; // The array is reversed to take into account the order of preference for same-rel links // See: https://tools.ietf.org/html/rfc7033#section-4.4.4 foreach (array_reverse($webfinger['links']) as $link) { - if (($link['rel'] == 'http://microformats.org/profile/hcard') && !empty($link['href'])) { + if (($link['rel'] == ActivityNamespace::HCARD) && !empty($link['href'])) { $hcard_url = $link['href']; - } elseif (($link['rel'] == 'http://joindiaspora.com/seed_location') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::DIASPORA_SEED) && !empty($link['href'])) { $data['baseurl'] = trim($link['href'], '/'); - } elseif (($link['rel'] == 'http://joindiaspora.com/guid') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::DIASPORA_GUID) && !empty($link['href'])) { $data['guid'] = $link['href']; - } elseif (($link['rel'] == 'http://webfinger.net/rel/profile-page') && (($link['type'] ?? '') == 'text/html') && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && (($link['type'] ?? '') == 'text/html') && !empty($link['href'])) { $data['url'] = $link['href']; - } elseif (($link['rel'] == 'http://webfinger.net/rel/profile-page') && empty($link['type']) && !empty($link['href'])) { + } elseif (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && empty($link['type']) && !empty($link['href'])) { $profile_url = $link['href']; } elseif (($link['rel'] == ActivityNamespace::FEED) && !empty($link['href'])) { $data['poll'] = $link['href']; @@ -1481,7 +1420,7 @@ class Probe if (!empty($webfinger['aliases']) && is_array($webfinger['aliases'])) { foreach ($webfinger['aliases'] as $alias) { - if (Strings::normaliseLink($alias) != Strings::normaliseLink($data['url']) && ! strstr($alias, '@')) { + if (Network::isValidHttpUrl($alias) && !Strings::compareLink($alias, $data['url'])) { $data['alias'] = $alias; } elseif (substr($alias, 0, 5) == 'acct:') { $data['addr'] = substr($alias, 5); @@ -1500,13 +1439,14 @@ class Probe return []; } - if (!empty($data['url']) + if ( + !empty($data['url']) && !empty($data['guid']) && !empty($data['baseurl']) && !empty($data['pubkey']) - && !empty($hcard_url) + && $hcard_url !== '' ) { - $data['network'] = Protocol::DIASPORA; + $data['network'] = Protocol::DIASPORA; $data['manually-approve'] = false; // The Diaspora handle must always be lowercase @@ -1539,14 +1479,15 @@ class Probe if (!empty($webfinger['aliases']) && is_array($webfinger['aliases'])) { foreach ($webfinger['aliases'] as $alias) { - if (strstr($alias, '@') && !strstr(Strings::normaliseLink($alias), 'http://')) { + if (strstr($alias, '@') && !Network::isValidHttpUrl($alias)) { $data['addr'] = str_replace('acct:', '', $alias); } } } - if (!empty($webfinger['subject']) && strstr($webfinger['subject'], '@') - && !strstr(Strings::normaliseLink($webfinger['subject']), 'http://') + if ( + !empty($webfinger['subject']) && strstr($webfinger['subject'], '@') + && !Network::isValidHttpUrl($webfinger['subject']) ) { $data['addr'] = str_replace('acct:', '', $webfinger['subject']); } @@ -1555,7 +1496,7 @@ class Probe // The array is reversed to take into account the order of preference for same-rel links // See: https://tools.ietf.org/html/rfc7033#section-4.4.4 foreach (array_reverse($webfinger['links']) as $link) { - if (($link['rel'] == 'http://webfinger.net/rel/profile-page') + if (($link['rel'] == ActivityNamespace::WEBFINGERPROFILE) && (($link['type'] ?? '') == 'text/html') && ($link['href'] != '') ) { @@ -1574,29 +1515,29 @@ class Probe $pubkey = substr($pubkey, 5); } } elseif (Strings::normaliseLink($pubkey) == 'http://') { - $curlResult = DI::httpClient()->get($pubkey, HttpClientAccept::MAGIC_KEY); + $curlResult = DI::httpClient()->get($pubkey, HttpClientAccept::MAGIC_KEY, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); if ($curlResult->isTimeout()) { self::$isTimeout = true; return $short ? false : []; } - Logger::debug('Fetched public key', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $pubkey]); - $pubkey = $curlResult->getBody(); + DI::logger()->debug('Fetched public key', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $pubkey]); + $pubkey = $curlResult->getBodyString(); } try { $data['pubkey'] = Salmon::magicKeyToPem($pubkey); } catch (\Throwable $e) { - } } } } - if (isset($data['notify']) && isset($data['pubkey']) + if ( + isset($data['notify']) && isset($data['pubkey']) && isset($data['poll']) && isset($data['url']) ) { - $data['network'] = Protocol::OSTATUS; + $data['network'] = Protocol::OSTATUS; $data['manually-approve'] = false; } else { return $short ? false : []; @@ -1607,12 +1548,17 @@ class Probe } // Fetch all additional data from the feed - $curlResult = DI::httpClient()->get($data['poll'], HttpClientAccept::FEED_XML); + try { + $curlResult = DI::httpClient()->get($data['poll'], HttpClientAccept::FEED_XML, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return []; + } if ($curlResult->isTimeout()) { self::$isTimeout = true; return []; } - $feed = $curlResult->getBody(); + $feed = $curlResult->getBodyString(); $feed_data = Feed::import($feed); if (!$feed_data) { return []; @@ -1650,124 +1596,6 @@ class Probe return $data; } - /** - * Fetch data from a pump.io profile page - * - * @param string $profile_link Link to the profile page - * - * @return array Profile data - */ - private static function pumpioProfileData(string $profile_link, string $baseurl): array - { - $curlResult = DI::httpClient()->get($profile_link, HttpClientAccept::HTML); - if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { - return []; - } - - $doc = new DOMDocument(); - if (!@$doc->loadHTML($curlResult->getBody())) { - return []; - } - - $xpath = new DomXPath($doc); - - $data = []; - $data['name'] = $xpath->query("//span[contains(@class, 'p-name')]")->item(0)->nodeValue; - - if ($data['name'] == '') { - // This is ugly - but pump.io doesn't seem to know a better way for it - $data['name'] = trim($xpath->query("//h1[@class='media-header']")->item(0)->nodeValue); - $pos = strpos($data['name'], chr(10)); - if ($pos) { - $data['name'] = trim(substr($data['name'], 0, $pos)); - } - } - - $data['location'] = XML::getFirstNodeValue($xpath, "//p[contains(@class, 'p-locality')]"); - - if ($data['location'] == '') { - $data['location'] = XML::getFirstNodeValue($xpath, "//p[contains(@class, 'location')]"); - } - - $data['about'] = XML::getFirstNodeValue($xpath, "//p[contains(@class, 'p-note')]"); - - if ($data['about'] == '') { - $data['about'] = XML::getFirstNodeValue($xpath, "//p[contains(@class, 'summary')]"); - } - - $avatar = $xpath->query("//img[contains(@class, 'u-photo')]")->item(0); - if (!$avatar) { - $avatar = $xpath->query("//img[@class='img-rounded media-object']")->item(0); - } - if ($avatar) { - foreach ($avatar->attributes as $attribute) { - if (($attribute->name == 'src') && !empty($attribute->value)) { - $data['photo'] = Network::addBasePath($attribute->value, $baseurl); - } - } - } - - return $data; - } - - /** - * Check for pump.io contact - * - * @param array $webfinger Webfinger data - * @param string $addr - * - * @return array pump.io data - */ - private static function pumpio(array $webfinger, string $addr, string $baseurl): array - { - $data = []; - // The array is reversed to take into account the order of preference for same-rel links - // See: https://tools.ietf.org/html/rfc7033#section-4.4.4 - foreach (array_reverse($webfinger['links']) as $link) { - if (($link['rel'] == 'http://webfinger.net/rel/profile-page') - && (($link['type'] ?? '') == 'text/html') - && ($link['href'] != '') - ) { - $data['url'] = $link['href']; - } elseif (($link['rel'] == 'activity-inbox') && ($link['href'] != '')) { - $data['notify'] = $link['href']; - } elseif (($link['rel'] == 'activity-outbox') && ($link['href'] != '')) { - $data['poll'] = $link['href']; - } elseif (($link['rel'] == 'dialback') && ($link['href'] != '')) { - $data['dialback'] = $link['href']; - } - } - if (isset($data['poll']) && isset($data['notify']) - && isset($data['dialback']) - && isset($data['url']) - ) { - // by now we use these fields only for the network type detection - // So we unset all data that isn't used at the moment - unset($data['dialback']); - - $data['network'] = Protocol::PUMPIO; - } else { - return []; - } - - $profile_data = self::pumpioProfileData($data['url'], $baseurl); - - if (!$profile_data) { - return []; - } - - $data = array_merge($data, $profile_data); - - if (($addr != '') && ($data['name'] != '')) { - $name = trim(str_replace($addr, '', $data['name'])); - if ($name != '') { - $data['name'] = $name; - } - } - - return $data; - } - /** * Checks HTML page for RSS feed link * @@ -1840,10 +1668,12 @@ class Probe // Resolve arbitrary relative path // Lifted from https://www.php.net/manual/en/function.realpath.php#84012 - $parts = array_filter(explode('/', $path), 'strlen'); + $parts = array_filter(explode('/', $path), 'strlen'); $absolutes = []; foreach ($parts as $part) { - if ('.' == $part) continue; + if ('.' == $part) { + continue; + } if ('..' == $part) { array_pop($absolutes); } else { @@ -1861,7 +1691,76 @@ class Probe unset($baseParts['query']); unset($baseParts['fragment']); - return Network::unparseURL((array)$baseParts); + return (string)Uri::fromParts((array)(array)$baseParts); + } + + /** + * Check for AT Protocol (Bluesky) + * + * @param string $uri Profile link + * @return array Profile data or empty array + */ + private static function atProtocol(string $uri): array + { + if (parse_url($uri, PHP_URL_SCHEME) == 'did') { + $did = $uri; + } elseif (parse_url($uri, PHP_URL_PATH) == $uri && strpos($uri, '@') === false) { + $did = DI::atProtocol()->getDid($uri); + if (empty($did)) { + return []; + } + } elseif (Network::isValidHttpUrl($uri)) { + $did = DI::atProtocol()->getDidByProfile($uri); + if (empty($did)) { + return []; + } + } else { + return []; + } + + $profile = DI::atProtocol()->XRPCGet('app.bsky.actor.getProfile', ['actor' => $did]); + if (empty($profile) || empty($profile->did)) { + return []; + } + + $nick = $profile->handle ?? $profile->did; + $name = $profile->displayName ?? $nick; + + $data = [ + 'network' => Protocol::BLUESKY, + 'url' => $profile->did, + 'alias' => ATProtocol::WEB . '/profile/' . $profile->did, + 'name' => $name ?: $nick, + 'nick' => $nick, + 'addr' => $nick, + 'poll' => ATProtocol::WEB . '/profile/' . $profile->did . '/rss', + 'photo' => $profile->avatar ?? '', + ]; + + if (!empty($profile->description)) { + $data['about'] = HTML::toBBCode($profile->description); + } + + if (!empty($profile->banner)) { + $data['header'] = $profile->banner; + } + + $directory = DI::atProtocol()->get(ATProtocol::DIRECTORY . '/' . $profile->did); + if (!empty($directory)) { + foreach ($directory->service as $service) { + if (($service->id == '#atproto_pds') && ($service->type == 'AtprotoPersonalDataServer') && !empty($service->serviceEndpoint)) { + $data['baseurl'] = $service->serviceEndpoint; + } + } + + foreach ($directory->verificationMethod as $method) { + if (!empty($method->publicKeyMultibase)) { + $data['pubkey'] = $method->publicKeyMultibase; + } + } + } + + return $data; } /** @@ -1876,8 +1775,8 @@ class Probe private static function feed(string $url, bool $probe = true): array { try { - $curlResult = DI::httpClient()->get($url, HttpClientAccept::FEED_XML); - } catch(\Throwable $e) { + $curlResult = DI::httpClient()->get($url, HttpClientAccept::FEED_XML, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $e) { DI::logger()->info('Error requesting feed URL', ['url' => $url, 'exception' => $e]); return []; } @@ -1887,10 +1786,12 @@ class Probe return []; } - $feed = $curlResult->getBody(); - $feed_data = Feed::import($feed); + $feed = $curlResult->getBodyString(); + if (strpos($curlResult->getContentType(), 'xml') !== false) { + $feed_data = Feed::import($feed); + } - if (!$feed_data) { + if (empty($feed_data)) { if (!$probe) { return []; } @@ -1920,7 +1821,7 @@ class Probe $data['alias'] = $feed_data['header']['author-id']; } - $data['url'] = $url; + $data['url'] = $url; $data['poll'] = $url; $data['network'] = Protocol::FEED; @@ -1950,23 +1851,23 @@ class Probe $user = DBA::selectFirst('user', ['prvkey'], ['uid' => $uid]); $condition = ["`uid` = ? AND `server` != ''", $uid]; - $fields = ['pass', 'user', 'server', 'port', 'ssltype', 'mailbox']; - $mailacct = DBA::selectFirst('mailacct', $fields, $condition); + $fields = ['pass', 'user', 'server', 'port', 'ssltype', 'mailbox']; + $mailacct = DBA::selectFirst('mailacct', $fields, $condition); if (!DBA::isResult($user) || !DBA::isResult($mailacct)) { return []; } - $mailbox = Email::constructMailboxName($mailacct); + $mailbox = Email::constructMailboxName($mailacct); $password = ''; openssl_private_decrypt(hex2bin($mailacct['pass']), $password, $user['prvkey']); $mbox = Email::connect($mailbox, $mailacct['user'], $password); - if (!$mbox) { + if ($mbox === false) { return []; } $msgs = Email::poll($mbox, $uri); - Logger::info('Messages found', ['uri' => $uri, 'count' => count($msgs)]); + DI::logger()->info('Messages found', ['uri' => $uri, 'count' => count($msgs)]); if (!count($msgs)) { return []; @@ -1984,7 +1885,7 @@ class Probe 'poll' => 'email ' . Strings::getRandomHex(), ]; - $data['nick'] = $data['name']; + $data['nick'] = $data['name']; $x = Email::messageMeta($mbox, $msgs[0]); @@ -1997,10 +1898,10 @@ class Probe if (isset($adr)) { foreach ($adr as $feadr) { if ((strcasecmp($feadr->mailbox, $data['name']) == 0) - &&(strcasecmp($feadr->host, $phost) == 0) - && (strlen($feadr->personal)) + && (strcasecmp($feadr->host, $phost) == 0) + && !empty($feadr->personal) ) { - $personal = imap_mime_header_decode($feadr->personal); + $personal = imap_mime_header_decode($feadr->personal); $data['name'] = ''; foreach ($personal as $perspart) { if ($perspart->charset != 'default') { @@ -2013,7 +1914,7 @@ class Probe } } - if (!empty($mbox)) { + if ($mbox !== false) { imap_close($mbox); } @@ -2051,9 +1952,9 @@ class Probe $query = isset($parts['query']) ? '?' . $parts['query'] : ''; $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : ''; - $fixed = $scheme.$host.$port.$path.$query.$fragment; + $fixed = $scheme . $host . $port . $path . $query . $fragment; - Logger::debug('Avatar fixed', ['base' => $base, 'avatar' => $avatar, 'fixed' => $fixed]); + DI::logger()->debug('Avatar fixed', ['base' => $base, 'avatar' => $avatar, 'fixed' => $fixed]); return $fixed; } @@ -2104,16 +2005,23 @@ class Probe } // Check the 'noscrape' endpoint when it is a Friendica server - $gserver = DBA::selectFirst('gserver', ['noscrape'], ["`nurl` = ? AND `noscrape` != ''", - Strings::normaliseLink($data['baseurl'])]); + $gserver = DBA::selectFirst('gserver', ['noscrape'], [ + "`nurl` = ? AND `noscrape` != ''", + Strings::normaliseLink($data['baseurl']) + ]); if (!DBA::isResult($gserver)) { return ''; } - $curlResult = DI::httpClient()->get($gserver['noscrape'] . '/' . $data['nick'], HttpClientAccept::JSON); + try { + $curlResult = DI::httpClient()->get($gserver['noscrape'] . '/' . $data['nick'], HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return ''; + } - if ($curlResult->isSuccess() && !empty($curlResult->getBody())) { - $noscrape = json_decode($curlResult->getBody(), true); + if ($curlResult->isSuccess() && !empty($curlResult->getBodyString())) { + $noscrape = json_decode($curlResult->getBodyString(), true); if (!empty($noscrape) && !empty($noscrape['updated'])) { return DateTimeFormat::utc($noscrape['updated'], DateTimeFormat::MYSQL); } @@ -2148,7 +2056,7 @@ class Probe if (is_string($outbox['first']) && ($outbox['first'] != $feed)) { return self::updateFromOutbox($outbox['first'], $data); } else { - Logger::warning('Unexpected data', ['outbox' => $outbox]); + DI::logger()->warning('Unexpected data', ['outbox' => $outbox]); } return ''; } else { @@ -2158,9 +2066,9 @@ class Probe $last_updated = ''; foreach ($items as $activity) { if (!empty($activity['published'])) { - $published = DateTimeFormat::utc($activity['published']); + $published = DateTimeFormat::utc($activity['published']); } elseif (!empty($activity['object']['published'])) { - $published = DateTimeFormat::utc($activity['object']['published']); + $published = DateTimeFormat::utc($activity['object']['published']); } else { continue; } @@ -2186,13 +2094,18 @@ class Probe private static function updateFromFeed(array $data): string { // Search for the newest entry in the feed - $curlResult = DI::httpClient()->get($data['poll'], HttpClientAccept::ATOM_XML); - if (!$curlResult->isSuccess() || !$curlResult->getBody()) { + try { + $curlResult = DI::httpClient()->get($data['poll'], HttpClientAccept::ATOM_XML, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return ''; + } + if (!$curlResult->isSuccess() || !$curlResult->getBodyString()) { return ''; } $doc = new DOMDocument(); - @$doc->loadXML($curlResult->getBody()); + @$doc->loadXML($curlResult->getBodyString()); $xpath = new DOMXPath($doc); $xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom'); @@ -2203,12 +2116,12 @@ class Probe foreach ($entries as $entry) { $published_item = $xpath->query('atom:published/text()', $entry)->item(0); - $updated_item = $xpath->query('atom:updated/text()' , $entry)->item(0); + $updated_item = $xpath->query('atom:updated/text()', $entry)->item(0); $published = !empty($published_item->nodeValue) ? DateTimeFormat::utc($published_item->nodeValue) : null; $updated = !empty($updated_item->nodeValue) ? DateTimeFormat::utc($updated_item->nodeValue) : null; if (empty($published) || empty($updated)) { - Logger::notice('Invalid entry for XPath.', ['entry' => $entry, 'url' => $data['url']]); + DI::logger()->notice('Invalid entry for XPath.', ['entry' => $entry, 'url' => $data['url']]); continue; } @@ -2251,7 +2164,7 @@ class Probe $split_name = Diaspora::splitName($owner['name']); if (empty($owner['gsid'])) { - $owner['gsid'] = GServer::getID($approfile['generator']['url']); + $owner['gsid'] = GServer::getRealID($approfile['generator']['url']); } $data = [ @@ -2263,14 +2176,15 @@ class Probe 'keywords' => $owner['keywords'], 'location' => $owner['location'], 'about' => $owner['about'], 'xmpp' => $owner['xmpp'], 'matrix' => $owner['matrix'], 'hide' => !$owner['net-publish'], 'batch' => '', 'notify' => $owner['notify'], - 'poll' => $owner['poll'], 'request' => $owner['request'], 'confirm' => $owner['confirm'], + 'poll' => $owner['poll'], 'subscribe' => $approfile['generator']['url'] . '/contact/follow?url={uri}', 'poco' => $owner['poco'], + 'openwebauth' => $approfile['generator']['url'] . '/owa', 'following' => $approfile['following'], 'followers' => $approfile['followers'], 'inbox' => $approfile['inbox'], 'outbox' => $approfile['outbox'], 'sharedinbox' => $approfile['endpoints']['sharedInbox'], 'network' => Protocol::DFRN, 'pubkey' => $owner['upubkey'], 'baseurl' => $approfile['generator']['url'], 'gsid' => $owner['gsid'], - 'manually-approve' => in_array($owner['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]), - 'networks' => [ + 'manually-approve' => in_array($owner['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP, User::PAGE_FLAGS_COMM_MAN]), + 'networks' => [ Protocol::DIASPORA => [ 'name' => $owner['name'], 'given_name' => $split_name['first'], @@ -2295,7 +2209,7 @@ class Probe } catch (Exception $e) { // Default values for nonexistent targets $data = [ - 'name' => $url, 'nick' => $url, 'url' => $url, 'network' => Protocol::PHANTOM, + 'name' => $url, 'nick' => $url, 'url' => $url, 'network' => Protocol::PHANTOM, 'photo' => DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO ]; } diff --git a/src/Object/Api/Friendica/Circle.php b/src/Object/Api/Friendica/Circle.php index 5ff1a32cce..5be86f656b 100644 --- a/src/Object/Api/Friendica/Circle.php +++ b/src/Object/Api/Friendica/Circle.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Friendica; diff --git a/src/Object/Api/Friendica/Notification.php b/src/Object/Api/Friendica/Notification.php index df59bb6b8c..2a49ea770c 100644 --- a/src/Object/Api/Friendica/Notification.php +++ b/src/Object/Api/Friendica/Notification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Friendica; @@ -76,37 +62,37 @@ class Notification extends BaseDataTransferObject /** @var string Message (Plaintext) */ protected $msg_plain; - public function __construct(Notify $Notify) + public function __construct(Notify $notify) { - $this->id = $Notify->id; - $this->type = $Notify->type; - $this->name = $Notify->name; - $this->url = $Notify->url->__toString(); - $this->photo = $Notify->photo->__toString(); - $this->date = DateTimeFormat::local($Notify->date->format(DateTimeFormat::MYSQL)); - $this->msg = $Notify->msg; - $this->uid = $Notify->uid; - $this->link = $Notify->link->__toString(); - $this->iid = $Notify->itemId; - $this->parent = $Notify->parent; - $this->seen = $Notify->seen; - $this->verb = $Notify->verb; - $this->otype = $Notify->otype; - $this->name_cache = $Notify->name_cache; - $this->msg_cache = $Notify->msg_cache; - $this->timestamp = $Notify->date->format('U'); + $this->id = $notify->id; + $this->type = $notify->type; + $this->name = $notify->name; + $this->url = $notify->url->__toString(); + $this->photo = $notify->photo->__toString(); + $this->date = DateTimeFormat::local($notify->date->format(DateTimeFormat::MYSQL)); + $this->msg = $notify->msg; + $this->uid = $notify->uid; + $this->link = $notify->link->__toString(); + $this->iid = $notify->itemId; + $this->parent = $notify->parent; + $this->seen = $notify->seen; + $this->verb = $notify->verb; + $this->otype = $notify->otype; + $this->name_cache = $notify->name_cache; + $this->msg_cache = $notify->msg_cache; + $this->timestamp = (int) $notify->date->format('U'); $this->date_rel = Temporal::getRelativeDate($this->date); try { - $this->msg_html = BBCode::convertForUriId($Notify->uriId, $this->msg, BBCode::EXTERNAL); + $this->msg_html = BBCode::convertForUriId($notify->uriId, $this->msg, BBCode::EXTERNAL); } catch (\Exception $e) { - $this->msg_html = ''; + $this->msg_html = ''; } try { $this->msg_plain = explode("\n", trim(HTML::toPlaintext($this->msg_html, 0)))[0]; } catch (\Exception $e) { - $this->msg_plain = ''; + $this->msg_plain = ''; } } } diff --git a/src/Object/Api/Mastodon/Account.php b/src/Object/Api/Mastodon/Account.php index d8f854d5b7..b4009b6e69 100644 --- a/src/Object/Api/Mastodon/Account.php +++ b/src/Object/Api/Mastodon/Account.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -52,6 +38,8 @@ class Account extends BaseDataTransferObject /** @var bool */ protected $discoverable; /** @var bool */ + protected $indexable; + /** @var bool */ protected $group; /** @var string|null (Datetime) */ protected $created_at; @@ -59,6 +47,8 @@ class Account extends BaseDataTransferObject protected $note; /** @var string (URL)*/ protected $url; + /** @var string (URL)*/ + protected $uri; /** @var string (URL) */ protected $avatar; /** @var string (URL) */ @@ -75,6 +65,8 @@ class Account extends BaseDataTransferObject protected $statuses_count; /** @var string|null (Datetime) */ protected $last_status_at = null; + /** @var bool */ + protected $hide_collections = false; /** @var Emoji[] */ protected $emojis; /** @var Account|null */ @@ -92,36 +84,37 @@ class Account extends BaseDataTransferObject */ public function __construct(BaseURL $baseUrl, array $account, Fields $fields) { - $this->id = (string)$account['pid']; - $this->username = $account['nick']; - $this->acct = - strpos($account['url'], $baseUrl . '/') === 0 ? + $this->id = (string)$account['pid']; + $this->username = $account['nick']; + $this->acct = strpos($account['url'], $baseUrl . '/') === 0 ? $account['nick'] : $account['addr']; - $this->display_name = $account['name']; - $this->locked = (bool)$account['manually-approve']; - $this->bot = ($account['contact-type'] == Contact::TYPE_NEWS); - $this->discoverable = !$account['unsearchable']; - $this->group = ($account['contact-type'] == Contact::TYPE_COMMUNITY); + $this->display_name = $account['name']; + $this->locked = (bool)$account['manually-approve']; + $this->bot = ($account['contact-type'] == Contact::TYPE_NEWS); + $this->discoverable = !$account['unsearchable']; + $this->indexable = $this->discoverable; + $this->group = ($account['contact-type'] == Contact::TYPE_COMMUNITY); - $this->created_at = DateTimeFormat::utc($account['created'] ?: DBA::NULL_DATETIME, DateTimeFormat::JSON); + $this->created_at = DateTimeFormat::utc($account['created'] ?: DBA::NULL_DATETIME, DateTimeFormat::JSON); $this->note = BBCode::convertForUriId($account['uri-id'], $account['about'], BBCode::EXTERNAL); - $this->url = $account['url']; + $this->url = $account['alias'] ?: $account['url']; + $this->uri = $account['url']; $this->avatar = Contact::getAvatarUrlForId($account['id'] ?? 0 ?: $account['pid'], Proxy::SIZE_SMALL, $account['updated'], $account['guid'] ?? ''); $this->avatar_static = Contact::getAvatarUrlForId($account['id'] ?? 0 ?: $account['pid'], Proxy::SIZE_SMALL, $account['updated'], $account['guid'] ?? '', true); $this->header = Contact::getHeaderUrlForId($account['id'] ?? 0 ?: $account['pid'], '', $account['updated'], $account['guid'] ?? ''); $this->header_static = Contact::getHeaderUrlForId($account['id'] ?? 0 ?: $account['pid'], '', $account['updated'], $account['guid'] ?? '', true); $this->followers_count = $account['ap-followers_count'] ?? $account['diaspora-interacted_count'] ?? 0; $this->following_count = $account['ap-following_count'] ?? $account['diaspora-interacting_count'] ?? 0; - $this->statuses_count = $account['ap-statuses_count'] ?? $account['diaspora-post_count'] ?? 0; + $this->statuses_count = $account['ap-statuses_count'] ?? $account['diaspora-post_count'] ?? 0; - $lastItem = $account['last-item'] ? DateTimeFormat::utc($account['last-item'], 'Y-m-d') : DBA::NULL_DATETIME; - $this->last_status_at = $lastItem != DBA::NULL_DATETIME ? DateTimeFormat::utc($lastItem, DateTimeFormat::JSON) : null; + $lastItem = $account['last-item'] ? DateTimeFormat::utc($account['last-item'], 'Y-m-d') : DBA::NULL_DATETIME; + $this->last_status_at = $lastItem != DBA::NULL_DATETIME ? DateTimeFormat::utc($lastItem, DateTimeFormat::JSON) : null; // No custom emojis per account in Friendica - $this->emojis = []; - $this->fields = $fields->getArrayCopy(); + $this->emojis = []; + $this->fields = $fields->getArrayCopy(); } /** diff --git a/src/Object/Api/Mastodon/Activity.php b/src/Object/Api/Mastodon/Activity.php index 168a5fb28c..5617bc7e7f 100644 --- a/src/Object/Api/Mastodon/Activity.php +++ b/src/Object/Api/Mastodon/Activity.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -42,7 +28,6 @@ class Activity extends BaseDataTransferObject /** * Creates an activity * - * @param array $item * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(int $week, int $statuses, int $logins, int $registrations) diff --git a/src/Object/Api/Mastodon/Application.php b/src/Object/Api/Mastodon/Application.php index 85451f88f8..8126bebe55 100644 --- a/src/Object/Api/Mastodon/Application.php +++ b/src/Object/Api/Mastodon/Application.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -42,14 +28,15 @@ class Application extends BaseDataTransferObject protected $redirect_uri; /** @var string */ protected $website; + /** @var string */ + protected $vapid_key; /** * Creates an application entry * - * @param array $item * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(string $name, string $client_id = null, string $client_secret = null, int $id = null, string $redirect_uri = null, string $website = null) + public function __construct(string $name, string $client_id = null, string $client_secret = null, int $id = null, string $redirect_uri = null, string $website = null, string $vapid_key = null) { $this->client_id = $client_id; $this->client_secret = $client_secret; @@ -57,6 +44,7 @@ class Application extends BaseDataTransferObject $this->name = $name; $this->redirect_uri = $redirect_uri; $this->website = $website; + $this->vapid_key = $vapid_key; } /** @@ -75,8 +63,8 @@ class Application extends BaseDataTransferObject unset($application['redirect_uri']); } - if (empty($application['website'])) { - unset($application['website']); + if (empty($application['vapid_key'])) { + unset($application['vapid_key']); } return $application; diff --git a/src/Object/Api/Mastodon/Attachment.php b/src/Object/Api/Mastodon/Attachment.php index aa6c5baacd..604ef41fac 100644 --- a/src/Object/Api/Mastodon/Attachment.php +++ b/src/Object/Api/Mastodon/Attachment.php @@ -1,29 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; use Friendica\BaseDataTransferObject; -use Friendica\Core\Logger; -use Friendica\Core\System; /** * Class Attachment @@ -43,6 +27,8 @@ class Attachment extends BaseDataTransferObject /** @var string */ protected $remote_url; /** @var string */ + protected $preview_remote_url; + /** @var string */ protected $text_url; /** @var string */ protected $description; @@ -63,26 +49,25 @@ class Attachment extends BaseDataTransferObject */ public function __construct(array $attachment, string $type, string $url, string $preview, string $remote) { - $this->id = (string)$attachment['id']; - $this->type = $type; - $this->url = $url; + $this->id = (string)$attachment['id']; + $this->type = $type; + $this->url = $url; $this->preview_url = $preview; - $this->remote_url = $remote; - $this->text_url = $this->remote_url ?? $this->url; + $this->remote_url = $remote; $this->description = $attachment['description']; - $this->blurhash = $attachment['blurhash']; + $this->blurhash = $attachment['blurhash']; if ($type === 'image') { if ((int) $attachment['width'] > 0 && (int) $attachment['height'] > 0) { - $this->meta['original']['width'] = (int) $attachment['width']; + $this->meta['original']['width'] = (int) $attachment['width']; $this->meta['original']['height'] = (int) $attachment['height']; - $this->meta['original']['size'] = (int) $attachment['width'] . 'x' . (int) $attachment['height']; + $this->meta['original']['size'] = (int) $attachment['width'] . 'x' . (int) $attachment['height']; $this->meta['original']['aspect'] = (float) ((int) $attachment['width'] / (int) $attachment['height']); } if (isset($attachment['preview-width']) && (int) $attachment['preview-width'] > 0 && (int) $attachment['preview-height'] > 0) { - $this->meta['small']['width'] = (int) $attachment['preview-width']; + $this->meta['small']['width'] = (int) $attachment['preview-width']; $this->meta['small']['height'] = (int) $attachment['preview-height']; - $this->meta['small']['size'] = (int) $attachment['preview-width'] . 'x' . (int) $attachment['preview-height']; + $this->meta['small']['size'] = (int) $attachment['preview-width'] . 'x' . (int) $attachment['preview-height']; $this->meta['small']['aspect'] = (float) ((int) $attachment['preview-width'] / (int) $attachment['preview-height']); } } diff --git a/src/Object/Api/Mastodon/Card.php b/src/Object/Api/Mastodon/Card.php index c4d21f66a3..7a1983ff8b 100644 --- a/src/Object/Api/Mastodon/Card.php +++ b/src/Object/Api/Mastodon/Card.php @@ -1,27 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; use Friendica\BaseDataTransferObject; +use Friendica\Util\DateTimeFormat; /** * Class Card @@ -37,6 +24,8 @@ class Card extends BaseDataTransferObject /** @var string */ protected $description; /** @var string */ + protected $language; + /** @var string */ protected $type; /** @var string */ protected $author_name; @@ -46,6 +35,8 @@ class Card extends BaseDataTransferObject protected $provider_name; /** @var string */ protected $provider_url; + /** @var string */ + protected $html; /** @var int */ protected $width; /** @var int */ @@ -53,7 +44,15 @@ class Card extends BaseDataTransferObject /** @var string */ protected $image; /** @var string */ + protected $image_description = ''; + /** @var string */ + protected $embed_url; + /** @var string */ protected $blurhash; + /** @var string|null (Datetime) */ + protected $published_at; + /** @var array */ + protected $authors = []; /** @var array */ protected $history; @@ -65,18 +64,22 @@ class Card extends BaseDataTransferObject */ public function __construct(array $attachment, array $history = []) { - $this->url = $attachment['url'] ?? ''; - $this->title = $attachment['title'] ?? ''; - $this->description = $attachment['description'] ?? ''; - $this->type = $attachment['type'] ?? ''; - $this->author_name = $attachment['author_name'] ?? ''; - $this->author_url = $attachment['author_url'] ?? ''; + $this->url = $attachment['url'] ?? ''; + $this->title = $attachment['title'] ?? ''; + $this->description = $attachment['description'] ?? ''; + $this->language = $attachment['language'] ?? ''; + $this->type = $attachment['type'] ?? ''; + $this->author_name = $attachment['author_name'] ?? ''; + $this->author_url = $attachment['author_url'] ?? ''; $this->provider_name = $attachment['provider_name'] ?? ''; - $this->provider_url = $attachment['provider_url'] ?? ''; - $this->width = $attachment['width'] ?? 0; + $this->provider_url = $attachment['provider_url'] ?? ''; + $this->html = ''; + $this->width = $attachment['width'] ?? 0; $this->height = $attachment['height'] ?? 0; - $this->image = $attachment['image'] ?? ''; + $this->image = $attachment['image'] ?? ''; + $this->embed_url = ''; $this->blurhash = $attachment['blurhash'] ?? ''; + $this->published_at = !empty($attachment['published']) ? DateTimeFormat::utc($attachment['published'], DateTimeFormat::JSON) : null; $this->history = $history; } @@ -91,6 +94,10 @@ class Card extends BaseDataTransferObject return []; } + if (empty($this->history)) { + unset($this->history); + } + return parent::toArray(); } } diff --git a/src/Object/Api/Mastodon/Conversation.php b/src/Object/Api/Mastodon/Conversation.php index 453783235c..c4cfc21762 100644 --- a/src/Object/Api/Mastodon/Conversation.php +++ b/src/Object/Api/Mastodon/Conversation.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/Emoji.php b/src/Object/Api/Mastodon/Emoji.php index 2db25727d5..a91f158e72 100644 --- a/src/Object/Api/Mastodon/Emoji.php +++ b/src/Object/Api/Mastodon/Emoji.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -46,7 +32,6 @@ class Emoji extends BaseDataTransferObject // Optional attributes /** * Unsupported - * @var string */ //protected $category; diff --git a/src/Object/Api/Mastodon/Error.php b/src/Object/Api/Mastodon/Error.php index d348ee6aee..945ebfc592 100644 --- a/src/Object/Api/Mastodon/Error.php +++ b/src/Object/Api/Mastodon/Error.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -37,9 +23,6 @@ class Error extends BaseDataTransferObject /** * Creates an error record - * - * @param string $error - * @param string error_description */ public function __construct(string $error, string $error_description) { diff --git a/src/Object/Api/Mastodon/ExtendedDescription.php b/src/Object/Api/Mastodon/ExtendedDescription.php new file mode 100644 index 0000000000..bcc4784ff5 --- /dev/null +++ b/src/Object/Api/Mastodon/ExtendedDescription.php @@ -0,0 +1,30 @@ +updated_at = $updated_at->format(DateTimeFormat::JSON); + $this->content = $content; + } +} diff --git a/src/Object/Api/Mastodon/Field.php b/src/Object/Api/Mastodon/Field.php index 086432b40a..092f33b361 100644 --- a/src/Object/Api/Mastodon/Field.php +++ b/src/Object/Api/Mastodon/Field.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -34,12 +20,12 @@ class Field extends BaseDataTransferObject protected $name; /** @var string (HTML) */ protected $value; - /** @var string (Datetime)*/ + /** @var string|null (Datetime)*/ protected $verified_at; public function __construct(string $name, string $value) { - $this->name = $name; + $this->name = $name; $this->value = $value; // Link verification unsupported $this->verified_at = null; diff --git a/src/Object/Api/Mastodon/Instance.php b/src/Object/Api/Mastodon/Instance.php index 6efb49c14a..baa06c8921 100644 --- a/src/Object/Api/Mastodon/Instance.php +++ b/src/Object/Api/Mastodon/Instance.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -38,44 +24,28 @@ use Friendica\Object\Api\Mastodon\InstanceV2\Configuration; */ class Instance extends BaseDataTransferObject { - /** @var string (URL) */ - protected $uri; - /** @var string */ - protected $title; - /** @var string */ - protected $short_description; - /** @var string */ - protected $description; - /** @var string */ - protected $email; - /** @var string */ - protected $version; - /** @var array */ - protected $urls; - /** @var Stats */ - protected $stats; - /** @var string|null This is meant as a server banner, default Mastodon "thumbnail" is 1600×620px */ - protected $thumbnail = null; - /** @var array */ - protected $languages; - /** @var int */ - protected $max_toot_chars; - /** @var bool */ - protected $registrations; - /** @var bool */ - protected $approval_required; - /** @var bool */ - protected $invites_enabled; - /** @var Configuration */ - protected $configuration; - /** @var Account|null */ - protected $contact_account = null; - /** @var array */ - protected $rules = []; + protected string $uri; + protected string $title; + protected string $short_description; + protected string $description; + protected string $email; + protected string $version; + protected array $urls; + protected Stats $stats; + /** This is meant as a server banner, default Mastodon "thumbnail" is 1600×620px */ + protected ?string $thumbnail = null; + protected array $languages; + protected int $max_toot_chars; + protected bool $registrations; + protected bool $approval_required; + protected bool $invites_enabled; + protected Configuration $configuration; + protected ?Account $contact_account = null; + protected array $rules = []; public function __construct(IManageConfigValues $config, BaseURL $baseUrl, Database $database, Configuration $configuration, ?Account $contact_account, array $rules) { - $register_policy = intval($config->get('config', 'register_policy')); + $register_policy = Register::getPolicy(); $this->uri = $baseUrl->getHost(); $this->title = $config->get('config', 'sitename'); @@ -87,11 +57,11 @@ class Instance extends BaseDataTransferObject $this->thumbnail = $baseUrl . (new Header($config))->getMastodonBannerPath(); $this->languages = [$config->get('system', 'language')]; $this->max_toot_chars = (int)$config->get('config', 'api_import_size', $config->get('config', 'max_import_size')); - $this->registrations = ($register_policy != Register::CLOSED); - $this->approval_required = ($register_policy == Register::APPROVE); + $this->registrations = ($register_policy !== Register::CLOSED); + $this->approval_required = ($register_policy === Register::APPROVE); $this->invites_enabled = false; $this->configuration = $configuration; - $this->contact_account = $contact_account ?? []; + $this->contact_account = $contact_account; $this->rules = $rules; } } diff --git a/src/Object/Api/Mastodon/InstanceV2.php b/src/Object/Api/Mastodon/InstanceV2.php index 89cca6daa7..26c5adabcb 100644 --- a/src/Object/Api/Mastodon/InstanceV2.php +++ b/src/Object/Api/Mastodon/InstanceV2.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/InstanceV2/Accounts.php b/src/Object/Api/Mastodon/InstanceV2/Accounts.php index 6996c08e30..fe89078eae 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Accounts.php +++ b/src/Object/Api/Mastodon/InstanceV2/Accounts.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/Configuration.php b/src/Object/Api/Mastodon/InstanceV2/Configuration.php index f06e335caa..79d5d5faad 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Configuration.php +++ b/src/Object/Api/Mastodon/InstanceV2/Configuration.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/Contact.php b/src/Object/Api/Mastodon/InstanceV2/Contact.php index 74a6f8e248..dc62c1e645 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Contact.php +++ b/src/Object/Api/Mastodon/InstanceV2/Contact.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/FriendicaExtensions.php b/src/Object/Api/Mastodon/InstanceV2/FriendicaExtensions.php index d3a2372105..5d467b3060 100644 --- a/src/Object/Api/Mastodon/InstanceV2/FriendicaExtensions.php +++ b/src/Object/Api/Mastodon/InstanceV2/FriendicaExtensions.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php b/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php index d9e9db4c4b..c8ee9da743 100644 --- a/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php +++ b/src/Object/Api/Mastodon/InstanceV2/MediaAttachmentsConfig.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; @@ -39,17 +25,19 @@ class MediaAttachmentsConfig extends BaseDataTransferObject /** @var int */ protected $video_size_limit = 0; /** @var int */ - protected $video_frame_rate_limit = 0; + protected $video_frame_rate_limit = 60; /** @var int */ protected $video_matrix_limit = 0; /** * @param array $supported_mime_types */ - public function __construct(array $supported_mime_types, int $image_size_limit, int $image_matrix_limit) + public function __construct(array $supported_mime_types, int $image_size_limit, int $image_matrix_limit, int $media_size_limit) { $this->supported_mime_types = $supported_mime_types; $this->image_size_limit = $image_size_limit; $this->image_matrix_limit = $image_matrix_limit; + $this->video_size_limit = $media_size_limit; + $this->video_matrix_limit = $image_matrix_limit; } } diff --git a/src/Object/Api/Mastodon/InstanceV2/Polls.php b/src/Object/Api/Mastodon/InstanceV2/Polls.php index f2d20def48..86aef8021c 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Polls.php +++ b/src/Object/Api/Mastodon/InstanceV2/Polls.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/Registrations.php b/src/Object/Api/Mastodon/InstanceV2/Registrations.php index c4c1089ed8..fb296d968c 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Registrations.php +++ b/src/Object/Api/Mastodon/InstanceV2/Registrations.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/StatusesConfig.php b/src/Object/Api/Mastodon/InstanceV2/StatusesConfig.php index c2e2e80aee..6fb321fd31 100644 --- a/src/Object/Api/Mastodon/InstanceV2/StatusesConfig.php +++ b/src/Object/Api/Mastodon/InstanceV2/StatusesConfig.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/Thumbnail.php b/src/Object/Api/Mastodon/InstanceV2/Thumbnail.php index 3d4989e367..49b60b3e00 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Thumbnail.php +++ b/src/Object/Api/Mastodon/InstanceV2/Thumbnail.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/Usage.php b/src/Object/Api/Mastodon/InstanceV2/Usage.php index 69e26b8ab7..551b8cffad 100644 --- a/src/Object/Api/Mastodon/InstanceV2/Usage.php +++ b/src/Object/Api/Mastodon/InstanceV2/Usage.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/InstanceV2/UserStats.php b/src/Object/Api/Mastodon/InstanceV2/UserStats.php index afe6048e5f..0f769f41b6 100644 --- a/src/Object/Api/Mastodon/InstanceV2/UserStats.php +++ b/src/Object/Api/Mastodon/InstanceV2/UserStats.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\InstanceV2; diff --git a/src/Object/Api/Mastodon/ListEntity.php b/src/Object/Api/Mastodon/ListEntity.php index ad526e892a..0bd613d0f9 100644 --- a/src/Object/Api/Mastodon/ListEntity.php +++ b/src/Object/Api/Mastodon/ListEntity.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -34,17 +20,17 @@ class ListEntity extends BaseDataTransferObject protected $id; /** @var string */ protected $title; + /** @var string */ + protected $replies_policy; /** * Creates an list record * - * @param int $id - * @param string $title * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function __construct(int $id, string $title, string $policy) + public function __construct(string $id, string $title, string $policy) { - $this->id = (string)$id; + $this->id = $id; $this->title = $title; $this->replies_policy = $policy; } diff --git a/src/Object/Api/Mastodon/Mention.php b/src/Object/Api/Mastodon/Mention.php index dca2c44f91..fe34ee80f9 100644 --- a/src/Object/Api/Mastodon/Mention.php +++ b/src/Object/Api/Mastodon/Mention.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/Notification.php b/src/Object/Api/Mastodon/Notification.php index 243a0bd82f..b18a7f1e9e 100644 --- a/src/Object/Api/Mastodon/Notification.php +++ b/src/Object/Api/Mastodon/Notification.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -58,9 +44,9 @@ class Notification extends BaseDataTransferObject protected $created_at; /** @var bool */ protected $dismissed; - /** @var Account */ + /** @var array */ protected $account; - /** @var Status|null */ + /** @var array|null */ protected $status = null; /** diff --git a/src/Object/Api/Mastodon/Poll.php b/src/Object/Api/Mastodon/Poll.php index 8cb47e77de..a17e8c57b5 100644 --- a/src/Object/Api/Mastodon/Poll.php +++ b/src/Object/Api/Mastodon/Poll.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -46,7 +32,7 @@ class Poll extends BaseDataTransferObject /** @var bool|null */ protected $voted = false; /** @var array|null */ - protected $own_votes = false; + protected $own_votes = null; /** @var array */ protected $options = []; /** @var Emoji[] */ @@ -61,7 +47,7 @@ class Poll extends BaseDataTransferObject * @param int $votes Number of total votes * @param array $ownvotes Own vote */ - public function __construct(array $question, array $options, bool $expired, int $votes, array $ownvotes = null) + public function __construct(array $question, array $options, bool $expired, int $votes, array $ownvotes = null, bool $voted = null) { $this->id = (string)$question['id']; $this->expires_at = !empty($question['end-time']) ? DateTimeFormat::utc($question['end-time'], DateTimeFormat::JSON) : null; @@ -69,9 +55,23 @@ class Poll extends BaseDataTransferObject $this->multiple = (bool)$question['multiple']; $this->votes_count = $votes; $this->voters_count = $this->multiple ? $question['voters'] : null; - $this->voted = null; + $this->voted = $voted; $this->own_votes = $ownvotes; $this->options = $options; $this->emojis = []; } + + public function toArray(): array + { + $status = parent::toArray(); + + if (is_null($status['voted'])) { + unset($status['voted']); + } + + if (is_null($status['own_votes'])) { + unset($status['own_votes']); + } + return $status; + } } diff --git a/src/Object/Api/Mastodon/Preferences.php b/src/Object/Api/Mastodon/Preferences.php index 99e706d20a..fcae499d92 100644 --- a/src/Object/Api/Mastodon/Preferences.php +++ b/src/Object/Api/Mastodon/Preferences.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -31,32 +17,58 @@ use Friendica\BaseDataTransferObject; */ class Preferences extends BaseDataTransferObject { -// /** @var string (Enumerable, oneOf) */ -// protected $posting_default_visibility; -// /** @var bool */ -// protected $posting_default_sensitive; -// /** @var string (ISO 639-1 language two-letter code), or null*/ -// protected $posting_default_language; -// /** @var string (Enumerable, oneOf) */ -// protected $reading_expand_media; -// /** @var bool */ -// protected $reading_expand_spoilers; + /** + * @var string (Enumerable, oneOf) + */ + private $visibility; + + /** + * @var bool + */ + private $sensitive; + + /** + * @var string (ISO 639-1 language two-letter code), or null + */ + private $language; + + /** + * @var string (Enumerable, oneOf) + */ + private $media; + + /** + * @var bool + */ + private $spoilers; /** * Creates a preferences record. * - * @param BaseURL $baseUrl - * @param array $publicContact Full contact table record with uid = 0 - * @param array $apcontact Optional full apcontact table record - * @param array $userContact Optional full contact table record with uid != 0 * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(string $visibility, bool $sensitive, string $language, string $media, bool $spoilers) { - $this->{'posting:default:visibility'} = $visibility; - $this->{'posting:default:sensitive'} = $sensitive; - $this->{'posting:default:language'} = $language; - $this->{'reading:expand:media'} = $media; - $this->{'reading:expand:spoilers'} = $spoilers; + $this->visibility = $visibility; + $this->sensitive = $sensitive; + $this->language = $language; + $this->media = $media; + $this->spoilers = $spoilers; + } + + /** + * Returns the current entity as an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'posting:default:visibility' => $this->visibility, + 'posting:default:sensitive' => $this->sensitive, + 'posting:default:language' => $this->language, + 'reading:expand:media' => $this->media, + 'reading:expand:spoilers' => $this->spoilers, + ]; } } diff --git a/src/Object/Api/Mastodon/Relationship.php b/src/Object/Api/Mastodon/Relationship.php index c042e81b5e..7a179dea3e 100644 --- a/src/Object/Api/Mastodon/Relationship.php +++ b/src/Object/Api/Mastodon/Relationship.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -32,7 +18,7 @@ use Friendica\Util\Network; */ class Relationship extends BaseDataTransferObject { - /** @var int */ + /** @var string */ protected $id; /** @var bool */ protected $following = false; @@ -60,15 +46,19 @@ class Relationship extends BaseDataTransferObject protected $blocking = false; /** @var bool */ protected $domain_blocking = false; + /** @var bool */ + protected $blocked_by = false; + /** + * Unsupported + * @var array + */ + protected $languages = []; /** * Unsupported * @var bool */ - protected $blocked_by = false; - /** - * Unsupported - * @var string - */ + protected $requested_by = false; + /** @var string */ protected $note = ''; /** @@ -77,7 +67,7 @@ class Relationship extends BaseDataTransferObject * @param bool $blocked "true" if user is blocked * @param bool $muted "true" if user is muted */ - public function __construct(int $contactId, array $contactRecord, bool $blocked = false, bool $muted = false) + public function __construct(int $contactId, array $contactRecord, bool $blocked = false, bool $muted = false, bool $isBlocked = false) { $this->id = (string)$contactId; $this->following = false; @@ -100,9 +90,8 @@ class Relationship extends BaseDataTransferObject $this->muting = (bool)($contactRecord['readonly'] ?? false) || $muted; $this->notifying = (bool)$contactRecord['notify_new_posts'] ?? false; $this->blocking = (bool)($contactRecord['blocked'] ?? false) || $blocked; + $this->blocked_by = $isBlocked; $this->note = $contactRecord['info']; } - - return $this; } } diff --git a/src/Object/Api/Mastodon/ScheduledStatus.php b/src/Object/Api/Mastodon/ScheduledStatus.php index 65506a21d5..630ebd7839 100644 --- a/src/Object/Api/Mastodon/ScheduledStatus.php +++ b/src/Object/Api/Mastodon/ScheduledStatus.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -49,7 +35,7 @@ class ScheduledStatus extends BaseDataTransferObject 'in_reply_to_id' => null, 'application_id' => '' ]; - /** @var Attachment */ + /** @var array */ protected $media_attachments = []; /** diff --git a/src/Object/Api/Mastodon/Stats.php b/src/Object/Api/Mastodon/Stats.php index c38ba33f65..ea46b1b800 100644 --- a/src/Object/Api/Mastodon/Stats.php +++ b/src/Object/Api/Mastodon/Stats.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/Status.php b/src/Object/Api/Mastodon/Status.php index 59d2a6cc58..652e6dde4d 100644 --- a/src/Object/Api/Mastodon/Status.php +++ b/src/Object/Api/Mastodon/Status.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -44,7 +30,7 @@ class Status extends BaseDataTransferObject protected $edited_at; /** @var string|null */ protected $in_reply_to_id = null; - /** @var Status|null - Fedilab extension, see issue https://github.com/friendica/friendica/issues/12672 */ + /** @var Status[]|null - Fedilab extension, see issue https://github.com/friendica/friendica/issues/12672 */ protected $in_reply_to_status = null; /** @var string|null */ protected $in_reply_to_account_id = null; @@ -78,25 +64,27 @@ class Status extends BaseDataTransferObject protected $pinned = false; /** @var string */ protected $content; - /** @var Status|null */ + /** @var array */ + protected $filtered = []; + /** @var Status[]|null */ protected $reblog = null; - /** @var Status|null - Akkoma extension, see issue https://github.com/friendica/friendica/issues/12603 */ + /** @var Status[]|null - Akkoma extension, see issue https://github.com/friendica/friendica/issues/12603 */ protected $quote = null; - /** @var Application */ + /** @var array */ protected $application = null; - /** @var Account */ + /** @var array */ protected $account; - /** @var Attachment */ + /** @var Attachment[] */ protected $media_attachments = []; - /** @var Mention */ + /** @var Mention[] */ protected $mentions = []; - /** @var Tag */ + /** @var Tag[] */ protected $tags = []; /** @var Emoji[] */ protected $emojis = []; - /** @var Card|null */ + /** @var array|null */ protected $card = null; - /** @var Poll|null */ + /** @var array|null */ protected $poll = null; /** @var FriendicaExtension */ protected $friendica; @@ -109,10 +97,10 @@ class Status extends BaseDataTransferObject */ public function __construct(array $item, Account $account, Counts $counts, UserAttributes $userAttributes, bool $sensitive, Application $application, array $mentions, array $tags, Card $card, array $attachments, array $in_reply, array $reblog, FriendicaExtension $friendica, array $quote = null, array $poll = null, array $emojis = null) { - $reblogged = !empty($reblog); - $this->id = (string)$item['uri-id']; - $this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::JSON); - $this->edited_at = DateTimeFormat::utc($item['edited'], DateTimeFormat::JSON); + $reblogged = !empty($reblog); + $this->id = (string)$item['uri-id']; + $this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::JSON); + $this->edited_at = DateTimeFormat::utc($item['edited'], DateTimeFormat::JSON); if ($item['gravity'] == Item::GRAVITY_COMMENT) { $this->in_reply_to_id = (string)$item['thr-parent-id']; @@ -123,7 +111,7 @@ class Status extends BaseDataTransferObject $this->sensitive = $sensitive; $this->spoiler_text = $item['title'] ?: $item['content-warning'] ?: ''; - $visibility = ['public', 'private', 'unlisted']; + $visibility = ['public', 'private', 'unlisted']; $this->visibility = $visibility[$item['private']]; $languages = json_decode($item['language'] ?? '', true); @@ -134,33 +122,33 @@ class Status extends BaseDataTransferObject $this->language = null; } - $this->uri = $item['uri']; - $this->url = $item['plink'] ?? null; - $this->replies_count = $reblogged ? 0 : $counts->replies; - $this->reblogs_count = $reblogged ? 0 : $counts->reblogs; - $this->favourites_count = $reblogged ? 0 : $counts->favourites; - $this->favourited = $userAttributes->favourited; - $this->reblogged = $userAttributes->reblogged; - $this->muted = $userAttributes->muted; - $this->bookmarked = $userAttributes->bookmarked; - $this->pinned = $userAttributes->pinned; - $this->content = $reblogged ? '' : BBCode::convertForUriId($item['uri-id'], BBCode::setMentionsToNicknames($item['raw-body'] ?? $item['body']), BBCode::MASTODON_API); - $this->reblog = $reblog; - $this->quote = $quote; - $this->application = $application->toArray(); - $this->account = $account->toArray(); + $this->uri = $item['uri']; + $this->url = $item['plink'] ?? null; + $this->replies_count = $reblogged ? 0 : $counts->replies; + $this->reblogs_count = $reblogged ? 0 : $counts->reblogs; + $this->favourites_count = $reblogged ? 0 : $counts->favourites; + $this->favourited = $userAttributes->favourited; + $this->reblogged = $userAttributes->reblogged; + $this->muted = $userAttributes->muted; + $this->bookmarked = $userAttributes->bookmarked; + $this->pinned = $userAttributes->pinned; + $this->content = $reblogged ? '' : BBCode::convertForUriId($item['uri-id'], BBCode::setMentionsToNicknames($item['raw-body'] ?? $item['body']), BBCode::MASTODON_API); + $this->reblog = $reblog; + $this->quote = $quote; + $this->application = $application->toArray(); + $this->account = $account->toArray(); $this->media_attachments = $reblogged ? [] : $attachments; - $this->mentions = $reblogged ? [] : $mentions; - $this->tags = $reblogged ? [] : $tags; - $this->emojis = $reblogged ? [] : ($emojis ?: []); - $this->card = $reblogged ? null : ($card->toArray() ?: null); - $this->poll = $reblogged ? null : $poll; - $this->friendica = $reblogged ? null : $friendica; + $this->mentions = $reblogged ? [] : $mentions; + $this->tags = $reblogged ? [] : $tags; + $this->emojis = $reblogged ? [] : ($emojis ?: []); + $this->card = $reblogged ? null : ($card->toArray() ?: null); + $this->poll = $reblogged ? null : $poll; + $this->friendica = $reblogged ? null : $friendica; } /** * Returns the current created_at string or null if not set - * @return \DateTime|null + * @return ?string */ public function createdAt(): ?string { diff --git a/src/Object/Api/Mastodon/Status/Counts.php b/src/Object/Api/Mastodon/Status/Counts.php index 207f78a43d..a28508d2ea 100644 --- a/src/Object/Api/Mastodon/Status/Counts.php +++ b/src/Object/Api/Mastodon/Status/Counts.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\Status; @@ -25,6 +11,11 @@ namespace Friendica\Object\Api\Mastodon\Status; * Class Counts * * @see https://docs.joinmastodon.org/entities/status + * + * @property-read int $replies + * @property-read int $reblogs + * @property-read int $favourites + * @property-read int $dislikes */ class Counts { diff --git a/src/Object/Api/Mastodon/Status/FriendicaDeliveryData.php b/src/Object/Api/Mastodon/Status/FriendicaDeliveryData.php index 3da0eae57c..31e1b30e4f 100644 --- a/src/Object/Api/Mastodon/Status/FriendicaDeliveryData.php +++ b/src/Object/Api/Mastodon/Status/FriendicaDeliveryData.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\Status; diff --git a/src/Object/Api/Mastodon/Status/FriendicaExtension.php b/src/Object/Api/Mastodon/Status/FriendicaExtension.php index b204fc530d..4564be254b 100644 --- a/src/Object/Api/Mastodon/Status/FriendicaExtension.php +++ b/src/Object/Api/Mastodon/Status/FriendicaExtension.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\Status; @@ -54,11 +40,26 @@ class FriendicaExtension extends BaseDataTransferObject /** @var bool */ protected $disliked = false; + /** @var string|null */ + protected $network; + + /** @var string|null */ + protected $platform; + + /** @var string|null */ + protected $version; + + /** @var string|null */ + protected $sitename; + /** * @var FriendicaVisibility|null */ protected $visibility; + /** @var string|null */ + protected $content; + /** * Creates a FriendicaExtension object * @@ -68,6 +69,10 @@ class FriendicaExtension extends BaseDataTransferObject * @param ?string $received_at * @param int $dislikes_count * @param bool $disliked + * @param ?string $network + * @param ?string $platform + * @param ?string $version + * @param ?string $sitename * @param ?FriendicaDeliveryData $delivery_data * @param ?FriendicaVisibility $visibility * @throws \Exception @@ -79,8 +84,13 @@ class FriendicaExtension extends BaseDataTransferObject ?string $received_at, int $dislikes_count, bool $disliked, + ?string $network, + ?string $platform, + ?string $version, + ?string $sitename, ?FriendicaDeliveryData $delivery_data, - ?FriendicaVisibility $visibility + ?FriendicaVisibility $visibility, + ?string $content ) { $this->title = $title; $this->changed_at = $changed_at ? DateTimeFormat::utc($changed_at, DateTimeFormat::JSON) : null; @@ -89,7 +99,12 @@ class FriendicaExtension extends BaseDataTransferObject $this->delivery_data = $delivery_data; $this->dislikes_count = $dislikes_count; $this->disliked = $disliked; + $this->network = $network; + $this->platform = $platform; + $this->version = $version; + $this->sitename = $sitename; $this->visibility = $visibility; + $this->content = $content; } /** diff --git a/src/Object/Api/Mastodon/Status/FriendicaVisibility.php b/src/Object/Api/Mastodon/Status/FriendicaVisibility.php index 6ded2bfc36..50e0a7d661 100644 --- a/src/Object/Api/Mastodon/Status/FriendicaVisibility.php +++ b/src/Object/Api/Mastodon/Status/FriendicaVisibility.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\Status; diff --git a/src/Object/Api/Mastodon/Status/UserAttributes.php b/src/Object/Api/Mastodon/Status/UserAttributes.php index ecc74af44a..0e7ae09e9d 100644 --- a/src/Object/Api/Mastodon/Status/UserAttributes.php +++ b/src/Object/Api/Mastodon/Status/UserAttributes.php @@ -1,29 +1,21 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon\Status; /** * Class UserAttributes * + * @property-read bool $favourited + * @property-read bool $reblogged + * @property-read bool $muted + * @property-read bool $bookmarked + * @property-read bool $pinned + * * @see https://docs.joinmastodon.org/entities/status */ class UserAttributes diff --git a/src/Object/Api/Mastodon/StatusSource.php b/src/Object/Api/Mastodon/StatusSource.php index 478133a616..ac9179608a 100644 --- a/src/Object/Api/Mastodon/StatusSource.php +++ b/src/Object/Api/Mastodon/StatusSource.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/Subscription.php b/src/Object/Api/Mastodon/Subscription.php index 8b0a84b0b1..5f7b3be066 100644 --- a/src/Object/Api/Mastodon/Subscription.php +++ b/src/Object/Api/Mastodon/Subscription.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/Tag.php b/src/Object/Api/Mastodon/Tag.php index 331dd177ac..808e4c3cea 100644 --- a/src/Object/Api/Mastodon/Tag.php +++ b/src/Object/Api/Mastodon/Tag.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; @@ -46,7 +32,7 @@ class Tag extends BaseDataTransferObject * @param BaseURL $baseUrl * @param array $tag tag-view record * @param array $history - * @param array $following "true" if the user is following this tag + * @param bool $following "true" if the user is following this tag * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(BaseURL $baseUrl, array $tag, array $history = [], bool $following = false) @@ -56,4 +42,13 @@ class Tag extends BaseDataTransferObject $this->history = $history; $this->following = $following; } + + public function toArray(): array + { + if (empty($this->history)) { + unset($this->history); + } + + return parent::toArray(); + } } diff --git a/src/Object/Api/Mastodon/TimelineOrderByTypes.php b/src/Object/Api/Mastodon/TimelineOrderByTypes.php index a58f892733..f762b93da1 100644 --- a/src/Object/Api/Mastodon/TimelineOrderByTypes.php +++ b/src/Object/Api/Mastodon/TimelineOrderByTypes.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Mastodon/Token.php b/src/Object/Api/Mastodon/Token.php index 706054c982..a31ce3c66b 100644 --- a/src/Object/Api/Mastodon/Token.php +++ b/src/Object/Api/Mastodon/Token.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Mastodon; diff --git a/src/Object/Api/Twitter/Attachment.php b/src/Object/Api/Twitter/Attachment.php index 54926cd462..8c5bcea305 100644 --- a/src/Object/Api/Twitter/Attachment.php +++ b/src/Object/Api/Twitter/Attachment.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -40,7 +26,6 @@ class Attachment extends BaseDataTransferObject /** * Creates an Attachment entity array * - * @param array $attachment * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(array $media) diff --git a/src/Object/Api/Twitter/DirectMessage.php b/src/Object/Api/Twitter/DirectMessage.php index 9ec7d54394..199d9462ed 100644 --- a/src/Object/Api/Twitter/DirectMessage.php +++ b/src/Object/Api/Twitter/DirectMessage.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -43,9 +29,9 @@ class DirectMessage extends BaseDataTransferObject protected $sender_screen_name = null; /** @var string */ protected $recipient_screen_name = null; - /** @var User */ + /** @var array */ protected $sender; - /** @var User */ + /** @var array */ protected $recipient; /** @var string|null */ protected $title; diff --git a/src/Object/Api/Twitter/Hashtag.php b/src/Object/Api/Twitter/Hashtag.php index eb5109748c..de8ed71cf6 100644 --- a/src/Object/Api/Twitter/Hashtag.php +++ b/src/Object/Api/Twitter/Hashtag.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -38,7 +24,6 @@ class Hashtag extends BaseDataTransferObject /** * Creates a hashtag * - * @param array $attachment * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(string $name, array $indices) diff --git a/src/Object/Api/Twitter/Media.php b/src/Object/Api/Twitter/Media.php index 9d740ab8dc..589aebcfc1 100644 --- a/src/Object/Api/Twitter/Media.php +++ b/src/Object/Api/Twitter/Media.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -45,7 +31,7 @@ class Media extends BaseDataTransferObject protected $media_url; /** @var string */ protected $media_url_https; - /** @var string */ + /** @var array> */ protected $sizes; /** @var string */ protected $type; @@ -55,7 +41,6 @@ class Media extends BaseDataTransferObject /** * Creates a media entity array * - * @param array $attachment * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(array $media, string $url, array $indices) diff --git a/src/Object/Api/Twitter/Mention.php b/src/Object/Api/Twitter/Mention.php index f187bb9122..d2a4d5065f 100644 --- a/src/Object/Api/Twitter/Mention.php +++ b/src/Object/Api/Twitter/Mention.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; -use Friendica\App\BaseURL; use Friendica\BaseDataTransferObject; /** @@ -45,14 +30,13 @@ class Mention extends BaseDataTransferObject /** * Creates a mention record from an tag-view record. * - * @param BaseURL $baseUrl * @param array $tag tag-view record * @param array $contact contact table record * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(array $tag, array $contact, array $indices) { - $this->id = (string)($contact['id'] ?? 0); + $this->id = (int)($contact['id'] ?? 0); $this->id_str = (string)($contact['id'] ?? 0); $this->indices = $indices; $this->name = $tag['name']; diff --git a/src/Object/Api/Twitter/SavedSearch.php b/src/Object/Api/Twitter/SavedSearch.php index 51a7c02878..28f62f83f4 100644 --- a/src/Object/Api/Twitter/SavedSearch.php +++ b/src/Object/Api/Twitter/SavedSearch.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -48,7 +34,6 @@ class SavedSearch extends BaseDataTransferObject /** * Creates a saved search record from a search record. * - * @param BaseURL $baseUrl * @param array $search Full search table record */ public function __construct(array $search) diff --git a/src/Object/Api/Twitter/Status.php b/src/Object/Api/Twitter/Status.php index f9ce6a209d..1c8a4ef7ae 100644 --- a/src/Object/Api/Twitter/Status.php +++ b/src/Object/Api/Twitter/Status.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -59,11 +45,11 @@ class Status extends BaseDataTransferObject protected $geo; /** @var bool */ protected $favorited = false; - /** @var User */ + /** @var array */ protected $user; - /** @var User */ + /** @var array */ protected $friendica_author; - /** @var User */ + /** @var array */ protected $friendica_owner; /** @var bool */ protected $friendica_private; @@ -81,9 +67,9 @@ class Status extends BaseDataTransferObject protected $friendica_html; /** @var int */ protected $friendica_comments; - /** @var Status|null */ + /** @var array|null */ protected $retweeted_status = null; - /** @var Status|null */ + /** @var array|null */ protected $quoted_status = null; /** @var array */ protected $attachments; @@ -135,7 +121,7 @@ class Status extends BaseDataTransferObject $this->entities = $entities; $this->extended_entities = $entities; - $origin = ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network']); + $origin = ContactSelector::networkToName($item['author-network'], $item['network'], $item['author-gsid']); if (empty($this->source)) { $this->source = $origin; diff --git a/src/Object/Api/Twitter/Url.php b/src/Object/Api/Twitter/Url.php index c05847e0a8..92c2727155 100644 --- a/src/Object/Api/Twitter/Url.php +++ b/src/Object/Api/Twitter/Url.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -42,7 +28,6 @@ class Url extends BaseDataTransferObject /** * Creates an URL entity array * - * @param array $attachment * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public function __construct(array $media, array $indices) diff --git a/src/Object/Api/Twitter/User.php b/src/Object/Api/Twitter/User.php index 3ad5c5461e..151a968bff 100644 --- a/src/Object/Api/Twitter/User.php +++ b/src/Object/Api/Twitter/User.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Api\Twitter; @@ -76,7 +62,7 @@ class User extends BaseDataTransferObject protected $default_profile; /** @var bool */ protected $default_profile_image; - /** @var Status */ + /** @var array */ protected $status; /** @var array */ protected $withheld_in_countries; @@ -131,12 +117,11 @@ class User extends BaseDataTransferObject * @param array $publicContact Full contact table record with uid = 0 * @param array $apcontact Optional full apcontact table record * @param array $userContact Optional full contact table record with uid != 0 - * @param null $status * @param bool $include_user_entities Whether to add the entities property * * @throws InternalServerErrorException */ - public function __construct(array $publicContact, array $apcontact = [], array $userContact = [], $status = null, bool $include_user_entities = true) + public function __construct(array $publicContact, array $apcontact = [], array $userContact = [], ?Status $status = null, bool $include_user_entities = true) { $uid = $userContact['uid'] ?? 0; @@ -145,7 +130,7 @@ class User extends BaseDataTransferObject $this->name = $publicContact['name'] ?: $publicContact['nick']; $this->screen_name = $publicContact['nick'] ?: $publicContact['name']; $this->location = $publicContact['location'] ?: - ContactSelector::networkToName($publicContact['network'], $publicContact['url'], $publicContact['protocol']); + ContactSelector::networkToName($publicContact['network'], $publicContact['protocol'], $publicContact['gsid']); $this->derived = []; $this->url = $publicContact['url']; // No entities needed since we don't perform any shortening in the URL or description @@ -170,10 +155,10 @@ class User extends BaseDataTransferObject $this->default_profile = false; $this->default_profile_image = false; - if (!empty($status)) { - $this->status = $status; - } else { + if ($status === null) { unset($this->status); + } else { + $this->status = $status->toArray(); } // Unused optional fields diff --git a/src/Object/EMail/IEmail.php b/src/Object/EMail/IEmail.php index f4ecc71411..fa054a856f 100644 --- a/src/Object/EMail/IEmail.php +++ b/src/Object/EMail/IEmail.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\EMail; -use Friendica\Util\Emailer; use JsonSerializable; /** diff --git a/src/Object/EMail/ItemCCEMail.php b/src/Object/EMail/ItemCCEMail.php index 8c52ec74ca..9edcea5b77 100644 --- a/src/Object/EMail/ItemCCEMail.php +++ b/src/Object/EMail/ItemCCEMail.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\EMail; @@ -25,30 +11,30 @@ use Friendica\App; use Friendica\App\BaseURL; use Friendica\Content\Text\HTML; use Friendica\Core\L10n; +use Friendica\Core\Session\Model\UserSession; use Friendica\Model\Item; use Friendica\Model\User; use Friendica\Object\Email; -use Friendica\Protocol\Email as EmailProtocol; /** * Class for creating CC emails based on a received item */ class ItemCCEMail extends Email { - public function __construct(App $a, L10n $l10n, BaseURL $baseUrl, array $item, string $toAddress, string $authorThumb) + public function __construct(UserSession $session, L10n $l10n, BaseURL $baseUrl, array $item, string $toAddress, string $authorThumb) { - $user = User::getById($a->getLoggedInUserId()); + $user = User::getById($session->getLocalUserId()); $disclaimer = '
                              ' . $l10n->t('This message was sent to you by %s, a member of the Friendica social network.', $user['username']) . '
                              '; - $disclaimer .= $l10n->t('You may visit them online at %s', $baseUrl . '/profile/' . $a->getLoggedInUserNickname()) . '
                              '; + $disclaimer .= $l10n->t('You may visit them online at %s', $baseUrl . '/profile/' . $session->getLocalUserNickname()) . '
                              '; $disclaimer .= $l10n->t('Please contact the sender by replying to this post if you do not wish to receive these messages.') . '
                              '; if (!$item['title'] == '') { $subject = $item['title']; } else { $subject = '[Friendica]' . ' ' . $l10n->t('%s posted an update.', $user['username']); } - $link = '' . $user['username'] . '

                              '; + $link = '' . $user['username'] . '

                              '; $html = Item::prepareBody($item); $message = '' . $link . $html . $disclaimer . '';; diff --git a/src/Object/Email.php b/src/Object/Email.php index f6a7298523..5eb7b26987 100644 --- a/src/Object/Email.php +++ b/src/Object/Email.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object; diff --git a/src/Object/Image.php b/src/Object/Image.php index 28fa636be2..0aad761341 100644 --- a/src/Object/Image.php +++ b/src/Object/Image.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object; @@ -27,7 +13,7 @@ use Friendica\Util\Images; use Imagick; use ImagickDraw; use ImagickPixel; -use GDImage; +use GdImage; use kornrunner\Blurhash\Blurhash; /** @@ -35,7 +21,7 @@ use kornrunner\Blurhash\Blurhash; */ class Image { - /** @var GDImage|Imagick|resource */ + /** @var GdImage|Imagick|resource */ private $image; /* @@ -45,25 +31,39 @@ class Image private $width; private $height; private $valid; - private $type; - private $types; + private $outputType; + private $originType; + private $filename; /** * Constructor * - * @param string $data Image data - * @param string $type optional, default null + * @param string $data Image data + * @param string $type optional, default '' + * @param string $filename optional, default '' + * @param bool $imagick optional, default 'true' * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public function __construct(string $data, string $type = null) + public function __construct(string $data, string $type = '', string $filename = '', bool $imagick = true) { - $this->imagick = class_exists('Imagick'); - $this->types = Images::supportedTypes(); - if (!array_key_exists($type, $this->types)) { - $type = 'image/jpeg'; + $this->filename = $filename; + $type = Images::addMimeTypeByDataIfInvalid($type, $data); + $type = Images::addMimeTypeByExtensionIfInvalid($type, $filename); + + if (Images::isSupportedMimeType($type)) { + $this->originType = $this->outputType = Images::getImageTypeByMimeType($type); + } elseif (($type == '') || substr($type, 0, 6) == 'image/' || substr($type, 0, 12) == ' application/') { + $this->originType = IMAGETYPE_UNKNOWN; + $this->outputType = IMAGETYPE_WEBP; + DI::logger()->debug('Unhandled image mime type, use WebP instead', ['type' => $type, 'filename' => $filename, 'size' => strlen($data)]); + } else { + DI::logger()->debug('Unhandled mime type', ['type' => $type, 'filename' => $filename, 'size' => strlen($data)]); + $this->valid = false; + return; } - $this->type = $type; + + $this->imagick = $imagick && $this->useImagick($data); if ($this->isImagick() && (empty($data) || $this->loadData($data))) { $this->valid = !empty($data); @@ -75,6 +75,54 @@ class Image $this->loadData($data); } + /** + * Check if Imagick will be used + * + * @param string $data + * @return boolean + */ + private function useImagick(string $data): bool + { + if (!class_exists('Imagick')) { + return false; + } + + if ($this->outputType == IMAGETYPE_PNG) { + return true; + } + + if ($this->originType == IMAGETYPE_GIF) { + $count = preg_match_all("#\\x00\\x21\\xF9\\x04.{4}\\x00[\\x2C\\x21]#s", $data); + return ($count > 0); + } + + return (($this->originType == IMAGETYPE_WEBP) && $this->isAnimatedWebP(substr($data, 0, 90))); + } + + /** + * Detect if a WebP image is animated. + * @see https://www.php.net/manual/en/function.imagecreatefromwebp.php#126269 + * @param string $data + * @return boolean + */ + private function isAnimatedWebP(string $data) + { + $header_format = 'A4Riff/I1Filesize/A4Webp/A4Vp/A74Chunk'; + $header = @unpack($header_format, $data); + + if (!isset($header['Riff']) || strtoupper($header['Riff']) !== 'RIFF') { + return false; + } + if (!isset($header['Webp']) || strtoupper($header['Webp']) !== 'WEBP') { + return false; + } + if (!isset($header['Vp']) || strpos(strtoupper($header['Vp']), 'VP8') === false) { + return false; + } + + return strpos(strtoupper($header['Chunk']), 'ANIM') !== false || strpos(strtoupper($header['Chunk']), 'ANMF') !== false; + } + /** * Destructor * @@ -118,28 +166,28 @@ class Image $this->image->readImageBlob($data); } catch (Exception $e) { // Imagick couldn't use the data + DI::logger()->debug('Error during readImageBlob', ['message' => $e->getMessage(), 'code' => $e->getCode(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious(), 'file' => $this->filename]); return false; } /* * Setup the image to the format it will be saved to */ - $map = Images::getFormatsMap(); - $format = $map[$this->type]; - $this->image->setFormat($format); + $this->image->setFormat(Images::getImagickFormatByImageType($this->outputType)); // Always coalesce, if it is not a multi-frame image it won't hurt anyway try { $this->image = $this->image->coalesceImages(); } catch (Exception $e) { + DI::logger()->debug('Error during coalesceImages', ['message' => $e->getMessage(), 'code' => $e->getCode(), 'trace' => $e->getTraceAsString(), 'previous' => $e->getPrevious(), 'file' => $this->filename]); return false; } /* * setup the compression here, so we'll do it only once */ - switch ($this->getType()) { - case 'image/png': + switch ($this->getImageType()) { + case IMAGETYPE_PNG: $quality = DI::config()->get('system', 'png_quality'); /* * From http://www.imagemagick.org/script/command-line-options.php#quality: @@ -150,13 +198,12 @@ class Image * unless the image has a color map, in which case it means compression level 7 with no PNG filtering' */ $quality = $quality * 10; - $this->image->setCompressionQuality($quality); + $this->image->setImageCompressionQuality($quality); break; - case 'image/jpg': - case 'image/jpeg': + case IMAGETYPE_JPEG: $quality = DI::config()->get('system', 'jpeg_quality'); - $this->image->setCompressionQuality($quality); + $this->image->setImageCompressionQuality($quality); } $this->width = $this->image->getImageWidth(); @@ -167,6 +214,12 @@ class Image } $this->valid = false; + + if (($this->originType == IMAGETYPE_WEBP) && $this->isAnimatedWebP(substr($data, 0, 90))) { + DI::logger()->notice('Animated WebP images are unsupported by GDlib. Please install Imagick.', ['file' => $this->filename]); + return false; + } + try { $this->image = @imagecreatefromstring($data); if ($this->image !== false) { @@ -175,15 +228,16 @@ class Image $this->valid = true; imagealphablending($this->image, false); imagesavealpha($this->image, true); + imageinterlace($this->image, true); return true; } } catch (\Throwable $error) { /** @see https://github.com/php/doc-en/commit/d09a881a8e9059d11e756ee59d75bf404d6941ed */ if (strstr($error->getMessage(), "gd-webp cannot allocate temporary buffer")) { - DI::logger()->notice('Image is probably animated and therefore unsupported', ['error' => $error]); + DI::logger()->notice('Image is probably animated and therefore unsupported', ['message' => $error->getMessage(), 'code' => $error->getCode(), 'trace' => $error->getTraceAsString(), 'file' => $this->filename]); } else { - DI::logger()->warning('Unexpected throwable.', ['error' => $error]); + DI::logger()->warning('Unexpected throwable.', ['message' => $error->getMessage(), 'code' => $error->getCode(), 'trace' => $error->getTraceAsString(), 'file' => $this->filename]); } } @@ -255,7 +309,19 @@ class Image return false; } - return $this->type; + return image_type_to_mime_type($this->outputType); + } + + /** + * @return mixed + */ + public function getImageType() + { + if (!$this->isValid()) { + return false; + } + + return $this->outputType; } /** @@ -267,7 +333,7 @@ class Image return false; } - return $this->types[$this->getType()]; + return Images::getExtensionByImageType($this->outputType); } /** @@ -291,7 +357,6 @@ class Image } else { return false; } - } /** @@ -397,7 +462,7 @@ class Image return false; } - if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) { + if ((!function_exists('exif_read_data')) || ($this->getImageType() !== IMAGETYPE_JPEG)) { return; } @@ -461,7 +526,7 @@ class Image $width = $this->getWidth(); $height = $this->getHeight(); - if ((!$width)|| (!$height)) { + if ((!$width) || (!$height)) { return false; } @@ -518,6 +583,9 @@ class Image if (!$this->isValid()) { return false; } + if ($dest_width <= 0 || $dest_height <= 0) { + return false; + } if ($this->isImagick()) { /* @@ -544,7 +612,7 @@ class Image imagealphablending($dest, false); imagesavealpha($dest, true); - if ($this->type=='image/png') { + if ($this->outputType == IMAGETYPE_PNG) { imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha } @@ -569,13 +637,13 @@ class Image */ public function toStatic() { - if ($this->type != 'image/gif') { + if ($this->outputType != IMAGETYPE_GIF) { return; } if ($this->isImagick()) { - $this->type == 'image/png'; - $this->image->setFormat('png'); + $this->outputType = IMAGETYPE_PNG; + $this->image->setFormat(Images::getImagickFormatByImageType($this->outputType)); } } @@ -613,7 +681,7 @@ class Image imagealphablending($dest, false); imagesavealpha($dest, true); - if ($this->type=='image/png') { + if ($this->outputType == IMAGETYPE_PNG) { imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha } imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h); @@ -665,22 +733,30 @@ class Image } } - $stream = fopen('php://memory','r+'); + $stream = fopen('php://memory', 'r+'); - // Enable interlacing - imageinterlace($this->image, true); - - switch ($this->getType()) { - case 'image/png': + switch ($this->getImageType()) { + case IMAGETYPE_PNG: $quality = DI::config()->get('system', 'png_quality'); imagepng($this->image, $stream, $quality); break; - case 'image/jpeg': - case 'image/jpg': + case IMAGETYPE_JPEG: $quality = DI::config()->get('system', 'jpeg_quality'); imagejpeg($this->image, $stream, $quality); break; + + case IMAGETYPE_GIF: + imagegif($this->image, $stream); + break; + + case IMAGETYPE_WEBP: + @imagewebp($this->image, $stream, DI::config()->get('system', 'jpeg_quality')); + break; + + case IMAGETYPE_BMP: + imagebmp($this->image, $stream); + break; } rewind($stream); return stream_get_contents($stream); @@ -689,13 +765,12 @@ class Image /** * Create a blurhash out of a given image string * - * @param string $img_str * @return string */ - public function getBlurHash(): string + public function getBlurHash(string $img_str = ''): string { - $image = New Image($this->asString()); - if (empty($image) || !$this->isValid()) { + $image = new Image($img_str ?: $this->asString(), $this->getType(), $this->filename, false); + if (!$this->isValid()) { return ''; } @@ -751,6 +826,7 @@ class Image { $scaled = Images::getScalingDimensions($width, $height, 90); $pixels = Blurhash::decode($blurhash, $scaled['width'], $scaled['height']); + $draw = null; if ($this->isImagick()) { $this->image = new Imagick(); @@ -763,7 +839,7 @@ class Image for ($y = 0; $y < $scaled['height']; ++$y) { for ($x = 0; $x < $scaled['width']; ++$x) { [$r, $g, $b] = $pixels[$y][$x]; - if ($this->isImagick()) { + if ($draw !== null) { $draw->setFillColor("rgb($r, $g, $b)"); $draw->point($x, $y); } else { @@ -772,7 +848,7 @@ class Image } } - if ($this->isImagick()) { + if ($draw !== null) { $this->image->drawImage($draw); $this->width = $this->image->getImageWidth(); $this->height = $this->image->getImageHeight(); diff --git a/src/Object/Log/ParsedLogLine.php b/src/Object/Log/ParsedLogLine.php index 33f9ad6650..8e552008a4 100644 --- a/src/Object/Log/ParsedLogLine.php +++ b/src/Object/Log/ParsedLogLine.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Log; @@ -43,7 +29,7 @@ class ParsedLogLine /** @var string */ public $message = null; - /** @var string */ + /** @var string|null */ public $data = null; /** @var string */ @@ -53,7 +39,7 @@ class ParsedLogLine public $logline; /** - * @param int line id + * @param int $id line id * @param string $logline Source log line to parse */ public function __construct(int $id, string $logline) diff --git a/src/Object/OEmbed.php b/src/Object/OEmbed.php index 3f53f48cd1..c5d7bfaac7 100644 --- a/src/Object/OEmbed.php +++ b/src/Object/OEmbed.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object; diff --git a/src/Object/Post.php b/src/Object/Post.php index d52a05dddc..b4fcfcc094 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -1,35 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object; use Friendica\Content\ContactSelector; use Friendica\Content\Feature; -use Friendica\Core\Addon; -use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Renderer; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; +use Friendica\Model\Conversation; use Friendica\Model\Item; use Friendica\Model\Post as PostModel; use Friendica\Model\Tag; @@ -47,31 +32,31 @@ use InvalidArgumentException; */ class Post { - private $data = []; - private $template = null; + private $data = []; + private $template = null; private $available_templates = [ - 'wall' => 'wall_thread.tpl', + 'wall' => 'wall_thread.tpl', 'wall2wall' => 'wallwall_thread.tpl' ]; private $comment_box_template = 'comment_item.tpl'; - private $toplevel = false; - private $writable = false; + private $toplevel = false; + private $writable = false; /** * @var Post[] */ private $children = []; - private $parent = null; + private $parent = null; /** - * @var Thread + * @var Thread|null */ - private $thread = null; + private $thread = null; private $redirect_url = null; - private $owner_url = ''; - private $owner_name = ''; + private $owner_url = ''; + private $owner_name = ''; private $wall_to_wall = false; - private $threaded = false; - private $visiting = false; + private $threaded = false; + private $visiting = false; /** * Constructor @@ -85,12 +70,12 @@ class Post $this->setTemplate('wall'); $this->toplevel = $this->getId() == $this->getDataValue('parent'); - if (!empty(DI::userSession()->getUserIDForVisitorContactID($this->getDataValue('contact-id')))) { + if (DI::userSession()->getUserIDForVisitorContactID($this->getDataValue('contact-id')) !== 0) { $this->visiting = true; } $this->writable = $this->getDataValue('writable') || $this->getDataValue('self'); - $author = [ + $author = [ 'uid' => 0, 'id' => $this->getDataValue('author-id'), 'network' => $this->getDataValue('author-network'), @@ -112,13 +97,13 @@ class Post continue; } - // You can always comment on Diaspora and OStatus items - if (in_array($item['network'], [Protocol::OSTATUS, Protocol::DIASPORA]) && (DI::userSession()->getLocalUserId() == $item['uid'])) { + // You can always comment on Diaspora items + if (in_array($item['network'], [Protocol::DIASPORA]) && (DI::userSession()->getLocalUserId() == $item['uid'])) { $item['writable'] = true; } $item['pagedrop'] = $data['pagedrop']; - $child = new Post($item); + $child = new Post($item); $this->addChild($child); } } @@ -167,7 +152,7 @@ class Post */ public function getTemplateData(array $conv_responses, string $formSecurityToken, int $thread_level = 1, array $thread_parent = []) { - $item = $this->getData(); + $item = $this->getData(); $edited = false; /* @@ -191,33 +176,46 @@ class Post 'share' => null, 'announce' => null, ]; - $dropping = false; - $pinned = ''; - $pin = false; - $star = false; - $ignore_thread = false; - $ispinned = 'unpinned'; - $isstarred = 'unstarred'; - $indent = ''; - $shiny = ''; - $osparkle = ''; - $total_children = $this->countDescendants(); + $dropping = false; + $pinned = ''; + $pin = false; + $star = false; + $ignore_thread = false; + $ispinned = 'unpinned'; + $isstarred = 'unstarred'; + $indent = ''; + $shiny = ''; + $osparkle = ''; + $total_children = $item['counts'] ?? $this->countDescendants(); $conv = $this->getThread(); $privacy = $this->fetchPrivacy($item); $lock = ($item['private'] == Item::PRIVATE) ? $privacy : false; - $connector = !in_array($item['network'], Protocol::NATIVE_SUPPORT) ? DI::l10n()->t('Connector Message') : false; + $connector = !in_array($item['network'], Protocol::NATIVE_SUPPORT) && ($item['protocol'] != Conversation::PARCEL_JETSTREAM) ? DI::l10n()->t('Connector Message') : false; $shareable = in_array($conv->getProfileOwner(), [0, DI::userSession()->getLocalUserId()]) && $item['private'] != Item::PRIVATE; $announceable = $shareable && in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::TWITTER, Protocol::TUMBLR, Protocol::BLUESKY]); $commentable = ($item['network'] != Protocol::TUMBLR); + $likeable = true; // On Diaspora only toplevel posts can be reshared if ($announceable && ($item['network'] == Protocol::DIASPORA) && ($item['gravity'] != Item::GRAVITY_PARENT)) { $announceable = false; } + if ($item['restrictions'] & Item::CANT_REPLY) { + $commentable = false; + } + + if ($item['restrictions'] & Item::CANT_LIKE) { + $likeable = false; + } + + if ($item['restrictions'] & Item::CANT_ANNOUNCE) { + $announceable = false; + } + $edpost = false; if (DI::userSession()->getLocalUserId()) { @@ -258,8 +256,8 @@ class Post $drop = [ 'dropping' => $dropping, 'pagedrop' => $item['pagedrop'], - 'select' => DI::l10n()->t('Select'), - 'label' => $origin ? DI::l10n()->t('Delete globally') : DI::l10n()->t('Remove locally'), + 'select' => DI::l10n()->t('Select'), + 'label' => $origin ? DI::l10n()->t('Delete globally') : DI::l10n()->t('Remove locally'), ]; } @@ -315,15 +313,21 @@ class Post $sparkle = ' sparkle'; } + $eventDispatcher = DI::eventDispatcher(); + $locate = ['location' => $item['location'], 'coord' => $item['coord'], 'html' => '']; - Hook::callAll('render_location', $locate); + + $locate = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::RENDER_LOCATION, $locate), + )->getArray(); + $location_html = $locate['html'] ?: Strings::escapeHtml($locate['location'] ?: $locate['coord'] ?: ''); // process action responses - e.g. like/dislike/attend/agree/whatever $response_verbs = ['like', 'dislike', 'announce', 'comment']; $isevent = false; - $attend = []; + $attend = []; if ($item['object-type'] === Activity\ObjectType::EVENT) { $response_verbs[] = 'attendyes'; $response_verbs[] = 'attendno'; @@ -331,7 +335,7 @@ class Post if ($conv->isWritable()) { $isevent = true; - $attend = [DI::l10n()->t('I will attend'), DI::l10n()->t('I will not attend'), DI::l10n()->t('I might attend')]; + $attend = [DI::l10n()->t('I will attend'), DI::l10n()->t('I will not attend'), DI::l10n()->t('I might attend')]; } } @@ -408,7 +412,7 @@ class Post 'toggle' => DI::l10n()->t('Toggle pin status'), 'classdo' => $item['featured'] ? 'hidden' : '', 'classundo' => $item['featured'] ? '' : 'hidden', - 'pinned' => DI::l10n()->t('Pinned'), + 'pinned' => DI::l10n()->t('Pinned'), ]; } @@ -423,13 +427,15 @@ class Post } if ($conv->isWritable()) { - $buttons['like'] = [DI::l10n()->t("I like this \x28toggle\x29"), DI::l10n()->t('Like')]; - $buttons['dislike'] = [DI::l10n()->t("I don't like this \x28toggle\x29"), DI::l10n()->t('Dislike')]; + if ($likeable) { + $buttons['like'] = [DI::l10n()->t("I like this \x28toggle\x29"), DI::l10n()->t('Like')]; + $buttons['dislike'] = [DI::l10n()->t("I don't like this \x28toggle\x29"), DI::l10n()->t('Dislike')]; + } if ($shareable) { $buttons['share'] = [DI::l10n()->t('Quote share this'), DI::l10n()->t('Quote Share')]; } if ($announceable) { - $buttons['announce'] = [DI::l10n()->t('Reshare this'), DI::l10n()->t('Reshare')]; + $buttons['announce'] = [DI::l10n()->t('Reshare this'), DI::l10n()->t('Reshare')]; $buttons['unannounce'] = [DI::l10n()->t('Cancel your Reshare'), DI::l10n()->t('Unshare')]; } } @@ -450,14 +456,6 @@ class Post list($categories, $folders) = DI::contentItem()->determineCategoriesTerms($item, DI::userSession()->getLocalUserId()); - if (!empty($item['title'])) { - $title = $item['title']; - } elseif (!empty($item['content-warning']) && DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'disable_cw', false)) { - $title = ucfirst($item['content-warning']); - } else { - $title = ''; - } - $hide_dislike = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'hide_dislike'); if ($hide_dislike) { $buttons['dislike'] = false; @@ -470,7 +468,7 @@ class Post } $isevent = false; - $tagger = ''; + $tagger = ''; } if ($buttons['like'] && in_array($item['network'], [Protocol::FEED, Protocol::MAIL])) { @@ -479,7 +477,7 @@ class Post $tags = Tag::populateFromItem($item); - $ago = Temporal::getRelativeDate($item['created']); + $ago = Temporal::getRelativeDate($item['created']); $ago_received = Temporal::getRelativeDate($item['received']); if (DI::config()->get('system', 'show_received') && (abs(strtotime($item['created']) - strtotime($item['received'])) > DI::config()->get('system', 'show_received_seconds')) && ($ago != $ago_received)) { $ago = DI::l10n()->t('%s (Received %s)', $ago, $ago_received); @@ -493,7 +491,7 @@ class Post ]; // Ensure to either display the remote comment or the local activities - $buttons = []; + $buttons = []; $comment_html = ''; } else { $remote_comment = ''; @@ -505,8 +503,10 @@ class Post } $languages = []; + $language = ''; if (!empty($item['language'])) { - $languages = [DI::l10n()->t('Languages'), Item::getLanguageMessage($item)]; + $languages = DI::l10n()->t('Languages'); + $language = array_key_first(json_decode($item['language'], true)); } if (in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]) && in_array($item['network'], Protocol::FEDERATED)) { @@ -520,107 +520,111 @@ class Post $parent_unknown = $parent_username ? '' : DI::l10n()->t('Unknown parent'); $tmp_item = [ - 'parentguid' => $parent_guid, - 'inreplyto' => DI::l10n()->t('in reply to %s', $parent_username), - 'isunknown' => $parent_unknown, - 'isunknown_label' => DI::l10n()->t('Parent is probably private or not federated.'), - 'template' => $this->getTemplate(), - 'type' => implode('', array_slice(explode('/', $item['verb']), -1)), + 'parentguid' => $parent_guid, + 'inreplyto' => DI::l10n()->t('in reply to %s', $parent_username), + 'isunknown' => $parent_unknown, + 'isunknown_label' => DI::l10n()->t('Parent is probably private or not federated.'), + 'template' => $this->getTemplate(), + 'type' => implode('', array_slice(explode('/', $item['verb']), -1)), 'comment_firstcollapsed' => false, - 'comment_lastcollapsed' => false, - 'suppress_tags' => DI::config()->get('system', 'suppress_tags'), - 'tags' => $tags['tags'], - 'hashtags' => $tags['hashtags'], - 'mentions' => $tags['mentions'], - 'implicit_mentions' => $tags['implicit_mentions'], - 'txt_cats' => DI::l10n()->t('Categories:'), - 'txt_folders' => DI::l10n()->t('Filed under:'), - 'has_cats' => ((count($categories)) ? 'true' : ''), - 'has_folders' => ((count($folders)) ? 'true' : ''), - 'categories' => $categories, - 'folders' => $folders, - 'body_html' => $body_html, - 'text' => strip_tags($body_html), - 'id' => $this->getId(), - 'guid' => urlencode($item['guid']), - 'isevent' => $isevent, - 'attend' => $attend, - 'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']), - 'olinktitle' => DI::l10n()->t('View %s\'s profile @ %s', $this->getOwnerName(), $item['owner-link']), - 'to' => DI::l10n()->t('to'), - 'via' => DI::l10n()->t('via'), - 'wall' => DI::l10n()->t('Wall-to-Wall'), - 'vwall' => DI::l10n()->t('via Wall-To-Wall:'), - 'profile_url' => $profile_link, - 'name' => $profile_name, - 'item_photo_menu_html' => DI::contentItem()->photoMenu($item, $formSecurityToken), - 'thumb' => DI::baseUrl()->remove(DI::contentItem()->getAuthorAvatar($item)), - 'osparkle' => $osparkle, - 'sparkle' => $sparkle, - 'title' => $title, - 'localtime' => DateTimeFormat::local($item['created'], 'r'), - 'ago' => $item['app'] ? DI::l10n()->t('%s from %s', $ago, $item['app']) : $ago, - 'app' => $item['app'], - 'created' => $ago, - 'lock' => $lock, - 'private' => $item['private'], - 'privacy' => $privacy, - 'connector' => $connector, - 'location_html' => $location_html, - 'indent' => $indent, - 'shiny' => $shiny, - 'owner_self' => $item['author-link'] == DI::session()->get('my_url'), - 'owner_url' => $this->getOwnerUrl(), - 'owner_photo' => DI::baseUrl()->remove(DI::contentItem()->getOwnerAvatar($item)), - 'owner_name' => $this->getOwnerName(), - 'plink' => Item::getPlink($item), - 'browsershare' => $browsershare, - 'edpost' => $edpost, - 'ispinned' => $ispinned, - 'pin' => $pin, - 'pinned' => $pinned, - 'isstarred' => $isstarred, - 'star' => $star, - 'ignore' => $ignore_thread, - 'tagger' => $tagger, - 'filer' => $filer, - 'language' => $languages, - 'drop' => $drop, - 'block' => $block, - 'ignore_author' => $ignore, - 'collapse' => $collapse, - 'report' => $report, - 'ignore_server' => $ignoreServer, - 'vote' => $buttons, - 'like_html' => $responses['like']['output'], - 'dislike_html' => $responses['dislike']['output'], - 'hide_dislike' => $hide_dislike, - 'emojis' => $emojis, - 'quoteshares' => $this->getQuoteShares($item['quoteshares']), - 'reactions' => $reactions, - 'responses' => $responses, - 'legacy_activities' => DI::config()->get('system', 'legacy_activities'), - 'switchcomment' => DI::l10n()->t('Comment'), - 'reply_label' => DI::l10n()->t('Reply to %s', $profile_name), - 'comment_html' => $comment_html, - 'remote_comment' => $remote_comment, - 'menu' => DI::l10n()->t('More'), - 'previewing' => $conv->isPreview() ? ' preview ' : '', - 'wait' => DI::l10n()->t('Please wait'), - 'thread_level' => $thread_level, - 'edited' => $edited, - 'author_gsid' => $item['author-gsid'], - 'network' => $item['network'], - 'network_name' => ContactSelector::networkToName($item['author-network'], $item['author-link'], $item['network'], $item['author-gsid']), - 'network_icon' => ContactSelector::networkToIcon($item['network'], $item['author-link'], $item['author-gsid']), - 'received' => $item['received'], - 'commented' => $item['commented'], - 'created_date' => $item['created'], - 'uriid' => $item['uri-id'], - 'return' => (DI::args()->getCommand()) ? bin2hex(DI::args()->getCommand()) : '', - 'direction' => $direction, - 'reshared' => $item['reshared'] ?? '', - 'delivery' => [ + 'comment_lastcollapsed' => false, + 'suppress_tags' => DI::config()->get('system', 'suppress_tags'), + 'tags' => $tags['tags'], + 'hashtags' => $tags['hashtags'], + 'mentions' => $tags['mentions'], + 'implicit_mentions' => $tags['implicit_mentions'], + 'txt_cats' => DI::l10n()->t('Categories:'), + 'txt_folders' => DI::l10n()->t('Filed under:'), + 'has_cats' => ((count($categories)) ? 'true' : ''), + 'has_folders' => ((count($folders)) ? 'true' : ''), + 'categories' => $categories, + 'folders' => $folders, + 'body_html' => $body_html, + 'text' => strip_tags($body_html), + 'id' => $this->getId(), + 'guid' => urlencode($item['guid']), + 'isevent' => $isevent, + 'attend' => $attend, + 'linktitle' => DI::l10n()->t('View %s\'s profile @ %s', $profile_name, $item['author-link']), + 'olinktitle' => DI::l10n()->t('View %s\'s profile @ %s', $this->getOwnerName(), $item['owner-link']), + 'to' => DI::l10n()->t('to'), + 'via' => DI::l10n()->t('via'), + 'wall' => DI::l10n()->t('Wall-to-Wall'), + 'vwall' => DI::l10n()->t('via Wall-To-Wall:'), + 'profile_url' => $profile_link, + 'name' => $profile_name, + 'item_photo_menu_html' => DI::contentItem()->photoMenu($item, $formSecurityToken), + 'thumb' => DI::baseUrl()->remove(DI::contentItem()->getAuthorAvatar($item)), + 'osparkle' => $osparkle, + 'sparkle' => $sparkle, + 'title' => $item['title'], + 'summary' => $item['content-warning'], + 'localtime' => DateTimeFormat::local($item['created'], 'r'), + 'utc' => DateTimeFormat::utc($item['created']), + 'ago' => $item['app'] ? DI::l10n()->t('%s from %s', $ago, $item['app']) : $ago, + 'app' => $item['app'], + 'created' => $ago, + 'lock' => $lock, + 'private' => $item['private'], + 'privacy' => $privacy, + 'connector' => $connector, + 'location_html' => $location_html, + 'indent' => $indent, + 'shiny' => $shiny, + 'owner_self' => $item['author-link'] == DI::session()->get('my_url'), + 'owner_url' => $this->getOwnerUrl(), + 'owner_photo' => DI::baseUrl()->remove(DI::contentItem()->getOwnerAvatar($item)), + 'owner_name' => $this->getOwnerName(), + 'plink' => Item::getPlink($item), + 'browsershare' => $browsershare, + 'edpost' => $edpost, + 'ispinned' => $ispinned, + 'pin' => $pin, + 'pinned' => $pinned, + 'isstarred' => $isstarred, + 'star' => $star, + 'ignore' => $ignore_thread, + 'tagger' => $tagger, + 'filer' => $filer, + 'language' => $languages, + 'lang' => $language, + 'searchtext' => DI::l10n()->t('Search Text'), + 'drop' => $drop, + 'block' => $block, + 'ignore_author' => $ignore, + 'collapse' => $collapse, + 'report' => $report, + 'ignore_server' => $ignoreServer, + 'vote' => $buttons, + 'like_html' => $responses['like']['output'], + 'dislike_html' => $responses['dislike']['output'], + 'hide_dislike' => $hide_dislike, + 'emojis' => $emojis, + 'quoteshares' => $this->getQuoteShares($item['quoteshares']), + 'reactions' => $reactions, + 'responses' => $responses, + 'legacy_activities' => DI::config()->get('system', 'legacy_activities'), + 'switchcomment' => DI::l10n()->t('Comment'), + 'reply_label' => DI::l10n()->t('Reply to %s', $profile_name), + 'comment_html' => $comment_html, + 'remote_comment' => $remote_comment, + 'menu' => DI::l10n()->t('More'), + 'previewing' => $conv->isPreview() ? ' preview ' : '', + 'wait' => DI::l10n()->t('Please wait'), + 'thread_level' => $thread_level, + 'edited' => $edited, + 'author_gsid' => $item['author-gsid'], + 'network' => $item['network'], + 'network_name' => ContactSelector::networkToName($item['author-network'], $item['network'], $item['author-gsid']), + 'network_svg' => ContactSelector::networkToSVG($item['network'], $item['author-gsid'], '', DI::userSession()->getLocalUserId()), + 'received' => $item['received'], + 'commented' => $item['commented'], + 'created_date' => $item['created'], + 'uriid' => $item['uri-id'], + 'return' => (DI::args()->getCommand()) ? bin2hex(DI::args()->getCommand()) : '', + 'direction' => $direction, + 'reshared' => $item['reshared'] ?? '', + 'delivery' => [ 'queue_count' => $item['delivery_queue_count'], 'queue_done' => $item['delivery_queue_done'] + $item['delivery_queue_failed'], /// @todo Possibly display it separately in the future 'notifier_pending' => DI::l10n()->t('Notifier task is pending'), @@ -632,26 +636,29 @@ class Post ]; $arr = ['item' => $item, 'output' => $tmp_item]; - Hook::callAll('display_item', $arr); + + $arr = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::DISPLAY_ITEM, $arr), + )->getArray(); $result = $arr['output']; $result['children'] = []; - $children = $this->getChildren(); - $nb_children = count($children); + $children = $this->getChildren(); + $nb_children = count($children); if ($nb_children > 0) { $thread_parent[$item['uri-id']] = ['guid' => $item['guid'], 'name' => $item['author-name']]; foreach ($children as $child) { $thread_parent[$child->getDataValue('uri-id')] = ['guid' => $child->getDataValue('guid'), 'name' => $child->getDataValue('author-name')]; - $result['children'][] = $child->getTemplateData($conv_responses, $formSecurityToken, $thread_level + 1, $thread_parent); + $result['children'][] = $child->getTemplateData($conv_responses, $formSecurityToken, $thread_level + 1, $thread_parent); } // Collapse if (($nb_children > 2) || ($thread_level > 1)) { $result['children'][0]['comment_firstcollapsed'] = true; - $result['children'][0]['num_comments'] = DI::l10n()->tt('%d comment', '%d comments', $total_children); - $result['children'][0]['show_text'] = DI::l10n()->t('Show more'); - $result['children'][0]['hide_text'] = DI::l10n()->t('Show fewer'); + $result['children'][0]['num_comments'] = DI::l10n()->tt('%d comment', '%d comments', $total_children); + $result['children'][0]['show_text'] = DI::l10n()->t('Show more'); + $result['children'][0]['hide_text'] = DI::l10n()->t('Show fewer'); if ($thread_level > 1) { $result['children'][$nb_children - 1]['comment_lastcollapsed'] = true; } else { @@ -697,6 +704,11 @@ class Post $icon = ['fa' => 'fa-eye', 'icon' => 'icon-eye-open']; break; + case Activity::READ: + $title = DI::l10n()->t('Read by: %s', $actors); + $icon = ['fa' => 'fa-book', 'icon' => 'icon-book']; + break; + case Activity::LIKE: $title = DI::l10n()->t('Liked by: %s', $actors); $icon = ['fa' => 'fa-thumbs-up', 'icon' => 'icon-thumbs-up']; @@ -726,7 +738,7 @@ class Post $title = DI::l10n()->t('Commented by: %s', $actors); $icon = ['fa' => 'fa-commenting', 'icon' => 'icon-commenting']; break; - + default: $title = DI::l10n()->t('Reacted with %s by: %s', $element['emoji'], $actors); $icon = []; @@ -781,10 +793,10 @@ class Post public function addChild(Post $item) { if (!$item->getId()) { - Logger::error('Post object has no id', ['post' => $item]); + DI::logger()->error('Post object has no id', ['post' => $item]); return false; } elseif ($this->getChild($item->getId())) { - Logger::warning('Post object already exists', ['post' => $item]); + DI::logger()->warning('Post object already exists', ['post' => $item]); return false; } @@ -792,13 +804,13 @@ class Post * Only add what will be displayed */ if ($item->getDataValue('network') === Protocol::MAIL && DI::userSession()->getLocalUserId() != $item->getDataValue('uid')) { - Logger::warning('Post object does not belong to local user', ['post' => $item, 'local_user' => DI::userSession()->getLocalUserId()]); + DI::logger()->warning('Post object does not belong to local user', ['post' => $item, 'local_user' => DI::userSession()->getLocalUserId()]); return false; } elseif ( DI::activity()->match($item->getDataValue('verb'), Activity::LIKE) || DI::activity()->match($item->getDataValue('verb'), Activity::DISLIKE) ) { - Logger::warning('Post objects is a like/dislike', ['post' => $item]); + DI::logger()->warning('Post objects is a like/dislike', ['post' => $item]); return false; } @@ -812,7 +824,7 @@ class Post * Get a child by its ID * * @param integer $id The child id - * @return Thread|null Thread or NULL if not found + * @return Post|null Post or NULL if not found */ public function getChild(int $id) { @@ -883,7 +895,7 @@ class Post } } - Logger::info('[WARN] Item::removeChild : Item is not a child (' . $id . ').'); + DI::logger()->info('[WARN] Item::removeChild : Item is not a child (' . $id . ').'); return false; } @@ -946,7 +958,7 @@ class Post public function getDataValue(string $name) { if (!isset($this->data[$name])) { - // Logger::info('[ERROR] Item::getDataValue : Item has no value name "'. $name .'".'); + // DI::logger()->info('[ERROR] Item::getDataValue : Item has no value name "'. $name .'".'); return false; } @@ -1022,7 +1034,7 @@ class Post private function countDescendants(): int { $children = $this->getChildren(); - $total = count($children); + $total = count($children); if ($total > 0) { foreach ($children as $child) { $total += $child->countDescendants(); @@ -1050,22 +1062,20 @@ class Post */ private function getDefaultText(): string { - $a = DI::app(); - if (!DI::userSession()->getLocalUserId()) { return ''; } - $owner = User::getOwnerDataById($a->getLoggedInUserId()); - $item = $this->getData(); + $owner = User::getOwnerDataById(DI::userSession()->getLocalUserId()); + $item = $this->getData(); - if (!empty($item['content-warning']) && Feature::isEnabled(DI::userSession()->getLocalUserId(), 'add_abstract')) { + if (!empty($item['content-warning']) && Feature::isEnabled(DI::userSession()->getLocalUserId(), Feature::ADD_ABSTRACT)) { $text = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $item['content-warning'] . "[/abstract]\n"; } else { $text = ''; } - if (!Feature::isEnabled(DI::userSession()->getLocalUserId(), 'explicit_mentions')) { + if (!Feature::isEnabled(DI::userSession()->getLocalUserId(), Feature::EXPLICIT_MENTIONS)) { return $text; } @@ -1103,27 +1113,27 @@ class Post */ private function getCommentBox(string $indent) { - $a = DI::app(); - $comment_box = ''; - $conv = $this->getThread(); + $conv = $this->getThread(); if ($conv->isWritable() && $this->isWritable()) { + $addonHelper = DI::addonHelper(); + /* * Hmmm, code depending on the presence of a particular addon? * This should be better if done by a hook */ $qcomment = null; - if (Addon::isEnabled('qcomment')) { - $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); + if ($addonHelper->isAddonEnabled('qcomment')) { + $words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words'); $qcomment = $words ? explode("\n", $words) : []; } // Fetch the user id from the parent when the owner user is empty - $uid = $conv->getProfileOwner(); + $uid = $conv->getProfileOwner(); $parent_uid = $this->getDataValue('uid'); - $owner = User::getOwnerDataById($a->getLoggedInUserId()); + $owner = User::getOwnerDataById(DI::userSession()->getLocalUserId()); $default_text = $this->getDefaultText(); @@ -1131,7 +1141,7 @@ class Post $uid = $parent_uid; } - $template = Renderer::getMarkupTemplate($this->getCommentBoxTemplate()); + $template = Renderer::getMarkupTemplate($this->getCommentBoxTemplate()); $comment_box = Renderer::replaceMacros($template, [ '$return_path' => DI::args()->getQueryString(), '$threaded' => $this->isThreaded(), @@ -1184,8 +1194,7 @@ class Post */ protected function checkWallToWall() { - $a = DI::app(); - $conv = $this->getThread(); + $conv = $this->getThread(); $this->wall_to_wall = false; if ($this->isToplevel()) { @@ -1205,7 +1214,7 @@ class Post * well that it's the same Bob Smith. * But it could be somebody else with the same name. It just isn't highly likely. */ - $this->owner_name = $this->getDataValue('owner-name'); + $this->owner_name = $this->getDataValue('owner-name'); $this->wall_to_wall = true; $owner = [ @@ -1223,7 +1232,7 @@ class Post if (!$this->wall_to_wall) { $this->setTemplate('wall'); - $this->owner_url = ''; + $this->owner_url = ''; $this->owner_name = ''; } } diff --git a/src/Object/Search/ContactResult.php b/src/Object/Search/ContactResult.php index c46b9b6e4c..c45cf9120b 100644 --- a/src/Object/Search/ContactResult.php +++ b/src/Object/Search/ContactResult.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Search; diff --git a/src/Object/Search/IResult.php b/src/Object/Search/IResult.php index 240510a836..af503c741c 100644 --- a/src/Object/Search/IResult.php +++ b/src/Object/Search/IResult.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Search; diff --git a/src/Object/Search/ResultList.php b/src/Object/Search/ResultList.php index b2186de599..fd1ac2841d 100644 --- a/src/Object/Search/ResultList.php +++ b/src/Object/Search/ResultList.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object\Search; diff --git a/src/Object/Thread.php b/src/Object/Thread.php index 5c825dfc7e..070d8c1281 100644 --- a/src/Object/Thread.php +++ b/src/Object/Thread.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Object; use Friendica\Content\Conversation; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\DI; use Friendica\Protocol\Activity; @@ -36,11 +21,11 @@ use Friendica\Security\Security; class Thread { /** @var Post[] */ - private $parents = []; - private $mode = null; - private $writable = false; + private $parents = []; + private $mode = null; + private $writable = false; private $profile_owner = 0; - private $preview = false; + private $preview = false; /** * Constructor @@ -71,29 +56,28 @@ class Thread return; } - $a = DI::app(); + $appHelper = DI::appHelper(); switch ($mode) { case Conversation::MODE_NETWORK: case Conversation::MODE_NOTES: $this->profile_owner = DI::userSession()->getLocalUserId(); - $this->writable = true; + $this->writable = true; break; case Conversation::MODE_PROFILE: case Conversation::MODE_DISPLAY: - $this->profile_owner = $a->getProfileOwner(); - $this->writable = Security::canWriteToUserWall($this->profile_owner) || $writable; + $this->profile_owner = $appHelper->getProfileOwner(); + $this->writable = Security::canWriteToUserWall($this->profile_owner) || $writable; break; case Conversation::MODE_CHANNEL: case Conversation::MODE_COMMUNITY: case Conversation::MODE_CONTACTS: $this->profile_owner = 0; - $this->writable = $writable; + $this->writable = $writable; break; default: - Logger::info('[ERROR] Conversation::setMode : Unhandled mode ('. $mode .').'); - return false; - break; + DI::logger()->info('[ERROR] Conversation::setMode : Unhandled mode ('. $mode .').'); + return; } $this->mode = $mode; } @@ -152,12 +136,12 @@ class Thread $item_id = $item->getId(); if (!$item_id) { - Logger::info('[ERROR] Conversation::addThread : Item has no ID!!'); + DI::logger()->info('[ERROR] Conversation::addThread : Item has no ID!!'); return false; } if ($this->getParent($item->getId())) { - Logger::info('[WARN] Conversation::addThread : Thread already exists ('. $item->getId() .').'); + DI::logger()->info('[WARN] Conversation::addThread : Thread already exists ('. $item->getId() .').'); return false; } @@ -165,12 +149,12 @@ class Thread * Only add will be displayed */ if ($item->getDataValue('network') === Protocol::MAIL && DI::userSession()->getLocalUserId() != $item->getDataValue('uid')) { - Logger::info('[WARN] Conversation::addThread : Thread is a mail ('. $item->getId() .').'); + DI::logger()->info('[WARN] Conversation::addThread : Thread is a mail ('. $item->getId() .').'); return false; } if ($item->getDataValue('verb') === Activity::LIKE || $item->getDataValue('verb') === Activity::DISLIKE) { - Logger::info('[WARN] Conversation::addThread : Thread is a (dis)like ('. $item->getId() .').'); + DI::logger()->info('[WARN] Conversation::addThread : Thread is a (dis)like ('. $item->getId() .').'); return false; } @@ -204,7 +188,7 @@ class Thread $item_data = $item->getTemplateData($conv_responses, $formSecurityToken); if (!$item_data) { - Logger::info('[ERROR] Conversation::getTemplateData : Failed to get item template data ('. $item->getId() .').'); + DI::logger()->info('[ERROR] Conversation::getTemplateData : Failed to get item template data ('. $item->getId() .').'); return false; } $result[] = $item_data; diff --git a/src/Privacy/Entity/AclReceivers.php b/src/Privacy/Entity/AclReceivers.php new file mode 100644 index 0000000000..22b78fa2b7 --- /dev/null +++ b/src/Privacy/Entity/AclReceivers.php @@ -0,0 +1,31 @@ +allowContacts = $allowContacts; + $this->allowCircles = $allowCircles; + $this->denyContacts = $denyContacts; + $this->denyCircles = $denyCircles; + } + + public function isEmpty(): bool + { + return empty($this->allowContacts) && empty($this->allowCircles) && empty($this->denyContacts) && empty($this->denyCircles); + } +} diff --git a/src/Privacy/Entity/AddressedReceivers.php b/src/Privacy/Entity/AddressedReceivers.php new file mode 100644 index 0000000000..b91ff429d5 --- /dev/null +++ b/src/Privacy/Entity/AddressedReceivers.php @@ -0,0 +1,33 @@ +to = $to; + $this->cc = $cc; + $this->bcc = $bcc; + $this->audience = $audience; + $this->attributed = $attributed; + } + + public function isEmpty(): bool + { + return empty($this->to) && empty($this->cc) && empty($this->bcc) && empty($this->audience) && empty($this->attributed); + } +} diff --git a/src/Profile/ProfileField/Collection/ProfileFields.php b/src/Profile/ProfileField/Collection/ProfileFields.php index c8053cc7d3..44f13673d7 100644 --- a/src/Profile/ProfileField/Collection/ProfileFields.php +++ b/src/Profile/ProfileField/Collection/ProfileFields.php @@ -1,52 +1,40 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Collection; use Friendica\BaseCollection; -use Friendica\Profile\ProfileField\Entity; +use Friendica\Profile\ProfileField\Entity\ProfileField as ProfileFieldEntity; class ProfileFields extends BaseCollection { - public function current(): Entity\ProfileField + public function current(): ProfileFieldEntity { return parent::current(); } - /** - * @param callable $callback - * @return ProfileFields (as an extended form of BaseCollection) - */ - public function map(callable $callback): BaseCollection + public function map(callable $callback): ProfileFields { - return parent::map($callback); + $profileFields = parent::map($callback); + + if (!$profileFields instanceof ProfileFields) { + // Show the possible error explicitly + throw new \Exception(sprintf( + 'BaseCollection::map() should return instance of %s, but returns %s instead.', + ProfileFields::class, + get_class($profileFields), + )); + } + + return $profileFields; } - /** - * @param callable|null $callback - * @param int $flag - * @return ProfileFields as an extended version of BaseCollection - */ - public function filter(callable $callback = null, int $flag = 0): BaseCollection + public function filter(?callable $callback = null, int $flag = 0): ProfileFields { - return parent::filter($callback, $flag); + return new self(array_filter($this->getArrayCopy(), $callback, $flag)); } } diff --git a/src/Profile/ProfileField/Entity/ProfileField.php b/src/Profile/ProfileField/Entity/ProfileField.php index 2461e02bd7..40bb499620 100644 --- a/src/Profile/ProfileField/Entity/ProfileField.php +++ b/src/Profile/ProfileField/Entity/ProfileField.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Entity; diff --git a/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php b/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php index 9b16825937..5a3fc356df 100644 --- a/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php +++ b/src/Profile/ProfileField/Exception/ProfileFieldNotFoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Exception; diff --git a/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php b/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php index 5cb129c2d4..481c2250cd 100644 --- a/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php +++ b/src/Profile/ProfileField/Exception/ProfileFieldPersistenceException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Exception; diff --git a/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php b/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php index dbf5412f1c..6bf8bada6a 100644 --- a/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php +++ b/src/Profile/ProfileField/Exception/UnexpectedPermissionSetException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Exception; diff --git a/src/Profile/ProfileField/Factory/ProfileField.php b/src/Profile/ProfileField/Factory/ProfileField.php index 3c8e4673ea..34ab2dfefb 100644 --- a/src/Profile/ProfileField/Factory/ProfileField.php +++ b/src/Profile/ProfileField/Factory/ProfileField.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Factory; diff --git a/src/Profile/ProfileField/Repository/ProfileField.php b/src/Profile/ProfileField/Repository/ProfileField.php index d297ca2377..5a46cfb6bc 100644 --- a/src/Profile/ProfileField/Repository/ProfileField.php +++ b/src/Profile/ProfileField/Repository/ProfileField.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Profile\ProfileField\Repository; diff --git a/src/Protocol/ATProtocol.php b/src/Protocol/ATProtocol.php new file mode 100644 index 0000000000..cc17d9e93e --- /dev/null +++ b/src/Protocol/ATProtocol.php @@ -0,0 +1,570 @@ +logger = $logger; + $this->db = $database; + $this->config = $config; + $this->pConfig = $pConfig; + $this->httpClient = $httpClient; + } + + /** + * Returns an array of user ids who want to import the Bluesky timeline + * + * @return array user ids + */ + public function getUids(): array + { + $uids = []; + $abandon_days = intval($this->config->get('system', 'account_abandon_days')); + if ($abandon_days < 1) { + $abandon_days = 0; + } + + $abandon_limit = date(DateTimeFormat::MYSQL, time() - $abandon_days * 86400); + + $pconfigs = $this->db->selectToArray('pconfig', [], ["`cat` = ? AND `k` = ? AND `v`", 'bluesky', 'import']); + foreach ($pconfigs as $pconfig) { + if (empty($this->getUserDid($pconfig['uid']))) { + continue; + } + + if ($abandon_days != 0) { + if (!$this->db->exists('user', ["`uid` = ? AND `login_date` >= ?", $pconfig['uid'], $abandon_limit])) { + continue; + } + } + $uids[] = $pconfig['uid']; + } + return $uids; + } + + /** + * Fetches XRPC data + * @see https://atproto.com/specs/xrpc#lexicon-http-endpoints + * + * @param string $url for example "app.bsky.feed.getTimeline" + * @param array $parameters Array with parameters + * @param integer $uid User ID + * @return stdClass|null Fetched data + */ + public function XRPCGet(string $url, array $parameters = [], int $uid = 0): ?stdClass + { + if (!empty($parameters)) { + $url .= '?' . http_build_query($parameters); + } + + if ($uid == 0) { + return $this->get(ATProtocol::APPVIEW_API . '/xrpc/' . $url); + } + + $pds = $this->getUserPds($uid); + if (empty($pds)) { + return null; + } + + $headers = ['Authorization' => ['Bearer ' . $this->getUserToken($uid)]]; + + $languages = User::getWantedLanguages($uid); + if (!empty($languages)) { + $headers['Accept-Language'] = implode(',', $languages); + } + + $data = $this->get($pds . '/xrpc/' . $url, [HttpClientOptions::HEADERS => $headers]); + + if ($data === null) { + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL); + + return null; + } + + if (!empty($data->code) && ($data->code < 200 || $data->code >= 400)) { + if (!empty($data->message)) { + $this->pConfig->set($uid, 'bluesky', 'status-message', $data->message); + } elseif (!empty($data->code)) { + $this->pConfig->set($uid, 'bluesky', 'status-message', 'Error Code: ' . $data->code); + } + + return $data; + } + + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_SUCCESS); + $this->pConfig->set($uid, 'bluesky', 'status-message', ''); + + return $data; + } + + /** + * Fetch data from the given URL via GET and return it as a JSON class + * + * @param string $url HTTP URL + * @param array $opts HTTP options + * @return stdClass|null Fetched data + */ + public function get(string $url, array $opts = []): ?stdClass + { + try { + $curlResult = $this->httpClient->get($url, HttpClientAccept::JSON, $opts); + } catch (\Exception $e) { + $this->logger->notice('Exception on get', ['url' => $url, 'exception' => $e]); + return null; + } + + $data = json_decode($curlResult->getBodyString()); + if (!$curlResult->isSuccess()) { + $this->logger->notice('API Error', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]); + if (!$data) { + return null; + } + $data->code = $curlResult->getReturnCode(); + } elseif (($curlResult->getReturnCode() < 200) || ($curlResult->getReturnCode() >= 400)) { + $this->logger->notice('Unexpected return code', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]); + $data->code = $curlResult->getReturnCode(); + } + + Item::incrementInbound(Protocol::BLUESKY); + return $data; + } + + /** + * Perform an XRPC post for a given user + * @see https://atproto.com/specs/xrpc#lexicon-http-endpoints + * + * @param int $uid User ID + * @param string $url Endpoints like "com.atproto.repo.createRecord" + * @param array|stdClass $parameters array or StdClass with parameters + */ + public function XRPCPost(int $uid, string $url, $parameters): ?stdClass + { + $data = $this->post($uid, '/xrpc/' . $url, json_encode($parameters), ['Content-type' => 'application/json', 'Authorization' => ['Bearer ' . $this->getUserToken($uid)]]); + return $data; + } + + /** + * Post data to the user PDS + * + * @param integer $uid User ID + * @param string $url HTTP URL without the hostname + * @param string $params Parameter string + * @param array $headers HTTP header information + * @return stdClass|null + */ + public function post(int $uid, string $url, string $params, array $headers): ?stdClass + { + $pds = $this->getUserPds($uid); + if (empty($pds)) { + return null; + } + + try { + $curlResult = $this->httpClient->post($pds . $url, $params, $headers); + } catch (\Exception $e) { + $this->logger->notice('Exception on post', ['exception' => $e]); + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL); + $this->pConfig->set($uid, 'bluesky', 'status-message', $e->getMessage()); + return null; + } + + $data = json_decode($curlResult->getBodyString(), false); + + if (!$curlResult->isSuccess()) { + $this->logger->notice('API Error', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]); + if (!$data) { + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL); + + return null; + } + $data->code = $curlResult->getReturnCode(); + } + + if (!empty($data->code) && ($data->code >= 200) && ($data->code < 400)) { + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_SUCCESS); + $this->pConfig->set($uid, 'bluesky', 'status-message', ''); + } else { + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_API_FAIL); + if (!empty($data->message)) { + $this->pConfig->set($uid, 'bluesky', 'status-message', $data->message); + } elseif (!empty($data->code)) { + $this->pConfig->set($uid, 'bluesky', 'status-message', 'Error Code: ' . $data->code); + } + } + return $data; + } + + /** + * Fetches the PDS for a given user + * @see https://atproto.com/guides/glossary#pds-personal-data-server + * + * @param integer $uid User ID or 0 + * @return string|null PDS or null if the user has got no PDS assigned. If UID set to 0, the public api URL is used + */ + private function getUserPds(int $uid): ?string + { + if ($uid == 0) { + return self::APPVIEW_API; + } + + $pds = $this->pConfig->get($uid, 'bluesky', 'pds'); + if (!empty($pds)) { + return $pds; + } + + $did = $this->getUserDid($uid); + if (empty($did)) { + return null; + } + + $pds = $this->getPdsOfDid($did); + if (empty($pds)) { + return null; + } + + $this->pConfig->set($uid, 'bluesky', 'pds', $pds); + return $pds; + } + + /** + * Fetch the DID for a given user + * @see https://atproto.com/guides/glossary#did-decentralized-id + * + * @param integer $uid User ID + * @param boolean $refresh Default "false". If set to true, the DID is detected from the handle again. + * @return string|null DID or null if no DID has been found. + */ + public function getUserDid(int $uid, bool $refresh = false): ?string + { + if (!$refresh) { + $did = $this->pConfig->get($uid, 'bluesky', 'did'); + if (!empty($did)) { + return $did; + } + } + + $handle = $this->pConfig->get($uid, 'bluesky', 'handle'); + if (empty($handle)) { + return null; + } + + $did = $this->getDid($handle); + if (empty($did)) { + return null; + } + + $this->logger->debug('Got DID for user', ['uid' => $uid, 'handle' => $handle, 'did' => $did]); + $this->pConfig->set($uid, 'bluesky', 'did', $did); + return $did; + } + + /** + * Fetches the DID for a given handle + * + * @param string $handle The user handle + * @return string DID (did:plc:...) + */ + public function getDid(string $handle): string + { + if ($handle == '') { + return ''; + } + + if (strpos($handle, '.') === false) { + $handle .= '.' . self::HOSTNAME; + } + + // At first we use the AppView API which *should* cover all cases. + $data = $this->get(self::APPVIEW_API . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle)); + if (!empty($data) && !empty($data->did)) { + $this->logger->debug('Got DID by system PDS call', ['handle' => $handle, 'did' => $data->did]); + return $data->did; + } + + // Then we query the DNS, which is used for third party handles (DNS should be faster than wellknown) + $did = $this->getDidByDns($handle); + if ($did != '') { + $this->logger->debug('Got DID by DNS', ['handle' => $handle, 'did' => $did]); + return $did; + } + + // Then we query wellknown, which should mostly cover the rest. + $did = $this->getDidByWellknown($handle); + if ($did != '') { + $this->logger->debug('Got DID by wellknown', ['handle' => $handle, 'did' => $did]); + return $did; + } + + $this->logger->notice('No DID detected', ['handle' => $handle]); + return ''; + } + + /** + * Fetches a DID for a given profile URL + * + * @param string $url HTTP path to the profile in the format https://bsky.app/profile/username + * @return string DID (did:plc:...) + */ + public function getDidByProfile(string $url): string + { + if (preg_match('#^' . self::WEB . '/profile/(.+)#', $url, $matches)) { + $did = $this->getDid($matches[1]); + if (!empty($did)) { + return $did; + } + } + try { + $curlResult = $this->httpClient->get($url, HttpClientAccept::HTML, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTINFO]); + } catch (\Throwable $th) { + return ''; + } + if (!$curlResult->isSuccess()) { + return ''; + } + $profile = $curlResult->getBodyString(); + if (empty($profile)) { + return ''; + } + + $doc = new DOMDocument(); + try { + @$doc->loadHTML($profile); + } catch (\Throwable $th) { + return ''; + } + $xpath = new DOMXPath($doc); + $list = $xpath->query('//p[@id]'); + foreach ($list as $node) { + foreach ($node->attributes as $attribute) { + if ($attribute->name == 'id') { + $ids[$attribute->value] = $node->textContent; + } + } + } + + if (empty($ids['bsky_handle']) || empty($ids['bsky_did'])) { + return ''; + } + + if (!$this->isValidDid($ids['bsky_did'], $ids['bsky_handle'])) { + $this->logger->notice('Invalid DID', ['handle' => $ids['bsky_handle'], 'did' => $ids['bsky_did']]); + return ''; + } + + return $ids['bsky_did']; + } + + /** + * Fetches the DID of a given handle via a HTTP request to the .well-known URL. + * This is one of the ways, custom handles can be authorized. + * + * @param string $handle The user handle + * @return string DID (did:plc:...) + */ + private function getDidByWellknown(string $handle): string + { + $curlResult = $this->httpClient->get('http://' . $handle . '/.well-known/atproto-did'); + if ($curlResult->isSuccess() && substr($curlResult->getBodyString(), 0, 4) == 'did:') { + $did = $curlResult->getBodyString(); + if (!$this->isValidDid($did, $handle)) { + $this->logger->notice('Invalid DID', ['handle' => $handle, 'did' => $did]); + return ''; + } + return $did; + } + return ''; + } + + /** + * Fetches the DID of a given handle via a DND request. + * This is one of the ways, custom handles can be authorized. + * + * @param string $handle The user handle + * @return string DID (did:plc:...) + */ + private function getDidByDns(string $handle): string + { + $records = @dns_get_record('_atproto.' . $handle . '.', DNS_TXT); + if (empty($records)) { + return ''; + } + foreach ($records as $record) { + if (!empty($record['txt']) && substr($record['txt'], 0, 4) == 'did=') { + $did = substr($record['txt'], 4); + if (!$this->isValidDid($did, $handle)) { + $this->logger->notice('Invalid DID', ['handle' => $handle, 'did' => $did]); + return ''; + } + return $did; + } + } + return ''; + } + + /** + * Fetch the PDS of a given DID + * + * @param string $did DID (did:plc:...) + * @return string|null URL of the PDS, e.g. https://enoki.us-east.host.bsky.network + */ + public function getPdsOfDid(string $did): ?string + { + $data = $this->get(self::DIRECTORY . '/' . $did); + if (empty($data) || empty($data->service)) { + return null; + } + + foreach ($data->service as $service) { + if (($service->id == '#atproto_pds') && ($service->type == 'AtprotoPersonalDataServer') && !empty($service->serviceEndpoint)) { + return $service->serviceEndpoint; + } + } + + return null; + } + + /** + * Checks if the provided DID matches the handle + * + * @param string $did DID (did:plc:...) + * @param string $handle The user handle + * @return boolean + */ + private function isValidDid(string $did, string $handle): bool + { + $data = $this->get(self::DIRECTORY . '/' . $did); + if (empty($data) || empty($data->alsoKnownAs)) { + return false; + } + + return in_array('at://' . $handle, $data->alsoKnownAs); + } + + /** + * Fetches the user token for a given user + * + * @param integer $uid User ID + * @return string user token + */ + public function getUserToken(int $uid): string + { + $token = $this->pConfig->get($uid, 'bluesky', 'access_token'); + $created = $this->pConfig->get($uid, 'bluesky', 'token_created'); + if (empty($token)) { + return ''; + } + + if ($created + 300 < time()) { + return $this->refreshUserToken($uid); + } + return $token; + } + + /** + * Refresh and returns the user token for a given user. + * + * @param integer $uid User ID + * @return string user token + */ + private function refreshUserToken(int $uid): string + { + $token = $this->pConfig->get($uid, 'bluesky', 'refresh_token'); + + $data = $this->post($uid, '/xrpc/com.atproto.server.refreshSession', '', ['Authorization' => ['Bearer ' . $token]]); + if (empty($data) || empty($data->accessJwt)) { + $this->logger->debug('Refresh failed', ['return' => $data]); + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_TOKEN_FAIL); + return ''; + } + + $this->logger->debug('Refreshed token', ['return' => $data]); + $this->pConfig->set($uid, 'bluesky', 'access_token', $data->accessJwt); + $this->pConfig->set($uid, 'bluesky', 'refresh_token', $data->refreshJwt); + $this->pConfig->set($uid, 'bluesky', 'token_created', time()); + return $data->accessJwt; + } + + /** + * Create a user token for the given user + * + * @param integer $uid User ID + * @param string $password Application password + * @return string user token + */ + public function createUserToken(int $uid, string $password): string + { + $did = $this->getUserDid($uid); + if (empty($did)) { + return ''; + } + + $data = $this->post($uid, '/xrpc/com.atproto.server.createSession', json_encode(['identifier' => $did, 'password' => $password]), ['Content-type' => 'application/json']); + if (empty($data) || empty($data->accessJwt)) { + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_TOKEN_FAIL); + return ''; + } + + $this->logger->debug('Created token', ['return' => $data]); + $this->pConfig->set($uid, 'bluesky', 'access_token', $data->accessJwt); + $this->pConfig->set($uid, 'bluesky', 'refresh_token', $data->refreshJwt); + $this->pConfig->set($uid, 'bluesky', 'token_created', time()); + $this->pConfig->set($uid, 'bluesky', 'status', self::STATUS_TOKEN_OK); + $this->pConfig->set($uid, 'bluesky', 'status-message', ''); + return $data->accessJwt; + } +} diff --git a/src/Protocol/ATProtocol/Actor.php b/src/Protocol/ATProtocol/Actor.php new file mode 100755 index 0000000000..4dae8b75cf --- /dev/null +++ b/src/Protocol/ATProtocol/Actor.php @@ -0,0 +1,223 @@ +logger = $logger; + $this->atprotocol = $atprotocol; + } + + /** + * Syncronize the contacts (followers, sharers) for the given user + * + * @param integer $uid User ID + * @return void + */ + public function syncContacts(int $uid): void + { + $this->logger->info('Sync contacts for user - start', ['uid' => $uid]); + $contacts = Contact::selectToArray(['id', 'url', 'rel'], ['uid' => $uid, 'network' => Protocol::BLUESKY, 'rel' => [Contact::FRIEND, Contact::SHARING, Contact::FOLLOWER]]); + + $follows = []; + $cursor = ''; + $profiles = []; + + do { + $parameters = [ + 'actor' => $this->atprotocol->getUserDid($uid), + 'limit' => 100, + 'cursor' => $cursor + ]; + + $data = $this->atprotocol->XRPCGet('app.bsky.graph.getFollows', $parameters); + + foreach ($data->follows ?? [] as $follow) { + $profiles[$follow->did] = $follow; + $follows[$follow->did] = Contact::SHARING; + } + $cursor = $data->cursor ?? ''; + } while (!empty($data->follows) && !empty($data->cursor)); + + $cursor = ''; + + do { + $parameters = [ + 'actor' => $this->atprotocol->getUserDid($uid), + 'limit' => 100, + 'cursor' => $cursor + ]; + + $data = $this->atprotocol->XRPCGet('app.bsky.graph.getFollowers', $parameters); + + foreach ($data->followers ?? [] as $follow) { + $profiles[$follow->did] = $follow; + $follows[$follow->did] = ($follows[$follow->did] ?? 0) | Contact::FOLLOWER; + } + $cursor = $data->cursor ?? ''; + } while (!empty($data->followers) && !empty($data->cursor)); + + foreach ($contacts as $contact) { + if (empty($follows[$contact['url']])) { + Contact::update(['rel' => Contact::NOTHING], ['id' => $contact['id']]); + } + } + + foreach ($follows as $did => $rel) { + $contact = $this->getContactByDID($did, $uid, $uid); + if (($contact['rel'] != $rel) && ($contact['uid'] != 0)) { + Contact::update(['rel' => $rel], ['id' => $contact['id']]); + } + } + $this->logger->info('Sync contacts for user - done', ['uid' => $uid]); + } + + /** + * Update a contact for a given DID and user id + * + * @param string $did DID (did:plc:...) + * @param integer $contact_uid User id of the contact to be updated + * @return void + */ + public function updateContactByDID(string $did, int $contact_uid): void + { + $profile = $this->atprotocol->XRPCGet('app.bsky.actor.getProfile', ['actor' => $did], $contact_uid); + if (empty($profile) || empty($profile->did)) { + return; + } + + $nick = $profile->handle ?? $profile->did; + $name = $profile->displayName ?? $nick; + + $fields = [ + 'alias' => ATProtocol::WEB . '/profile/' . $profile->did, + 'name' => $name ?: $nick, + 'nick' => $nick, + 'addr' => $nick, + 'updated' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL), + ]; + + if (!empty($profile->description)) { + $fields['about'] = HTML::toBBCode($profile->description); + } + + if (!empty($profile->banner)) { + $fields['header'] = $profile->banner; + } + + $directory = $this->atprotocol->get(ATProtocol::DIRECTORY . '/' . $profile->did); + if (!empty($directory->service)) { + foreach ($directory->service as $service) { + if (($service->id == '#atproto_pds') && ($service->type == 'AtprotoPersonalDataServer') && !empty($service->serviceEndpoint)) { + $fields['baseurl'] = $service->serviceEndpoint; + } + } + + if (!empty($fields['baseurl'])) { + GServer::check($fields['baseurl'], Protocol::BLUESKY); + $fields['gsid'] = GServer::getRealID($fields['baseurl'], true); + } + + if (!empty($directory->verificationMethod)) { + foreach ($directory->verificationMethod as $method) { + if (!empty($method->publicKeyMultibase)) { + $fields['pubkey'] = $method->publicKeyMultibase; + } + } + } + } + + Contact::update($fields, ['nurl' => $profile->did, 'network' => Protocol::BLUESKY]); + + if (!empty($profile->avatar)) { + $contact = Contact::selectFirst(['id', 'avatar'], ['network' => Protocol::BLUESKY, 'nurl' => $did, 'uid' => 0]); + if (!empty($contact['id']) && ($contact['avatar'] != $profile->avatar)) { + Contact::updateAvatar($contact['id'], $profile->avatar); + } + } + + $this->logger->notice('Update global profile', ['did' => $profile->did, 'fields' => $fields]); + + if (!empty($profile->viewer) && ($contact_uid != 0)) { + if (!empty($profile->viewer->following) && !empty($profile->viewer->followedBy)) { + $user_fields = ['rel' => Contact::FRIEND]; + } elseif (!empty($profile->viewer->following) && empty($profile->viewer->followedBy)) { + $user_fields = ['rel' => Contact::SHARING]; + } elseif (empty($profile->viewer->following) && !empty($profile->viewer->followedBy)) { + $user_fields = ['rel' => Contact::FOLLOWER]; + } else { + $user_fields = ['rel' => Contact::NOTHING]; + } + Contact::update($user_fields, ['nurl' => $profile->did, 'network' => Protocol::BLUESKY, 'uid' => $contact_uid]); + $this->logger->notice('Update user profile', ['uid' => $contact_uid, 'did' => $profile->did, 'fields' => $user_fields]); + } + } + + /** + * Fetch and possibly create a contact array for a given DID + * + * @param string $did The contact DID + * @param integer $uid "0" when either the public contact or the user contact is desired + * @param integer $contact_uid If not found, the contact will be created for this user id + * @param boolean $auto_update Default "false". If activated, the contact will be updated every 24 hours + * @return array Contact array + */ + public function getContactByDID(string $did, int $uid, int $contact_uid, bool $auto_update = false): array + { + $contact = Contact::selectFirst([], ['network' => Protocol::BLUESKY, 'nurl' => $did, 'uid' => [$contact_uid, $uid]], ['order' => ['uid' => true]]); + + if (!empty($contact) && (!$auto_update || ($contact['updated'] > DateTimeFormat::utc('now -24 hours')))) { + return $contact; + } + + $fields = [ + 'uid' => $contact_uid, + 'network' => Protocol::BLUESKY, + 'priority' => 1, + 'writable' => true, + 'blocked' => false, + 'readonly' => false, + 'pending' => false, + 'url' => $did, + 'nurl' => $did, + 'alias' => ATProtocol::WEB . '/profile/' . $did, + 'name' => $did, + 'nick' => $did, + 'addr' => $did, + 'rel' => Contact::NOTHING, + ]; + + $cid = Contact::insert($fields); + + $this->updateContactByDID($did, $contact_uid); + + return Contact::getById($cid); + } +} diff --git a/src/Protocol/ATProtocol/DID.php b/src/Protocol/ATProtocol/DID.php new file mode 100644 index 0000000000..311adae77a --- /dev/null +++ b/src/Protocol/ATProtocol/DID.php @@ -0,0 +1,67 @@ +getHost(); + + if (($host == $server['SERVER_NAME']) || !strpos($server['SERVER_NAME'], '.' . $host)) { + return; + } + + if (!DI::config()->get('bluesky', 'friendica_handles')) { + throw new HTTPException\NotFoundException(); + } + + if (!in_array($path, ['.well-known/atproto-did', ''])) { + throw new HTTPException\NotFoundException(); + } + + $nick = str_replace('.' . $host, '', $server['SERVER_NAME']); + + $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]); + if (empty($user['uid'])) { + throw new HTTPException\NotFoundException(); + } + + if (!DI::pConfig()->get($user['uid'], 'bluesky', 'friendica_handle')) { + throw new HTTPException\NotFoundException(); + } + + if ($path == '') { + System::externalRedirect(DI::baseUrl() . '/profile/' . urlencode($nick), 0); + } + + $did = DI::pConfig()->get($user['uid'], 'bluesky', 'did'); + if (empty($did)) { + throw new HTTPException\NotFoundException(); + } + + header('Content-Type: text/plain'); + echo $did; + System::exit(); + } +} diff --git a/src/Protocol/ATProtocol/Jetstream.php b/src/Protocol/ATProtocol/Jetstream.php new file mode 100755 index 0000000000..a4771cae44 --- /dev/null +++ b/src/Protocol/ATProtocol/Jetstream.php @@ -0,0 +1,517 @@ +logger = $logger; + $this->config = $config; + $this->keyValue = $keyValue; + $this->atprotocol = $atprotocol; + $this->actor = $actor; + $this->processor = $processor; + } + + /** + * Listen to incoming webstream messages from Jetstream + * + * @return void + */ + public function listen(): void + { + $timeout = 300; + $timeout_limit = 10; + $timestamp = $this->keyValue->get('jetstream_timestamp') ?? 0; + $cursor = ''; + $this->logger->notice('Start listening'); + + while (true) { + if ($timestamp) { + $cursor = '&cursor=' . $timestamp; + $this->logger->notice('Start with cursor', ['cursor' => $cursor]); + } + + $this->syncContacts(); + try { + // @todo make the path configurable + $this->client = new \WebSocket\Client('wss://jetstream1.us-west.bsky.network/subscribe?requireHello=true' . $cursor); + $this->client->setTimeout($timeout); + $this->client->setLogger($this->logger); + } catch (\WebSocket\ConnectionException $e) { + $this->logger->error('Error while trying to establish the connection', ['code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); + echo "Connection wasn't established.\n"; + exit(1); + } + $this->setOptions(); + $last_timeout = time(); + while (true) { + try { + $message = $this->client->receive(); + + if (empty($message)) { + $this->logger->notice('Empty message received'); + break; + } + $data = json_decode($message); + if (is_object($data)) { + $timestamp = $data->time_us; + $this->route($data); + $this->keyValue->set('jetstream_timestamp', $timestamp); + $this->incrementMessages(); + } else { + $this->logger->warning('Unexpected return value', ['data' => $data]); + break; + } + } catch (\WebSocket\ConnectionException $e) { + if ($e->getCode() == 1024) { + $timeout_duration = time() - $last_timeout; + if ($timeout_duration < $timeout_limit) { + $this->logger->notice('Timeout - connection lost', ['duration' => $timeout_duration, 'timestamp' => $timestamp, 'code' => $e->getCode(), 'message' => $e->getMessage()]); + break; + } + $this->logger->notice('Timeout', ['duration' => $timeout_duration, 'timestamp' => $timestamp, 'code' => $e->getCode(), 'message' => $e->getMessage()]); + break; + } else { + $this->logger->error('Error while trying to receive a message', ['code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); + break; + } + } + $last_timeout = time(); + } + try { + $this->client->close(); + } catch (\WebSocket\ConnectionException $e) { + $this->logger->error('Error while trying to close the connection', ['code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); + } + } + $this->logger->notice('Stop listening'); + } + + /** + * Increment the message counter for the statistics page + * + * @return void + */ + private function incrementMessages(): void + { + $packets = (int)($this->keyValue->get('jetstream_messages') ?? 0); + if ($packets >= PHP_INT_MAX) { + $packets = 0; + } + $this->keyValue->set('jetstream_messages', $packets + 1); + } + + /** + * Synchronize contacts for all active users + * + * @return void + */ + private function syncContacts() + { + $active_uids = $this->atprotocol->getUids(); + if (empty($active_uids)) { + return; + } + + foreach ($active_uids as $uid) { + $this->actor->syncContacts($uid); + } + } + + /** + * Set options like the followed DIDs + * + * @return void + */ + private function setOptions() + { + $active_uids = $this->atprotocol->getUids(); + if (empty($active_uids)) { + return; + } + + $contacts = Contact::selectToArray(['uid', 'url'], ['uid' => $active_uids, 'network' => Protocol::BLUESKY, 'rel' => [Contact::FRIEND, Contact::SHARING]]); + + $self = []; + foreach ($active_uids as $uid) { + $did = $this->atprotocol->getUserDid($uid); + $contacts[] = ['uid' => $uid, 'url' => $did]; + $self[$did] = $uid; + } + $this->self = $self; + + $uids = []; + foreach ($contacts as $contact) { + $uids[$contact['url']][] = $contact['uid']; + } + $this->uids = $uids; + + $did_limit = $this->config->get('jetstream', 'did_limit'); + + $dids = array_keys($uids); + if (count($dids) > $did_limit) { + $contacts = Contact::selectToArray(['url'], ['uid' => $active_uids, 'network' => Protocol::BLUESKY, 'rel' => [Contact::FRIEND, Contact::SHARING]], ['order' => ['last-item' => true]]); + $dids = $this->addDids($contacts, $uids, $did_limit, array_keys($self)); + } + + if (count($dids) < $did_limit) { + $contacts = Contact::selectToArray(['url'], ['uid' => $active_uids, 'network' => Protocol::BLUESKY, 'rel' => Contact::FOLLOWER], ['order' => ['last-item' => true]]); + $dids = $this->addDids($contacts, $uids, $did_limit, $dids); + } + + if (!$this->capped && count($dids) < $did_limit) { + $condition = ["`uid` = ? AND `network` = ? AND EXISTS(SELECT `author-id` FROM `post-user` WHERE `author-id` = `contact`.`id` AND `post-user`.`uid` != ?)", 0, Protocol::BLUESKY, 0]; + $contacts = Contact::selectToArray(['url'], $condition, ['order' => ['last-item' => true], 'limit' => $did_limit]); + $dids = $this->addDids($contacts, $uids, $did_limit, $dids); + } + + $this->keyValue->set('jetstream_did_count', count($dids)); + $this->keyValue->set('jetstream_did_limit', $did_limit); + + $this->logger->debug('Selected DIDs', ['uids' => $active_uids, 'count' => count($dids), 'capped' => $this->capped]); + $update = [ + 'type' => 'options_update', + 'payload' => [ + 'wantedCollections' => ['app.bsky.feed.post', 'app.bsky.feed.repost', 'app.bsky.feed.like', 'app.bsky.graph.block', 'app.bsky.actor.profile', 'app.bsky.graph.follow'], + 'wantedDids' => $dids, + 'maxMessageSizeBytes' => 1000000 + ] + ]; + try { + $this->client->send(json_encode($update)); + } catch (\WebSocket\ConnectionException $e) { + $this->logger->error('Error while trying to send options.', ['code' => $e->getCode(), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()]); + } + } + + /** + * Returns an array of DIDs provided by an array of contacts + * + * @param array $contacts Array of contact records + * @param array $uids Array with the user ids with enabled bluesky timeline import + * @param integer $did_limit Maximum limit of entries + * @param array $dids Array of DIDs that are added to the output list + * @return array DIDs + */ + private function addDids(array $contacts, array $uids, int $did_limit, array $dids): array + { + foreach ($contacts as $contact) { + if (in_array($contact['url'], $uids)) { + continue; + } + $dids[] = $contact['url']; + if (count($dids) >= $did_limit) { + break; + } + } + return $dids; + } + + /** + * Route incoming messages + * + * @param stdClass $data message object + * @return void + */ + private function route(stdClass $data): void + { + Item::incrementInbound(Protocol::BLUESKY); + + switch ($data->kind) { + case 'account': + if (!empty($data->identity->did)) { + $this->processor->processAccount($data); + } + break; + + case 'identity': + $this->processor->processIdentity($data); + break; + + case 'commit': + $this->routeCommits($data); + break; + } + } + + /** + * Route incoming commit messages + * + * @param stdClass $data message object + * @return void + */ + private function routeCommits(stdClass $data): void + { + $drift = $this->getDrift($data); + $this->logger->notice('Received commit', ['time' => date(DateTimeFormat::ATOM, $data->time_us / 1000000), 'drift' => $drift, 'capped' => $this->capped, 'did' => $data->did, 'operation' => $data->commit->operation, 'collection' => $data->commit->collection, 'timestamp' => $data->time_us]); + $timestamp = microtime(true); + + switch ($data->commit->collection) { + case 'app.bsky.feed.post': + $this->routePost($data, $drift); + break; + + case 'app.bsky.feed.repost': + $this->routeRepost($data, $drift); + break; + + case 'app.bsky.feed.like': + $this->routeLike($data); + break; + + case 'app.bsky.graph.block': + $this->processor->performBlocks($data, $this->self[$data->did] ?? 0); + break; + + case 'app.bsky.actor.profile': + $this->routeProfile($data); + break; + + case 'app.bsky.graph.follow': + $this->routeFollow($data); + break; + + case 'app.bsky.feed.generator': + case 'app.bsky.feed.postgate': + case 'app.bsky.feed.threadgate': + case 'app.bsky.graph.list': + case 'app.bsky.graph.listblock': + case 'app.bsky.graph.listitem': + case 'app.bsky.graph.starterpack': + // Ignore these collections, since we can't really process them + break; + + default: + $this->storeCommitMessage($data); + break; + } + if (microtime(true) - $timestamp > 2) { + $this->logger->notice('Commit processed', ['duration' => round(microtime(true) - $timestamp, 3), 'drift' => $drift, 'capped' => $this->capped, 'time' => date(DateTimeFormat::ATOM, $data->time_us / 1000000), 'did' => $data->did, 'operation' => $data->commit->operation, 'collection' => $data->commit->collection]); + } + } + + /** + * Calculate the drift between the server timestamp and the current time. + * + * @param stdClass $data message object + * @return integer The calculated drift + */ + private function getDrift(stdClass $data): int + { + $drift = max(0, round(time() - $data->time_us / 1000000)); + $this->keyValue->set('jetstream_drift', $drift); + + if ($drift > 60 && !$this->capped) { + $this->capped = true; + $this->setOptions(); + $this->logger->notice('Drift is too high, dids will be capped'); + } elseif ($drift == 0 && $this->capped) { + $this->capped = false; + $this->setOptions(); + $this->logger->notice('Drift is low enough, dids will be uncapped'); + } + return $drift; + } + + /** + * Route app.bsky.feed.post commits + * + * @param stdClass $data message object + * @param integer $drift + * @return void + */ + private function routePost(stdClass $data, int $drift): void + { + switch ($data->commit->operation) { + case 'delete': + $this->processor->deleteRecord($data); + break; + + case 'create': + $this->processor->createPost($data, $this->uids[$data->did] ?? [0], ($drift > 30)); + break; + + default: + $this->storeCommitMessage($data); + break; + } + } + + /** + * Route app.bsky.feed.repost commits + * + * @param stdClass $data message object + * @param integer $drift + * @return void + */ + private function routeRepost(stdClass $data, int $drift): void + { + switch ($data->commit->operation) { + case 'delete': + $this->processor->deleteRecord($data); + break; + + case 'create': + $this->processor->createRepost($data, $this->uids[$data->did] ?? [0], ($drift > 30)); + break; + + default: + $this->storeCommitMessage($data); + break; + } + } + + /** + * Route app.bsky.feed.like commits + * + * @param stdClass $data message object + * @return void + */ + private function routeLike(stdClass $data): void + { + switch ($data->commit->operation) { + case 'delete': + $this->processor->deleteRecord($data); + break; + + case 'create': + $this->processor->createLike($data); + break; + + default: + $this->storeCommitMessage($data); + break; + } + } + + /** + * Route app.bsky.actor.profile commits + * + * @param stdClass $data message object + * @return void + */ + private function routeProfile(stdClass $data): void + { + switch ($data->commit->operation) { + case 'delete': + $this->storeCommitMessage($data); + break; + + case 'create': + $this->actor->updateContactByDID($data->did, 0); + break; + + case 'update': + $this->actor->updateContactByDID($data->did, 0); + break; + + default: + $this->storeCommitMessage($data); + break; + } + } + + /** + * Route app.bsky.graph.follow commits + * + * @param stdClass $data message object + * @return void + */ + private function routeFollow(stdClass $data): void + { + switch ($data->commit->operation) { + case 'delete': + if ($this->processor->deleteFollow($data, $this->self)) { + $this->syncContacts(); + $this->setOptions(); + } + break; + + case 'create': + if ($this->processor->createFollow($data, $this->self)) { + $this->syncContacts(); + $this->setOptions(); + } + break; + + default: + $this->storeCommitMessage($data); + break; + } + } + + /** + * Store commit messages for debugging purposes + * + * @param stdClass $data message object + * @return void + */ + private function storeCommitMessage(stdClass $data): void + { + if ($this->config->get('debug', 'jetstream_log')) { + $tempfile = tempnam(System::getTempPath(), 'at-proto.commit.' . $data->commit->collection . '.' . $data->commit->operation . '-'); + file_put_contents($tempfile, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + } + } +} diff --git a/src/Protocol/ATProtocol/Processor.php b/src/Protocol/ATProtocol/Processor.php new file mode 100755 index 0000000000..1ac2a9f00c --- /dev/null +++ b/src/Protocol/ATProtocol/Processor.php @@ -0,0 +1,914 @@ +db = $database; + $this->logger = $logger; + $this->baseURL = $baseURL; + $this->atprotocol = $atprotocol; + $this->actor = $actor; + } + + public function processAccount(stdClass $data) + { + $fields = [ + 'archive' => !$data->account->active, + 'failed' => !$data->account->active, + 'updated' => DateTimeFormat::utc($data->account->time, DateTimeFormat::MYSQL) + ]; + + $this->logger->notice('Process account', ['did' => $data->identity->did, 'fields' => $fields]); + + Contact::update($fields, ['nurl' => $data->identity->did, 'network' => Protocol::BLUESKY]); + } + + public function processIdentity(stdClass $data) + { + $fields = [ + 'alias' => ATProtocol::WEB . '/profile/' . $data->identity->did, + 'nick' => $data->identity->handle, + 'addr' => $data->identity->handle, + 'updated' => DateTimeFormat::utc($data->identity->time, DateTimeFormat::MYSQL), + ]; + + $this->logger->notice('Process identity', ['did' => $data->identity->did, 'fields' => $fields]); + + Contact::update($fields, ['nurl' => $data->identity->did, 'network' => Protocol::BLUESKY]); + } + + public function performBlocks(stdClass $data, int $uid) + { + if (!$uid) { + $this->logger->info('Not a block from a local user'); + return; + } + + if (empty($data->commit->record->subject)) { + $this->logger->info('No subject in data', ['data' => $data]); + return; + } + + $contact = Contact::selectFirst(['id'], ['nurl' => $data->commit->record->subject, 'uid' => 0]); + if (empty($contact['id'])) { + $this->logger->info('Contact not found', ['did' => $data->commit->record->subject]); + return; + } + + // @todo unblock doesn't provide a subject. We will only arrive here, wenn the operation is "create". + Contact\User::setBlocked($contact['id'], $uid, ($data->commit->operation == 'create'), true); + $this->logger->info('Contact blocked', ['id' => $contact['id'], 'did' => $data->commit->record->subject, 'uid' => $uid]); + } + + public function deleteRecord(stdClass $data) + { + $uri = 'at://' . $data->did . '/' . $data->commit->collection . '/' . $data->commit->rkey; + $itemuri = $this->db->selectFirst('item-uri', ['id'], ["`uri` LIKE ?", $uri . ':%']); + if (empty($itemuri['id'])) { + $this->logger->info('URI not found', ['url' => $uri]); + return; + } + + $condition = ['uri-id' => $itemuri['id'], 'author-link' => $data->did, 'network' => Protocol::BLUESKY]; + if (!Post::exists($condition)) { + $this->logger->info('Record not found', $condition); + return; + } + Item::markForDeletion($condition); + $this->logger->info('Record deleted', $condition); + } + + public function createPost(stdClass $data, array $uids, bool $dont_fetch) + { + $parent = ''; + + if (!empty($data->commit->record->reply)) { + $root = $this->getUri($data->commit->record->reply->root); + $parent = $this->getUri($data->commit->record->reply->parent); + $uids = $this->getPostUids($root, true); + if (!$uids) { + $this->logger->debug('Comment is not imported since the root post is not found.', ['root' => $root, 'parent' => $parent]); + return; + } + if ($dont_fetch && !$this->getPostUids($parent, false)) { + $this->logger->debug('Comment is not imported since the parent post is not found.', ['root' => $root, 'parent' => $parent]); + return; + } + } + + foreach ($uids as $uid) { + $item = []; + $item = $this->getHeaderFromJetstream($data, $uid); + if (empty($item)) { + continue; + } + + if (!empty($root)) { + $item['parent-uri'] = $root; + $item['thr-parent'] = $this->fetchMissingPost($parent, $uid, Item::PR_FETCHED, $item['contact-id'], 0, $parent, false, Conversation::PARCEL_JETSTREAM); + $item['gravity'] = Item::GRAVITY_COMMENT; + } else { + $item['gravity'] = Item::GRAVITY_PARENT; + } + + $item['body'] = $this->parseFacets($data->commit->record, $item['uri-id']); + $item['transmitted-languages'] = $data->commit->record->langs ?? []; + + if (!empty($data->commit->record->embed)) { + if (empty($post)) { + $uri = 'at://' . $data->did . '/' . $data->commit->collection . '/' . $data->commit->rkey; + $post = $this->atprotocol->XRPCGet('app.bsky.feed.getPostThread', ['uri' => $uri]); + if (empty($post->thread->post->embed)) { + $this->logger->notice('Post was not fetched', ['uri' => $uri, 'post' => $post]); + return; + } + } + $item['source'] = json_encode($post); + $item = $this->addMedia($post->thread->post->embed, $item, 0); + } + + $id = Item::insert($item); + + if ($id) { + $this->logger->info('Post inserted', ['id' => $id, 'guid' => $item['guid']]); + } elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) { + $this->logger->notice('Post was found', ['guid' => $item['guid'], 'uri' => $item['uri']]); + } else { + $this->logger->warning('Post was not inserted', ['guid' => $item['guid'], 'uri' => $item['uri']]); + } + } + } + + public function createRepost(stdClass $data, array $uids, bool $dont_fetch) + { + if ($dont_fetch && !$this->getPostUids($this->getUri($data->commit->record->subject), true)) { + $this->logger->debug('Repost is not imported since the subject is not found.', ['subject' => $this->getUri($data->commit->record->subject)]); + return; + } + + foreach ($uids as $uid) { + $item = $this->getHeaderFromJetstream($data, $uid); + if (empty($item)) { + continue; + } + + $item['gravity'] = Item::GRAVITY_ACTIVITY; + $item['body'] = $item['verb'] = Activity::ANNOUNCE; + $item['thr-parent'] = $this->getUri($data->commit->record->subject); + $item['thr-parent'] = $this->fetchMissingPost($item['thr-parent'], 0, Item::PR_FETCHED, $item['contact-id'], 0, $item['thr-parent'], false, Conversation::PARCEL_JETSTREAM); + + $id = Item::insert($item); + + if ($id) { + $this->logger->info('Repost inserted', ['id' => $id]); + } elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) { + $this->logger->notice('Repost was found', ['uri' => $item['uri']]); + } else { + $this->logger->warning('Repost was not inserted', ['uri' => $item['uri']]); + } + } + } + + public function createLike(stdClass $data) + { + $uids = $this->getPostUids($this->getUri($data->commit->record->subject), false); + if (!$uids) { + $this->logger->debug('Like is not imported since the subject is not found.', ['subject' => $this->getUri($data->commit->record->subject)]); + return; + } + foreach ($uids as $uid) { + $item = $this->getHeaderFromJetstream($data, $uid); + if (empty($item)) { + continue; + } + + $item['gravity'] = Item::GRAVITY_ACTIVITY; + $item['body'] = $item['verb'] = Activity::LIKE; + $item['thr-parent'] = $this->getPostUri($this->getUri($data->commit->record->subject), $uid); + + $id = Item::insert($item); + + if ($id) { + $this->logger->info('Like inserted', ['id' => $id]); + } elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) { + $this->logger->notice('Like was found', ['uri' => $item['uri']]); + } else { + $this->logger->warning('Like was not inserted', ['uri' => $item['uri']]); + } + } + } + + public function deleteFollow(stdClass $data, array $self): bool + { + return !empty($self[$data->did]); + } + + public function createFollow(stdClass $data, array $self): bool + { + if (!empty($self[$data->did])) { + $uid = $self[$data->did]; + $target = $data->commit->record->subject; + $rel = Contact::SHARING; + $this->logger->debug('Follow by a local user', ['uid' => $uid, 'following' => $target]); + } elseif (!empty($self[$data->commit->record->subject])) { + $uid = $self[$data->commit->record->subject]; + $target = $data->did; + $rel = Contact::FOLLOWER; + $this->logger->debug('New follower for a local user', ['uid' => $uid, 'follower' => $target]); + } else { + $this->logger->debug('No local part', ['did' => $data->did, 'target' => $data->commit->record->subject]); + return false; + } + $contact = $this->actor->getContactByDID($target, $uid, $uid); + if (empty($contact)) { + $this->logger->notice('Contact not found', ['uid' => $uid, 'target' => $target]); + return false; + } + Contact::update(['rel' => $rel | $contact['rel']], ['id' => $contact['id']]); + return true; + } + + public function processPost(stdClass $post, int $uid, int $post_reason, int $causer, int $level, int $protocol): int + { + $uri = $this->getUri($post); + + if ($uri_id = $this->fetchUriId($uri, $uid)) { + return $uri_id; + } + + if (empty($post->record)) { + $this->logger->debug('Invalid post', ['uri' => $uri]); + return 0; + } + + $this->logger->debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid, 'root' => $post->record->reply->root ?? '']); + + $item = $this->getHeaderFromPost($post, $uri, $uid, $protocol); + if (empty($item)) { + return 0; + } + $item = $this->getContent($item, $post->record, $uri, $uid, $level); + if (empty($item)) { + return 0; + } + + if (!empty($post->embed)) { + $item = $this->addMedia($post->embed, $item, $level); + } + + $item['restrictions'] = $this->getRestrictionsForUser($post, $item, $post_reason); + + if (empty($item['post-reason'])) { + $item['post-reason'] = $post_reason; + } + + if ($causer != 0) { + $item['causer-id'] = $causer; + } + + $id = Item::insert($item); + + if ($id) { + $this->logger->info('Fetched post inserted', ['id' => $id, 'guid' => $item['guid']]); + } elseif (Post::exists(['uid' => $uid, 'uri-id' => $item['uri-id']])) { + $this->logger->notice('Fetched post was found', ['guid' => $item['guid'], 'uri' => $item['uri']]); + } else { + $this->logger->warning('Fetched post was not inserted', ['guid' => $item['guid'], 'uri' => $item['uri']]); + } + + return $this->fetchUriId($uri, $uid); + } + + private function getHeaderFromJetstream(stdClass $data, int $uid, int $protocol = Conversation::PARCEL_JETSTREAM): array + { + $contact = $this->actor->getContactByDID($data->did, $uid, 0, true); + if (empty($contact)) { + $this->logger->info('Contact not found for user', ['did' => $data->did, 'uid' => $uid]); + return []; + } + + $item = [ + 'network' => Protocol::BLUESKY, + 'protocol' => $protocol, + 'uid' => $uid, + 'wall' => false, + 'uri' => 'at://' . $data->did . '/' . $data->commit->collection . '/' . $data->commit->rkey . ':' . $data->commit->cid, + 'guid' => $data->commit->cid, + 'created' => DateTimeFormat::utc($data->commit->record->createdAt, DateTimeFormat::MYSQL), + 'private' => Item::UNLISTED, + 'verb' => Activity::POST, + 'contact-id' => $contact['id'], + 'author-name' => $contact['name'], + 'author-link' => $contact['url'], + 'author-avatar' => $contact['avatar'], + 'owner-name' => $contact['name'], + 'owner-link' => $contact['url'], + 'owner-avatar' => $contact['avatar'], + 'plink' => $contact['alias'] . '/post/' . $data->commit->rkey, + 'source' => json_encode($data), + ]; + + if ((time() - strtotime($item['created'])) > 600) { + $item['received'] = $item['created']; + } + + if ($this->postExists($item['uri'], [$uid])) { + $this->logger->info('Post already exists for user', ['uri' => $item['uri'], 'uid' => $uid]); + return []; + } + + $account = Contact::selectAccountUserById($contact['id'], ['pid']); + $item['owner-id'] = $item['author-id'] = $account['pid']; + $item['uri-id'] = ItemURI::getIdByURI($item['uri']); + + if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) { + $item['post-reason'] = Item::PR_FOLLOWER; + } + + if (!empty($data->commit->record->labels)) { + foreach ($data->commit->record->labels as $label) { + // Only flag posts as sensitive based on labels that had been provided by the author. + // When "ver" is set to "1" it was flagged by some automated process. + if (empty($label->ver)) { + $item['sensitive'] = true; + $item['content-warning'] = $label->val ?? ''; + $this->logger->debug('Sensitive content', ['uri-id' => $item['uri-id'], 'label' => $label]); + } + } + } + + return $item; + } + + public function getHeaderFromPost(stdClass $post, string $uri, int $uid, int $protocol): array + { + $parts = $this->getUriParts($uri); + if (empty($post->author) || empty($post->cid) || empty($parts->rkey)) { + return []; + } + $contact = $this->actor->getContactByDID($post->author->did, $uid, 0, true); + if (empty($contact)) { + $this->logger->info('Contact not found for user', ['did' => $post->author->did, 'uid' => $uid]); + return []; + } + + $item = [ + 'network' => Protocol::BLUESKY, + 'protocol' => $protocol, + 'uid' => $uid, + 'wall' => false, + 'uri' => $uri, + 'guid' => $post->cid, + 'received' => DateTimeFormat::utc($post->indexedAt, DateTimeFormat::MYSQL), + 'private' => Item::UNLISTED, + 'verb' => Activity::POST, + 'contact-id' => $contact['id'], + 'author-name' => $contact['name'], + 'author-link' => $contact['url'], + 'author-avatar' => $contact['avatar'], + 'owner-name' => $contact['name'], + 'owner-link' => $contact['url'], + 'owner-avatar' => $contact['avatar'], + 'plink' => $contact['alias'] . '/post/' . $parts->rkey, + 'source' => json_encode($post), + ]; + + if ($this->postExists($item['uri'], [$uid])) { + $this->logger->info('Post already exists for user', ['uri' => $item['uri'], 'uid' => $uid]); + return []; + } + + $account = Contact::selectAccountUserById($contact['id'], ['pid']); + + $item['owner-id'] = $item['author-id'] = $account['pid']; + $item['uri-id'] = ItemURI::getIdByURI($uri); + + if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) { + $item['post-reason'] = Item::PR_FOLLOWER; + } + + if (!empty($post->labels)) { + foreach ($post->labels as $label) { + // Only flag posts as sensitive based on labels that had been provided by the author. + // When "ver" is set to "1" it was flagged by some automated process. + if (empty($label->ver)) { + $item['sensitive'] = true; + $item['content-warning'] = $label->val ?? ''; + $this->logger->debug('Sensitive content', ['uri-id' => $item['uri-id'], 'label' => $label]); + } + } + } + + return $item; + } + + private function getContent(array $item, stdClass $record, string $uri, int $uid, int $level): array + { + if (empty($item)) { + return []; + } + + if (!empty($record->reply)) { + $item['parent-uri'] = $this->getUri($record->reply->root); + if ($item['parent-uri'] != $uri) { + $item['parent-uri'] = $this->getPostUri($item['parent-uri'], $uid); + if (empty($item['parent-uri'])) { + $this->logger->notice('Parent-uri not found', ['uri' => $this->getUri($record->reply->root)]); + return []; + } + } + + $item['thr-parent'] = $this->getUri($record->reply->parent); + if (!in_array($item['thr-parent'], [$uri, $item['parent-uri']])) { + $item['thr-parent'] = $this->getPostUri($item['thr-parent'], $uid) ?: $item['thr-parent']; + } + } + + $item['body'] = $this->parseFacets($record, $item['uri-id']); + $item['created'] = DateTimeFormat::utc($record->createdAt, DateTimeFormat::MYSQL); + $item['transmitted-languages'] = $record->langs ?? []; + + return $item; + } + + private function parseFacets(stdClass $record, int $uri_id): string + { + $text = $record->text ?? ''; + + if (empty($record->facets)) { + return $text; + } + + $facets = []; + foreach ($record->facets as $facet) { + $facets[$facet->index->byteStart] = $facet; + } + krsort($facets); + + foreach ($facets as $facet) { + $prefix = substr($text, 0, $facet->index->byteStart); + $linktext = substr($text, $facet->index->byteStart, $facet->index->byteEnd - $facet->index->byteStart); + $suffix = substr($text, $facet->index->byteEnd); + + $url = ''; + $type = '$type'; + foreach ($facet->features as $feature) { + + switch ($feature->$type) { + case 'app.bsky.richtext.facet#link': + $url = $feature->uri; + break; + + case 'app.bsky.richtext.facet#mention': + $url = $feature->did; + if (substr($linktext, 0, 1) == '@') { + $prefix .= '@'; + $linktext = substr($linktext, 1); + } + break; + + case 'app.bsky.richtext.facet#tag': + Tag::store($uri_id, Tag::HASHTAG, $feature->tag); + $url = $this->baseURL . '/search?tag=' . urlencode($feature->tag); + $linktext = '#' . $feature->tag; + break; + + default: + $this->logger->notice('Unhandled feature type', ['type' => $feature->$type, 'feature' => $feature, 'record' => $record]); + break; + } + } + if (!empty($url)) { + $text = $prefix . '[url=' . $url . ']' . $linktext . '[/url]' . $suffix; + } + } + return $text; + } + + private function addMedia(stdClass $embed, array $item, int $level): array + { + $type = '$type'; + switch ($embed->$type) { + case 'app.bsky.embed.images#view': + foreach ($embed->images as $image) { + $media = [ + 'uri-id' => $item['uri-id'], + 'type' => Post\Media::IMAGE, + 'url' => $image->fullsize, + 'preview' => $image->thumb, + 'description' => $image->alt, + 'height' => $image->aspectRatio->height ?? null, + 'width' => $image->aspectRatio->width ?? null, + ]; + Post\Media::insert($media); + } + break; + + case 'app.bsky.embed.video#view': + $media = [ + 'uri-id' => $item['uri-id'], + 'type' => Post\Media::HLS, + 'url' => $embed->playlist, + 'preview' => $embed->thumbnail, + 'description' => $embed->alt ?? '', + 'height' => $embed->aspectRatio->height ?? null, + 'width' => $embed->aspectRatio->width ?? null, + ]; + Post\Media::insert($media); + break; + + case 'app.bsky.embed.external#view': + $media = [ + 'uri-id' => $item['uri-id'], + 'type' => Post\Media::HTML, + 'url' => $embed->external->uri, + 'preview' => $embed->external->thumb ?? null, + 'name' => $embed->external->title, + 'description' => $embed->external->description, + ]; + Post\Media::insert($media); + break; + + case 'app.bsky.embed.record#view': + $original_uri = $uri = $this->getUri($embed->record); + $type = '$type'; + if (!empty($embed->record->record->$type)) { + $embed_type = $embed->record->record->$type; + if ($embed_type == 'app.bsky.graph.starterpack') { + $this->addStarterpack($item, $embed->record); + break; + } + } + $fetched_uri = $this->getPostUri($uri, $item['uid']); + if (!$fetched_uri) { + $uri = $this->fetchMissingPost($uri, 0, Item::PR_FETCHED, $item['contact-id'], $level, $uri); + } else { + $uri = $fetched_uri; + } + if ($uri) { + $shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => [$item['uid'], 0]]); + $uri_id = $shared['uri-id'] ?? 0; + } + if (!empty($uri_id)) { + $item['quote-uri-id'] = $uri_id; + } else { + $this->logger->debug('Quoted post could not be fetched', ['original-uri' => $original_uri, 'uri' => $uri]); + } + break; + + case 'app.bsky.embed.recordWithMedia#view': + $this->addMedia($embed->media, $item, $level); + $original_uri = $uri = $this->getUri($embed->record->record); + $uri = $this->fetchMissingPost($uri, 0, Item::PR_FETCHED, $item['contact-id'], $level, $uri); + if ($uri) { + $shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => [$item['uid'], 0]]); + $uri_id = $shared['uri-id'] ?? 0; + } + if (!empty($uri_id)) { + $item['quote-uri-id'] = $uri_id; + } else { + $this->logger->debug('Quoted post could not be fetched', ['original-uri' => $original_uri, 'uri' => $uri]); + } + break; + + default: + $this->logger->notice('Unhandled embed type', ['uri-id' => $item['uri-id'], 'type' => $embed->$type, 'embed' => $embed]); + break; + } + return $item; + } + + private function addStarterpack(array $item, stdClass $record) + { + $this->logger->debug('Received starterpack', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'uri' => $record->uri]); + if (!preg_match('#^at://(.+)/app.bsky.graph.starterpack/(.+)#', $record->uri, $matches)) { + return; + } + + $media = [ + 'uri-id' => $item['uri-id'], + 'type' => Post\Media::HTML, + 'url' => 'https://bsky.app/starter-pack/' . $matches[1] . '/' . $matches[2], + 'name' => $record->record->name, + 'description' => $record->record->description ?? '', + ]; + + Post\Media::insert($media); + + $fields = [ + 'name' => $record->record->name, + 'description' => $record->record->description ?? '', + ]; + Post\Media::update($fields, ['uri-id' => $media['uri-id'], 'url' => $media['url']]); + } + + private function getRestrictionsForUser(stdClass $post, array $item, int $post_reason): ?int + { + if (!empty($post->viewer->replyDisabled)) { + return Item::CANT_REPLY; + } + + if (empty($post->threadgate)) { + return null; + } + + if (!isset($post->threadgate->record->allow)) { + return null; + } + + if ($item['uid'] == 0) { + return Item::CANT_REPLY; + } + + $restrict = true; + $type = '$type'; + + foreach ($post->threadgate->record->allow as $allow) { + switch ($allow->$type) { + case 'app.bsky.feed.threadgate#followingRule': + // Only followers can reply. + if (Contact::isFollower($item['author-id'], $item['uid'])) { + $restrict = false; + } + break; + + case 'app.bsky.feed.threadgate#mentionRule': + // Only mentioned accounts can reply. + if ($post_reason == Item::PR_TO) { + $restrict = false; + } + break; + + case 'app.bsky.feed.threadgate#listRule': + // Only accounts in the provided list can reply. We don't support this at the moment. + break; + } + } + + return $restrict ? Item::CANT_REPLY : null; + } + + public function fetchMissingPost(string $uri, int $uid, int $post_reason, int $causer, int $level, string $fallback = '', bool $always_fetch = false, int $Protocol = Conversation::PARCEL_JETSTREAM): string + { + $timestamp = microtime(true); + $stamp = Strings::getRandomHex(30); + $this->logger->debug('Fetch missing post', ['uri' => $uri, 'stamp' => $stamp]); + + $fetched_uri = $this->getPostUri($uri, $uid); + if (!$always_fetch && !empty($fetched_uri)) { + return $fetched_uri; + } + + if (++$level > 100) { + $this->logger->info('Recursion level too deep', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $fallback]); + // When the level is too deep we will fallback to the parent uri. + // Allthough the threading won't be correct, we at least had stored all posts and won't try again + return $fallback; + } + + $class = $this->getUriClass($uri); + if (empty($class)) { + return $fallback; + } + + $fetch_uri = $class->uri; + + $this->logger->debug('Fetch missing post', ['level' => $level, 'uid' => $uid, 'uri' => $uri]); + $data = $this->atprotocol->XRPCGet('app.bsky.feed.getPostThread', ['uri' => $fetch_uri]); + if (empty($data) || empty($data->thread)) { + $this->logger->info('Thread was not fetched', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $fallback]); + if (microtime(true) - $timestamp > 2) { + $this->logger->debug('Not fetched', ['duration' => round(microtime(true) - $timestamp, 3), 'uri' => $uri, 'stamp' => $stamp]); + } + return $fallback; + } + + $this->logger->debug('Reply count', ['level' => $level, 'uid' => $uid, 'uri' => $uri]); + + if ($causer != 0) { + $causer = Contact::getPublicContactId($causer, $uid); + } + + if (!empty($data->thread->parent)) { + $parents = $this->fetchParents($data->thread->parent, $uid); + + if (!empty($parents)) { + if ($data->thread->post->record->reply->root->uri != $parents[0]->uri) { + $parent_uri = $this->getUri($data->thread->post->record->reply->root); + $this->fetchMissingPost($parent_uri, $uid, $post_reason, $causer, $level, $data->thread->post->record->reply->root->uri, false, $Protocol); + } + } + + foreach ($parents as $parent) { + $uri_id = $this->processPost($parent, $uid, Item::PR_FETCHED, $causer, $level, $Protocol); + $this->logger->debug('Parent created', ['uri-id' => $uri_id]); + } + } + + $uri = $this->processThread($data->thread, $uid, $post_reason, $causer, $level, $Protocol); + if (microtime(true) - $timestamp > 2) { + $this->logger->debug('Fetched and processed post', ['duration' => round(microtime(true) - $timestamp, 3), 'uri' => $uri, 'stamp' => $stamp]); + } + return $uri; + } + + private function fetchParents(stdClass $parent, int $uid, array $parents = []): array + { + if (!empty($parent->parent)) { + $parents = $this->fetchParents($parent->parent, $uid, $parents); + } + + if (!empty($parent->post) && empty($this->getPostUri($this->getUri($parent->post), $uid))) { + $parents[] = $parent->post; + } + + return $parents; + } + + private function processThread(stdClass $thread, int $uid, int $post_reason, int $causer, int $level, int $protocol): string + { + if (empty($thread->post)) { + $this->logger->info('Invalid post', ['post' => $thread]); + return ''; + } + $uri = $this->getUri($thread->post); + + $fetched_uri = $this->getPostUri($uri, $uid); + if (empty($fetched_uri)) { + $uri_id = $this->processPost($thread->post, $uid, $post_reason, $causer, $level, $protocol); + if ($uri_id) { + $this->logger->debug('Post has been processed and stored', ['uri-id' => $uri_id, 'uri' => $uri]); + return $uri; + } else { + $this->logger->info('Post has not not been stored', ['uri' => $uri]); + return ''; + } + } else { + $this->logger->debug('Post exists', ['uri' => $uri]); + $uri = $fetched_uri; + } + + foreach ($thread->replies ?? [] as $reply) { + $reply_uri = $this->processThread($reply, $uid, Item::PR_FETCHED, $causer, $level, $protocol); + $this->logger->debug('Reply has been processed', ['uri' => $uri, 'reply' => $reply_uri]); + } + + return $uri; + } + + public function getUriParts(string $uri): ?stdClass + { + $class = $this->getUriClass($uri); + if (empty($class)) { + return null; + } + + $parts = explode('/', substr($class->uri, 5)); + + $class = new stdClass(); + + $class->repo = $parts[0]; + $class->collection = $parts[1]; + $class->rkey = $parts[2]; + + return $class; + } + + public function getUriClass(string $uri): ?stdClass + { + if (empty($uri)) { + return null; + } + + $elements = explode(':', $uri); + if ($elements[0] !== 'at') { + $post = Post::selectFirstPost(['extid'], ['uri' => $uri]); + return $this->getUriClass($post['extid'] ?? ''); + } + + $class = new stdClass(); + + $class->cid = array_pop($elements); + $class->uri = implode(':', $elements); + + if ((substr_count($class->uri, '/') == 2) && (substr_count($class->cid, '/') == 2)) { + $class->uri .= ':' . $class->cid; + $class->cid = ''; + } + + return $class; + } + + public function fetchUriId(string $uri, int $uid): int + { + $reply = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => [$uid, 0]]); + if (!empty($reply['uri-id'])) { + $this->logger->debug('Post exists', ['uri' => $uri]); + return (int) $reply['uri-id']; + } + $reply = Post::selectFirst(['uri-id'], ['extid' => $uri, 'uid' => [$uid, 0]]); + if (!empty($reply['uri-id'])) { + $this->logger->debug('Post with extid exists', ['uri' => $uri]); + return (int) $reply['uri-id']; + } + return 0; + } + + private function getPostUids(string $uri, bool $with_public_user): array + { + $condition = $with_public_user ? [] : ["`uid` != ?", 0]; + + $uids = []; + $posts = Post::select(['uid'], DBA::mergeConditions(['uri' => $uri], $condition)); + while ($post = Post::fetch($posts)) { + $uids[] = $post['uid']; + } + $this->db->close($posts); + + $posts = Post::select(['uid'], DBA::mergeConditions(['extid' => $uri], $condition)); + while ($post = Post::fetch($posts)) { + $uids[] = $post['uid']; + } + $this->db->close($posts); + return array_unique($uids); + } + + private function postExists(string $uri, array $uids): bool + { + if (Post::exists(['uri' => $uri, 'uid' => $uids])) { + return true; + } + + return Post::exists(['extid' => $uri, 'uid' => $uids]); + } + + public function getUri(stdClass $post): string + { + if (empty($post->cid)) { + $this->logger->info('Invalid URI', ['post' => $post]); + return ''; + } + return $post->uri . ':' . $post->cid; + } + + public function getPostUri(string $uri, int $uid): string + { + if (Post::exists(['uri' => $uri, 'uid' => [$uid, 0]])) { + $this->logger->debug('Post exists', ['uri' => $uri]); + return $uri; + } + + $reply = Post::selectFirst(['uri'], ['extid' => $uri, 'uid' => [$uid, 0]]); + if (!empty($reply['uri'])) { + $this->logger->debug('Post with extid exists', ['uri' => $uri]); + return $reply['uri']; + } + return ''; + } +} diff --git a/src/Protocol/Activity.php b/src/Protocol/Activity.php index 36bc5e99b3..815767e5d7 100644 --- a/src/Protocol/Activity.php +++ b/src/Protocol/Activity.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; diff --git a/src/Protocol/Activity/ObjectType.php b/src/Protocol/Activity/ObjectType.php index daefb46086..28041c3726 100644 --- a/src/Protocol/Activity/ObjectType.php +++ b/src/Protocol/Activity/ObjectType.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\Activity; diff --git a/src/Protocol/ActivityNamespace.php b/src/Protocol/ActivityNamespace.php index bcbf5b4035..e02ec0a644 100644 --- a/src/Protocol/ActivityNamespace.php +++ b/src/Protocol/ActivityNamespace.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; @@ -104,6 +90,39 @@ final class ActivityNamespace * @var string */ const OSTATUSSUB = 'http://ostatus.org/schema/1.0/subscribe'; + /** + * Webfinger avatar + * + * @see https://webfinger.net/rel/#avatar + * @var string + */ + const WEBFINGERAVATAR = 'http://webfinger.net/rel/avatar'; + /** + * Webfinger profile + * + * @see https://webfinger.net/rel/#profile-page + * @var string + */ + const WEBFINGERPROFILE = 'http://webfinger.net/rel/profile-page'; + /** + * HCard + * + * @see http://microformats.org/wiki/hcard + * @var string + */ + const HCARD = 'http://microformats.org/profile/hcard'; + /** + * Base url of the Diaspora installation + * + * @var string + */ + const DIASPORA_SEED = 'http://joindiaspora.com/seed_location'; + /** + * Diaspora Guid + * + * @var string + */ + const DIASPORA_GUID = 'http://joindiaspora.com/guid'; /** * GeoRSS was designed as a lightweight, community driven way to extend existing feeds with geographic information. * @@ -120,6 +139,12 @@ final class ActivityNamespace * @var string */ const POCO = 'http://portablecontacts.net/spec/1.0'; + /** + * OpenWebAuth is used by Friendica and Hubzilla to authenticate at remote systems + * + * @var string + */ + const OPENWEBAUTH = 'http://purl.org/openwebauth/v1'; /** * @var string */ diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 09464a13c2..405d230341 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\DI; @@ -62,30 +47,36 @@ use Friendica\Util\JsonLD; class ActivityPub { const PUBLIC_COLLECTION = 'https://www.w3.org/ns/activitystreams#Public'; - const CONTEXT = [ + const CONTEXT = [ 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', [ - 'vcard' => 'http://www.w3.org/2006/vcard/ns#', - 'dfrn' => 'http://purl.org/macgirvin/dfrn/1.0/', + 'ostatus' => 'http://ostatus.org#', + 'vcard' => 'http://www.w3.org/2006/vcard/ns#', + 'dfrn' => 'http://purl.org/macgirvin/dfrn/1.0/', 'diaspora' => 'https://diasporafoundation.org/ns/', - 'litepub' => 'http://litepub.social/ns#', - 'toot' => 'http://joinmastodon.org/ns#', + 'litepub' => 'http://litepub.social/ns#', + 'toot' => 'http://joinmastodon.org/ns#', 'featured' => [ - "@id" => "toot:featured", + "@id" => "toot:featured", "@type" => "@id", ], - 'schema' => 'http://schema.org#', + 'schema' => 'http://schema.org#', 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'sensitive' => 'as:sensitive', 'Hashtag' => 'as:Hashtag', - 'quoteUrl' => 'as:quoteUrl', - 'conversation' => 'ostatus:conversation', - 'directMessage' => 'litepub:directMessage', - 'discoverable' => 'toot:discoverable', - 'PropertyValue' => 'schema:PropertyValue', - 'value' => 'schema:value', + 'sensitive' => 'as:sensitive', 'Hashtag' => 'as:Hashtag', + 'quoteUrl' => 'as:quoteUrl', + 'conversation' => 'ostatus:conversation', + 'directMessage' => 'litepub:directMessage', + 'discoverable' => 'toot:discoverable', + 'PropertyValue' => 'schema:PropertyValue', + 'value' => 'schema:value', ] ]; const ACCOUNT_TYPES = ['Person', 'Organization', 'Service', 'Group', 'Application', 'Tombstone']; + + const ARTICLE_DEFAULT = 0; + const ARTICLE_USE_SUMMARY = 1; + const ARTICLE_EMBED_TITLE = 2; + /** * Checks if the web request is done for the AP protocol * @@ -94,16 +85,17 @@ class ActivityPub public static function isRequest(): bool { header('Vary: Accept', false); - - $isrequest = stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/activity+json') || - stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json') || - stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/ld+json'); - - if ($isrequest) { - Logger::debug('Is AP request', ['accept' => $_SERVER['HTTP_ACCEPT'], 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); + if (stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/activity+json') || stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/ld+json')) { + DI::logger()->debug('Is AP request', ['accept' => $_SERVER['HTTP_ACCEPT'], 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); + return true; } - return $isrequest; + if (stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json')) { + DI::logger()->debug('Is JSON request', ['accept' => $_SERVER['HTTP_ACCEPT'], 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); + return true; + } + + return false; } private static function getAccountType(array $apcontact): int @@ -150,35 +142,35 @@ class ActivityPub return []; } - $profile = ['network' => Protocol::ACTIVITYPUB]; - $profile['nick'] = $apcontact['nick']; - $profile['name'] = $apcontact['name']; - $profile['guid'] = $apcontact['uuid']; - $profile['url'] = $apcontact['url']; - $profile['addr'] = $apcontact['addr']; - $profile['alias'] = $apcontact['alias']; - $profile['following'] = $apcontact['following']; - $profile['followers'] = $apcontact['followers']; - $profile['inbox'] = $apcontact['inbox']; - $profile['outbox'] = $apcontact['outbox']; - $profile['sharedinbox'] = $apcontact['sharedinbox']; - $profile['photo'] = $apcontact['photo']; - $profile['header'] = $apcontact['header']; + $profile = ['network' => Protocol::ACTIVITYPUB]; + $profile['nick'] = $apcontact['nick']; + $profile['name'] = $apcontact['name']; + $profile['guid'] = $apcontact['uuid']; + $profile['url'] = $apcontact['url']; + $profile['addr'] = $apcontact['addr']; + $profile['alias'] = $apcontact['alias']; + $profile['following'] = $apcontact['following']; + $profile['followers'] = $apcontact['followers']; + $profile['inbox'] = $apcontact['inbox']; + $profile['outbox'] = $apcontact['outbox']; + $profile['sharedinbox'] = $apcontact['sharedinbox']; + $profile['photo'] = $apcontact['photo']; + $profile['header'] = $apcontact['header']; $profile['account-type'] = self::getAccountType($apcontact); - $profile['community'] = ($profile['account-type'] == User::ACCOUNT_TYPE_COMMUNITY); + $profile['community'] = ($profile['account-type'] == User::ACCOUNT_TYPE_COMMUNITY); // $profile['keywords'] // $profile['location'] - $profile['about'] = $apcontact['about']; - $profile['xmpp'] = $apcontact['xmpp']; - $profile['matrix'] = $apcontact['matrix']; - $profile['batch'] = $apcontact['sharedinbox']; - $profile['notify'] = $apcontact['inbox']; - $profile['poll'] = $apcontact['outbox']; - $profile['pubkey'] = $apcontact['pubkey']; - $profile['subscribe'] = $apcontact['subscribe']; + $profile['about'] = $apcontact['about']; + $profile['xmpp'] = $apcontact['xmpp']; + $profile['matrix'] = $apcontact['matrix']; + $profile['batch'] = $apcontact['sharedinbox']; + $profile['notify'] = $apcontact['inbox']; + $profile['poll'] = $apcontact['outbox']; + $profile['pubkey'] = $apcontact['pubkey']; + $profile['subscribe'] = $apcontact['subscribe']; $profile['manually-approve'] = $apcontact['manually-approve']; - $profile['baseurl'] = $apcontact['baseurl']; - $profile['gsid'] = $apcontact['gsid']; + $profile['baseurl'] = $apcontact['baseurl']; + $profile['gsid'] = $apcontact['gsid']; if (!is_null($apcontact['discoverable'])) { $profile['hide'] = !$apcontact['discoverable']; @@ -239,7 +231,7 @@ class ActivityPub $start_timestamp = $start_timestamp ?: time(); if ((time() - $start_timestamp) > 60) { - Logger::info('Fetch time limit reached', ['url' => $url, 'uid' => $uid]); + DI::logger()->info('Fetch time limit reached', ['url' => $url, 'uid' => $uid]); return []; } @@ -252,13 +244,19 @@ class ActivityPub $items = $data['orderedItems']; } elseif (!empty($data['first']['orderedItems'])) { $items = $data['first']['orderedItems']; + } elseif (!empty($data['items'])) { + $items = $data['items']; + } elseif (!empty($data['first']['items'])) { + $items = $data['first']['items']; } elseif (!empty($data['first']) && is_string($data['first']) && ($data['first'] != $url)) { return self::fetchItems($data['first'], $uid, $start_timestamp); } else { return []; } - if (!empty($data['next']) && is_string($data['next'])) { + if (!empty($data['first']['next']) && is_string($data['first']['next'])) { + $items = array_merge($items, self::fetchItems($data['first']['next'], $uid, $start_timestamp)); + } elseif (!empty($data['next']) && is_string($data['next'])) { $items = array_merge($items, self::fetchItems($data['next'], $uid, $start_timestamp)); } @@ -285,39 +283,41 @@ class ActivityPub $signer = HTTPSignature::getSigner('', $_SERVER); if (!$signer) { - Logger::debug('No signer or invalid signature', ['uid' => $uid, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'called_by' => $called_by]); + DI::logger()->debug('No signer or invalid signature', ['uid' => $uid, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'called_by' => $called_by]); return false; } $apcontact = APContact::getByURL($signer); if (empty($apcontact)) { - Logger::info('APContact not found', ['uid' => $uid, 'handle' => $signer, 'called_by' => $called_by]); + DI::logger()->info('APContact not found', ['uid' => $uid, 'handle' => $signer, 'called_by' => $called_by]); return false; } - if (empty($apcontact['gsid'] || empty($apcontact['baseurl']))) { - Logger::debug('No server found', ['uid' => $uid, 'signer' => $signer, 'called_by' => $called_by]); + if (empty($apcontact['gsid']) || empty($apcontact['baseurl'])) { + DI::logger()->debug('No server found', ['uid' => $uid, 'signer' => $signer, 'called_by' => $called_by]); return false; } $contact = Contact::getByURL($signer, false, ['id', 'baseurl', 'gsid']); if (!empty($contact) && Contact\User::isBlocked($contact['id'], $uid)) { - Logger::info('Requesting contact is blocked', ['uid' => $uid, 'id' => $contact['id'], 'signer' => $signer, 'baseurl' => $contact['baseurl'], 'called_by' => $called_by]); + DI::logger()->info('Requesting contact is blocked', ['uid' => $uid, 'id' => $contact['id'], 'signer' => $signer, 'baseurl' => $contact['baseurl'], 'called_by' => $called_by]); return false; } $limited = DI::config()->get('system', 'limited_servers'); if (!empty($limited)) { $servers = explode(',', str_replace(' ', '', $limited)); - $host = parse_url($apcontact['baseurl'], PHP_URL_HOST); + $host = parse_url($apcontact['baseurl'], PHP_URL_HOST); if (!empty($host) && in_array($host, $servers)) { return false; } } - // @todo Look for user blocked domains + if (DI::userGServer()->isIgnoredByUser($uid, $apcontact['gsid'])) { + return false; + } - Logger::debug('Server is an accepted requester', ['uid' => $uid, 'id' => $apcontact['gsid'], 'url' => $apcontact['baseurl'], 'signer' => $signer, 'called_by' => $called_by]); + DI::logger()->debug('Server is an accepted requester', ['uid' => $uid, 'id' => $apcontact['gsid'], 'url' => $apcontact['baseurl'], 'signer' => $signer, 'called_by' => $called_by]); return true; } diff --git a/src/Protocol/ActivityPub/ClientToServer.php b/src/Protocol/ActivityPub/ClientToServer.php index 8bc4dcc059..cab58075f0 100644 --- a/src/Protocol/ActivityPub/ClientToServer.php +++ b/src/Protocol/ActivityPub/ClientToServer.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; use Friendica\Content\Text\Markdown; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Database\DBA; use Friendica\DI; @@ -53,24 +38,24 @@ class ClientToServer { $ldactivity = JsonLD::compact($activity); if (empty($ldactivity)) { - Logger::notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]); + DI::logger()->notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]); return []; } $type = JsonLD::fetchElement($ldactivity, '@type'); if (!$type) { - Logger::notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]); + DI::logger()->notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]); return []; } $object_id = JsonLD::fetchElement($ldactivity, 'as:object', '@id') ?? ''; $object_type = Receiver::fetchObjectType($ldactivity, $object_id, $uid); if (!$object_type && !$object_id) { - Logger::notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]); + DI::logger()->notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]); return []; } - Logger::debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]); + DI::logger()->debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]); return self::routeActivities($type, $object_type, $object_id, $uid, $application, $ldactivity); } @@ -119,7 +104,7 @@ class ClientToServer { $object_data = self::processObject($ldactivity['as:object']); $item = ClientToServer::processContent($object_data, $application, $uid); - Logger::debug('Got data', ['item' => $item, 'object' => $object_data]); + DI::logger()->debug('Got data', ['item' => $item, 'object' => $object_data]); $id = Item::insert($item, true); if (!empty($id)) { @@ -145,18 +130,18 @@ class ClientToServer $id = Item::fetchByLink($object_id, $uid, ActivityPub\Receiver::COMPLETION_ASYNC); $original_post = Post::selectFirst(['uri-id'], ['uid' => $uid, 'origin' => true, 'id' => $id]); if (empty($original_post)) { - Logger::debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); + DI::logger()->debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); return []; } $object_data = self::processObject($ldactivity['as:object']); $item = ClientToServer::processContent($object_data, $application, $uid); if (empty($item['title']) && empty($item['body'])) { - Logger::debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); + DI::logger()->debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); return []; } $post = ['title' => $item['title'], 'body' => $item['body']]; - Logger::debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]); + DI::logger()->debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]); Item::update($post, ['id' => $id]); Item::updateDisplayCache($original_post['uri-id']); @@ -265,7 +250,7 @@ class ClientToServer $item['uid'] = $uid; $item['verb'] = Activity::POST; $item['contact-id'] = $owner['id']; - $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); + $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); $item['title'] = $object_data['name']; $item['body'] = Markdown::toBBCode($object_data['content'] ?? ''); $item['app'] = $application['name'] ?? 'API'; @@ -321,7 +306,6 @@ class ClientToServer * @param integer $page Page number * @param integer $max_id Maximum ID * @param string $requester URL of requesting account - * @param boolean $nocache Wether to bypass caching * @return array of posts * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException @@ -337,9 +321,11 @@ class ClientToServer $requester_id = Contact::getIdForURL($requester, $owner['uid']); if (!empty($requester_id)) { $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']); - if (!empty($permissionSets)) { - $condition = ['psid' => array_merge($permissionSets->column('id'), - [DI::permissionSet()->selectPublicForUser($owner['uid'])])]; + if (count($permissionSets) > 0) { + $condition = ['psid' => array_merge( + $permissionSets->column('id'), + [DI::permissionSet()->selectPublicForUser($owner['uid'])] + )]; } } } diff --git a/src/Protocol/ActivityPub/Delivery.php b/src/Protocol/ActivityPub/Delivery.php index 04f5841c2e..9e243ed1c6 100644 --- a/src/Protocol/ActivityPub/Delivery.php +++ b/src/Protocol/ActivityPub/Delivery.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; @@ -56,13 +41,15 @@ class Delivery continue; } + $result = []; + if (!$serverfail) { $result = self::deliverToInbox($post['command'], 0, $inbox, $owner, $post['receivers'], $post['uri-id']); if ($result['serverfailure']) { // In a timeout situation we assume that every delivery to that inbox will time out. // So we set the flag and try all deliveries at a later time. - Logger::notice('Inbox delivery has a server failure', ['inbox' => $inbox]); + DI::logger()->notice('Inbox delivery has a server failure', ['inbox' => $inbox]); $serverfail = true; } Worker::coolDown(); @@ -73,7 +60,7 @@ class Delivery } } - Logger::debug('Inbox delivery done', ['inbox' => $inbox, 'posts' => count($posts), 'failed' => count($uri_ids), 'serverfailure' => $serverfail]); + DI::logger()->debug('Inbox delivery done', ['inbox' => $inbox, 'posts' => count($posts), 'failed' => count($uri_ids), 'serverfailure' => $serverfail]); return ['success' => empty($uri_ids), 'uri_ids' => $uri_ids]; } @@ -96,11 +83,11 @@ class Delivery if (empty($item_id) && !empty($uri_id) && !empty($uid)) { $item = Post::selectFirst(['id', 'parent', 'origin', 'gravity', 'verb'], ['uri-id' => $uri_id, 'uid' => [$uid, 0]], ['order' => ['uid' => true]]); if (empty($item['id'])) { - Logger::warning('Item not found, removing delivery', ['uri-id' => $uri_id, 'uid' => $uid, 'cmd' => $cmd, 'inbox' => $inbox]); + DI::logger()->warning('Item not found, removing delivery', ['uri-id' => $uri_id, 'uid' => $uid, 'cmd' => $cmd, 'inbox' => $inbox]); Post\Delivery::remove($uri_id, $inbox); return ['success' => true, 'serverfailure' => false, 'drop' => false]; } elseif (!DI::config()->get('system', 'redistribute_activities') && !$item['origin'] && ($item['gravity'] == Item::GRAVITY_ACTIVITY)) { - Logger::notice('Activities are not relayed, removing delivery', ['verb' => $item['verb'], 'uri-id' => $uri_id, 'uid' => $uid, 'cmd' => $cmd, 'inbox' => $inbox]); + DI::logger()->notice('Activities are not relayed, removing delivery', ['verb' => $item['verb'], 'uri-id' => $uri_id, 'uid' => $uid, 'cmd' => $cmd, 'inbox' => $inbox]); Post\Delivery::remove($uri_id, $inbox); return ['success' => true, 'serverfailure' => false, 'drop' => false]; } else { @@ -128,12 +115,19 @@ class Delivery } else { $data = ActivityPub\Transmitter::createCachedActivityFromItem($item_id); if (!empty($data)) { - $timestamp = microtime(true); - $response = HTTPSignature::post($data, $inbox, $owner); - $runtime = microtime(true) - $timestamp; - $success = $response->isSuccess(); - $serverfail = $response->isTimeout(); - if (!$success) { + $timestamp = microtime(true); + try { + $response = HTTPSignature::post($data, $inbox, $owner); + $success = $response->isSuccess(); + $serverfail = $response->isTimeout(); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + $response = null; + $success = false; + $serverfail = true; + } + $runtime = microtime(true) - $timestamp; + if ($success === false) { // 5xx errors are problems on the server. We don't need to continue delivery then. if (!$serverfail && ($response->getReturnCode() >= 500) && ($response->getReturnCode() <= 599)) { $serverfail = true; @@ -156,10 +150,10 @@ class Delivery // Resubscribe to relay server upon client error if (!$serverfail && ($response->getReturnCode() >= 400) && ($response->getReturnCode() <= 499)) { - $actor = self:: fetchActorForRelayInbox($inbox); + $actor = self::fetchActorForRelayInbox($inbox); if (!empty($actor)) { $drop = !ActivityPub\Transmitter::sendRelayFollow($actor); - Logger::notice('Resubscribed to relay', ['url' => $actor, 'success' => !$drop]); + DI::logger()->notice('Resubscribed to relay', ['url' => $actor, 'success' => !$drop]); } elseif ($cmd == ProtocolDelivery::DELETION) { // Remote systems not always accept our deletion requests, so we drop them if rejected. // Situation is: In Friendica we allow the thread owner to delete foreign comments to their thread. @@ -169,7 +163,7 @@ class Delivery } - Logger::notice('Delivery failed', ['retcode' => $response->getReturnCode(), 'serverfailure' => $serverfail, 'drop' => $drop, 'runtime' => round($runtime, 3), 'uri-id' => $uri_id, 'uid' => $uid, 'item_id' => $item_id, 'cmd' => $cmd, 'inbox' => $inbox]); + DI::logger()->notice('Delivery failed', ['retcode' => $response->getReturnCode() ?? 0, 'serverfailure' => $serverfail, 'drop' => $drop, 'runtime' => round($runtime, 3), 'uri-id' => $uri_id, 'uid' => $uid, 'item_id' => $item_id, 'cmd' => $cmd, 'inbox' => $inbox]); } if ($uri_id) { if ($success) { @@ -185,7 +179,7 @@ class Delivery self::setSuccess($receivers, $success); - Logger::debug('Delivered', ['uri-id' => $uri_id, 'uid' => $uid, 'item_id' => $item_id, 'cmd' => $cmd, 'inbox' => $inbox, 'success' => $success, 'serverfailure' => $serverfail, 'drop' => $drop]); + DI::logger()->debug('Delivered', ['uri-id' => $uri_id, 'uid' => $uid, 'item_id' => $item_id, 'cmd' => $cmd, 'inbox' => $inbox, 'success' => $success, 'serverfailure' => $serverfail, 'drop' => $drop]); if (($success || $drop) && in_array($cmd, [ProtocolDelivery::POST])) { Post\DeliveryData::incrementQueueDone($uri_id, Post\DeliveryData::ACTIVITYPUB); diff --git a/src/Protocol/ActivityPub/Fetch.php b/src/Protocol/ActivityPub/Fetch.php index 48234d509d..3c787981ab 100644 --- a/src/Protocol/ActivityPub/Fetch.php +++ b/src/Protocol/ActivityPub/Fetch.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; -use Friendica\Core\Logger; use Friendica\Database\Database; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Util\DateTimeFormat; /** @@ -36,15 +22,13 @@ class Fetch DBA::insert('fetch-entry', ['url' => $url, 'created' => DateTimeFormat::utcNow()], Database::INSERT_IGNORE); $fetch = DBA::selectFirst('fetch-entry', ['id'], ['url' => $url]); - Logger::debug('Added fetch entry', ['url' => $url, 'fetch' => $fetch]); + DI::logger()->debug('Added fetch entry', ['url' => $url, 'fetch' => $fetch]); return $fetch['id'] ?? 0; } /** * Set the worker id for the queue entry * - * @param array $activity - * @param int $wid * @return void */ public static function setWorkerId(string $url, int $wid) @@ -54,27 +38,24 @@ class Fetch } DBA::update('fetch-entry', ['wid' => $wid], ['url' => $url]); - Logger::debug('Worker id set', ['url' => $url, 'wid' => $wid]); + DI::logger()->debug('Worker id set', ['url' => $url, 'wid' => $wid]); } /** * Check if there is an assigned worker task - * - * @param array $activity - * @return bool */ public static function hasWorker(string $url): bool { $fetch = DBA::selectFirst('fetch-entry', ['id', 'wid'], ['url' => $url]); if (empty($fetch['id'])) { - Logger::debug('No entry found for url', ['url' => $url]); + DI::logger()->debug('No entry found for url', ['url' => $url]); return false; } // We don't have a workerqueue id yet. So most likely is isn't assigned yet. // To avoid the ramping up of another fetch request we simply claim that there is a waiting worker. if (!empty($fetch['id']) && empty($fetch['wid'])) { - Logger::debug('Entry without worker found for url', ['url' => $url]); + DI::logger()->debug('Entry without worker found for url', ['url' => $url]); return true; } diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 3c0d57caa5..b96486ef3a 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; @@ -25,7 +11,6 @@ use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Content\Text\Markdown; use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Core\Worker; @@ -108,7 +93,7 @@ class Processor private static function processLanguages(array $languages): string { $codes = array_keys($languages); - $lang = []; + $lang = []; foreach ($codes as $code) { $lang[$code] = 1; } @@ -130,7 +115,8 @@ class Processor */ private static function replaceEmojis(int $uri_id, string $body, array $emojis): string { - $body = strtr($body, + $body = strtr( + $body, array_combine( array_column($emojis, 'name'), array_map(function ($emoji) { @@ -159,15 +145,15 @@ class Processor return; } - $data = ['uri-id' => $uriid]; - $data['type'] = Post\Media::UNKNOWN; - $data['url'] = $attachment['url']; - $data['mimetype'] = $attachment['mediaType'] ?? null; - $data['height'] = $attachment['height'] ?? null; - $data['width'] = $attachment['width'] ?? null; - $data['size'] = $attachment['size'] ?? null; - $data['preview'] = $attachment['image'] ?? null; - $data['description'] = $attachment['name'] ?? null; + $data = ['uri-id' => $uriid]; + $data['type'] = Post\Media::UNKNOWN; + $data['url'] = $attachment['url']; + $data['mimetype'] = $attachment['mediaType'] ?? null; + $data['height'] = $attachment['height'] ?? null; + $data['width'] = $attachment['width'] ?? null; + $data['size'] = $attachment['size'] ?? null; + $data['preview'] = $attachment['image'] ?? null; + $data['description'] = $attachment['name'] ?? null; Post\Media::insert($data); } @@ -217,7 +203,7 @@ class Processor Post\QuestionOption::update($item['uri-id'], $key, $option); } - Logger::debug('Storing incoming question', ['type' => $activity['type'], 'uri-id' => $item['uri-id'], 'question' => $activity['question']]); + DI::logger()->debug('Storing incoming question', ['type' => $activity['type'], 'uri-id' => $item['uri-id'], 'question' => $activity['question']]); } /** @@ -229,9 +215,9 @@ class Processor */ public static function updateItem(array $activity) { - $item = Post::selectFirst(['uri', 'uri-id', 'thr-parent', 'gravity', 'post-type', 'private'], ['uri' => $activity['id']]); + $item = Post::selectFirst(['uri', 'uri-id', 'guid', 'thr-parent', 'gravity', 'post-type', 'private'], ['uri' => $activity['id']]); if (!DBA::isResult($item)) { - Logger::notice('No existing item, item will be created', ['uri' => $activity['id']]); + DI::logger()->notice('No existing item, item will be created', ['uri' => $activity['id']]); $item = self::createItem($activity, false); if (empty($item)) { Queue::remove($activity); @@ -243,7 +229,7 @@ class Processor } $item['changed'] = DateTimeFormat::utcNow(); - $item['edited'] = DateTimeFormat::utc($activity['updated']); + $item['edited'] = DateTimeFormat::utc($activity['updated']); Post\Media::deleteByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::VIDEO, Post\Media::IMAGE, Post\Media::HTML]); $item = self::processContent($activity, $item); @@ -266,6 +252,7 @@ class Processor self::updateEvent($post['event-id'], $activity); } } + self::processReplies($activity, $item); } /** @@ -278,11 +265,11 @@ class Processor { $event = DBA::selectFirst('event', [], ['id' => $event_id]); - $event['edited'] = DateTimeFormat::utc($activity['updated']); - $event['summary'] = HTML::toBBCode($activity['name']); - $event['desc'] = HTML::toBBCode($activity['content']); + $event['edited'] = DateTimeFormat::utc($activity['updated']); + $event['summary'] = HTML::toBBCode($activity['name']); + $event['desc'] = HTML::toBBCode($activity['content']); if (!empty($activity['start-time'])) { - $event['start'] = DateTimeFormat::utc($activity['start-time']); + $event['start'] = DateTimeFormat::utc($activity['start-time']); } if (!empty($activity['end-time'])) { $event['finish'] = DateTimeFormat::utc($activity['end-time']); @@ -290,7 +277,7 @@ class Processor $event['nofinish'] = empty($event['finish']); $event['location'] = $activity['location']; - Logger::info('Updating event', ['uri' => $activity['id'], 'id' => $event_id]); + DI::logger()->info('Updating event', ['uri' => $activity['id'], 'id' => $event_id]); Event::store($event); } @@ -307,38 +294,47 @@ class Processor */ public static function createItem(array $activity, bool $fetch_parents): array { - $item = []; - $item['verb'] = Activity::POST; + $item = []; + $item['verb'] = Activity::POST; $item['thr-parent'] = $activity['reply-to-id']; if ($activity['reply-to-id'] == $activity['id']) { - $item['gravity'] = Item::GRAVITY_PARENT; + $item['gravity'] = Item::GRAVITY_PARENT; $item['object-type'] = Activity\ObjectType::NOTE; } else { - $item['gravity'] = Item::GRAVITY_COMMENT; + $item['gravity'] = Item::GRAVITY_COMMENT; $item['object-type'] = Activity\ObjectType::COMMENT; } + if (!empty($activity['context'])) { + $item['context'] = $activity['context']; + } + if (!empty($activity['conversation'])) { $item['conversation'] = $activity['conversation']; - } elseif (!empty($activity['context'])) { - $item['conversation'] = $activity['context']; } - if (!empty($item['conversation'])) { + if (!empty($item['context'])) { + $conversation = Post::selectFirstThread(['uri'], ['context' => $item['context']]); + if (!empty($conversation)) { + DI::logger()->debug('Got context', ['context' => $item['context'], 'parent' => $conversation]); + $item['parent-uri'] = $conversation['uri']; + $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); + } + } elseif (!empty($item['conversation'])) { $conversation = Post::selectFirstThread(['uri'], ['conversation' => $item['conversation']]); if (!empty($conversation)) { - Logger::debug('Got conversation', ['conversation' => $item['conversation'], 'parent' => $conversation]); - $item['parent-uri'] = $conversation['uri']; + DI::logger()->debug('Got conversation', ['conversation' => $item['conversation'], 'parent' => $conversation]); + $item['parent-uri'] = $conversation['uri']; $item['parent-uri-id'] = ItemURI::getIdByURI($item['parent-uri']); } } else { $conversation = []; } - Logger::debug('Create Item', ['id' => $activity['id'], 'conversation' => $item['conversation'] ?? '']); + DI::logger()->debug('Create Item', ['id' => $activity['id'], 'conversation' => $item['conversation'] ?? '']); if (empty($activity['author']) && empty($activity['actor'])) { - Logger::notice('Missing author and actor. We quit here.', ['activity' => $activity]); + DI::logger()->notice('Missing author and actor. We quit here.', ['activity' => $activity]); Queue::remove($activity); return []; } @@ -357,18 +353,18 @@ class Processor $item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? ''; if (empty($conversation) && empty($activity['directmessage']) && ($item['gravity'] != Item::GRAVITY_PARENT) && !Post::exists(['uri' => $item['thr-parent']])) { - Logger::notice('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]); + DI::logger()->notice('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]); if (!$fetch_parents) { Queue::remove($activity); } return []; } - $item['network'] = Protocol::ACTIVITYPUB; + $item['network'] = Protocol::ACTIVITYPUB; $item['author-link'] = $activity['author']; - $item['author-id'] = Contact::getIdForURL($activity['author']); - $item['owner-link'] = $activity['actor']; - $item['owner-id'] = Contact::getIdForURL($activity['actor']); + $item['author-id'] = Contact::getIdForURL($activity['author']); + $item['owner-link'] = $activity['actor']; + $item['owner-id'] = Contact::getIdForURL($activity['actor']); if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) { $item['private'] = Item::UNLISTED; @@ -416,15 +412,15 @@ class Processor if (!empty($activity['thread-completion'])) { if ($activity['thread-completion'] != $item['owner-id']) { - $actor = Contact::getById($activity['thread-completion'], ['url']); + $actor = Contact::getById($activity['thread-completion'], ['url']); $item['causer-link'] = $actor['url']; $item['causer-id'] = $activity['thread-completion']; - Logger::info('Use inherited actor as causer.', ['id' => $item['owner-id'], 'activity' => $activity['thread-completion'], 'owner' => $item['owner-link'], 'actor' => $actor['url']]); + DI::logger()->info('Use inherited actor as causer.', ['id' => $item['owner-id'], 'activity' => $activity['thread-completion'], 'owner' => $item['owner-link'], 'actor' => $actor['url']]); } else { // Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts $item['causer-link'] = $item['owner-link']; $item['causer-id'] = $item['owner-id']; - Logger::info('Use actor as causer.', ['id' => $item['owner-id'], 'actor' => $item['owner-link']]); + DI::logger()->info('Use actor as causer.', ['id' => $item['owner-id'], 'actor' => $item['owner-link']]); } $item['owner-link'] = $item['author-link']; @@ -435,7 +431,7 @@ class Processor foreach ($activity['receiver_urls']['as:audience'] as $audience) { $actor = APContact::getByURL($audience, false); if (($actor['type'] ?? 'Person') == 'Group') { - Logger::debug('Group post detected via audience.', ['audience' => $audience, 'actor' => $activity['actor'], 'author' => $activity['author']]); + DI::logger()->debug('Group post detected via audience.', ['audience' => $audience, 'actor' => $activity['actor'], 'author' => $activity['author']]); $item['isGroup'] = true; $item['group-link'] = $item['owner-link'] = $audience; $item['owner-id'] = Contact::getIdForURL($audience); @@ -447,7 +443,7 @@ class Processor } if (!$item['isGroup'] && (($owner['type'] ?? 'Person') == 'Group')) { - Logger::debug('Group post detected via owner.', ['actor' => $activity['actor'], 'author' => $activity['author']]); + DI::logger()->debug('Group post detected via owner.', ['actor' => $activity['actor'], 'author' => $activity['author']]); $item['isGroup'] = true; $item['group-link'] = $item['owner-link']; } elseif (!empty($item['causer-link'])) { @@ -455,7 +451,7 @@ class Processor } if (!$item['isGroup'] && (($causer['type'] ?? 'Person') == 'Group')) { - Logger::debug('Group post detected via causer.', ['actor' => $activity['actor'], 'author' => $activity['author'], 'causer' => $item['causer-link']]); + DI::logger()->debug('Group post detected via causer.', ['actor' => $activity['actor'], 'author' => $activity['author'], 'causer' => $item['causer-link']]); $item['isGroup'] = true; $item['group-link'] = $item['causer-link']; } @@ -465,20 +461,21 @@ class Processor $item['causer-id'] = Contact::getIdForURL($item['causer-link']); } - $item['uri'] = $activity['id']; + $item['uri'] = $activity['id']; + $item['sensitive'] = $activity['sensitive']; if (empty($activity['published']) || empty($activity['updated'])) { DI::logger()->notice('published or updated keys are empty for activity', ['activity' => $activity]); } $item['created'] = DateTimeFormat::utc($activity['published'] ?? 'now'); - $item['edited'] = DateTimeFormat::utc($activity['updated'] ?? 'now'); - $guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']); - $item['guid'] = $activity['diaspora:guid'] ?: $guid; + $item['edited'] = DateTimeFormat::utc($activity['updated'] ?? 'now'); + $guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']); + $item['guid'] = $activity['diaspora:guid'] ?: $guid; $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); if (empty($item['uri-id'])) { - Logger::warning('Unable to get a uri-id for an item uri', ['uri' => $item['uri'], 'guid' => $item['guid']]); + DI::logger()->warning('Unable to get a uri-id for an item uri', ['uri' => $item['uri'], 'guid' => $item['guid']]); return []; } @@ -486,13 +483,17 @@ class Processor $item = self::processContent($activity, $item); if (empty($item)) { - Logger::info('Message was not processed'); + DI::logger()->info('Message was not processed'); Queue::remove($activity); return []; } $item['plink'] = $activity['alternate-url'] ?? $item['uri']; + if (!empty($activity['replies'])) { + $item['replies'] = $activity['replies']; + } + self::storeAttachments($activity, $item); self::storeQuestion($activity, $item); @@ -512,6 +513,33 @@ class Processor return $item; } + private static function processReplies(array $activity, array $item) + { + // @todo fetch replies not only in the decoupled mode + if (!DI::config()->get('system', 'decoupled_receiver')) { + return; + } + + $replies = [$item['thr-parent']]; + if (!empty($item['parent-uri'])) { + $replies[] = $item['parent-uri']; + } + $condition = DBA::mergeConditions(['uri' => $replies], ["`replies-id` IS NOT NULL"]); + $posts = Post::select(['replies', 'replies-id'], $condition); + while ($post = Post::fetch($posts)) { + $cachekey = 'Processor-CreateItem-Replies-' . $post['replies-id']; + if (!DI::cache()->get($cachekey)) { + self::fetchReplies($post['replies'], $activity); + DI::cache()->set($cachekey, true); + } + } + DBA::close($replies); + + if (!empty($item['replies'])) { + self::fetchReplies($item['replies'], $activity); + } + } + /** * Fetch and process parent posts for the given activity * @@ -522,8 +550,15 @@ class Processor */ private static function fetchParent(array $activity, bool $in_background = false): string { + $activity['callstack'] = self::addToCallstack($activity['callstack'] ?? []); + if (self::isFetched($activity['reply-to-id'])) { - Logger::info('Id is already fetched', ['id' => $activity['reply-to-id']]); + DI::logger()->info('Id is already fetched', ['id' => $activity['reply-to-id']]); + return ''; + } + + if (in_array($activity['reply-to-id'], $activity['children'] ?? [])) { + DI::logger()->notice('reply-to-id is already in the list of children', ['id' => $activity['reply-to-id'], 'children' => $activity['children'], 'depth' => count($activity['children'])]); return ''; } @@ -538,20 +573,20 @@ class Processor $recursion_depth = $activity['recursion-depth'] ?? 0; if (!$in_background && ($recursion_depth < DI::config()->get('system', 'max_recursion_depth'))) { - Logger::info('Parent not found. Try to refetch it.', ['completion' => $completion, 'recursion-depth' => $recursion_depth, 'parent' => $activity['reply-to-id']]); + DI::logger()->info('Parent not found. Try to refetch it.', ['completion' => $completion, 'recursion-depth' => $recursion_depth, 'parent' => $activity['reply-to-id']]); $result = self::fetchMissingActivity($activity['reply-to-id'], $activity, '', Receiver::COMPLETION_AUTO); if (empty($result) && self::isActivityGone($activity['reply-to-id'])) { - Logger::notice('The activity is gone, the queue entry will be deleted', ['parent' => $activity['reply-to-id']]); + DI::logger()->notice('The activity is gone, the queue entry will be deleted', ['parent' => $activity['reply-to-id']]); if (!empty($activity['entry-id'])) { Queue::deleteById($activity['entry-id']); } } elseif (!empty($result)) { $post = Post::selectFirstPost(['uri'], ['uri' => [$result, $activity['reply-to-id']]]); if (!empty($post['uri'])) { - Logger::info('The activity has been fetched and created.', ['result' => $result, 'uri' => $post['uri']]); + DI::logger()->info('The activity has been fetched and created.', ['result' => $result, 'uri' => $post['uri']]); return $post['uri']; } else { - Logger::notice('The activity exists but has not been created, the queue entry will be deleted.', ['parent' => $result]); + DI::logger()->notice('The activity exists but has not been created, the queue entry will be deleted.', ['parent' => $result]); if (!empty($activity['entry-id'])) { Queue::deleteById($activity['entry-id']); } @@ -559,7 +594,7 @@ class Processor } return ''; } elseif (self::isActivityGone($activity['reply-to-id'])) { - Logger::notice('The activity is gone. We will not spawn a worker. The queue entry will be deleted', ['parent' => $activity['reply-to-id']]); + DI::logger()->notice('The activity is gone. We will not spawn a worker. The queue entry will be deleted', ['parent' => $activity['reply-to-id']]); if ($in_background) { // fetching in background is done for all activities where we have got the conversation // There we only delete the single activity and not the whole thread since we can store the @@ -570,19 +605,19 @@ class Processor } return ''; } elseif ($in_background) { - Logger::notice('Fetching is done in the background.', ['parent' => $activity['reply-to-id']]); + DI::logger()->notice('Fetching is done in the background.', ['parent' => $activity['reply-to-id']]); } else { - Logger::notice('Recursion level is too high.', ['parent' => $activity['reply-to-id'], 'recursion-depth' => $recursion_depth]); + DI::logger()->notice('Recursion level is too high.', ['parent' => $activity['reply-to-id'], 'recursion-depth' => $recursion_depth]); } if (!Fetch::hasWorker($activity['reply-to-id'])) { - Logger::notice('Fetching is done by worker.', ['parent' => $activity['reply-to-id'], 'recursion-depth' => $recursion_depth]); + DI::logger()->notice('Fetching is done by worker.', ['parent' => $activity['reply-to-id'], 'recursion-depth' => $recursion_depth]); Fetch::add($activity['reply-to-id']); $activity['recursion-depth'] = 0; - $wid = Worker::add(Worker::PRIORITY_HIGH, 'FetchMissingActivity', $activity['reply-to-id'], $activity, '', Receiver::COMPLETION_ASYNC); + $wid = Worker::add(Worker::PRIORITY_HIGH, 'FetchMissingActivity', $activity['reply-to-id'], $activity, '', Receiver::COMPLETION_ASYNC); Fetch::setWorkerId($activity['reply-to-id'], $wid); } else { - Logger::debug('Activity will already be fetched via a worker.', ['url' => $activity['reply-to-id']]); + DI::logger()->debug('Activity will already be fetched via a worker.', ['url' => $activity['reply-to-id']]); } return ''; @@ -604,7 +639,7 @@ class Processor try { $curlResult = HTTPSignature::fetchRaw($url, 0); } catch (\Exception $exception) { - Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]); + DI::logger()->notice('Error fetching url', ['url' => $url, 'exception' => $exception]); return true; } @@ -614,7 +649,7 @@ class Processor } if ($curlResult->isSuccess()) { - $object = json_decode($curlResult->getBody(), true); + $object = json_decode($curlResult->getBodyString(), true); if (!empty($object)) { $activity = JsonLD::compact($object); if (JsonLD::fetchElement($activity, '@type') == 'as:Tombstone') { @@ -641,7 +676,7 @@ class Processor { $owner = Contact::getIdForURL($activity['actor']); - Logger::info('Deleting item', ['object' => $activity['object_id'], 'owner' => $owner]); + DI::logger()->info('Deleting item', ['object' => $activity['object_id'], 'owner' => $owner]); Item::markForDeletion(['uri' => $activity['object_id'], 'owner-id' => $owner]); Queue::remove($activity); } @@ -667,12 +702,12 @@ class Processor } if (($item['author-link'] != $activity['actor']) && !$item['origin']) { - Logger::info('Not origin, not from the author, skipping update', ['id' => $item['id'], 'author' => $item['author-link'], 'actor' => $activity['actor']]); + DI::logger()->info('Not origin, not from the author, skipping update', ['id' => $item['id'], 'author' => $item['author-link'], 'actor' => $activity['actor']]); continue; } Tag::store($item['uri-id'], Tag::HASHTAG, $activity['object_content'], $activity['object_id']); - Logger::info('Tagged item', ['id' => $item['id'], 'tag' => $activity['object_content'], 'uri' => $activity['target_id'], 'actor' => $activity['actor']]); + DI::logger()->info('Tagged item', ['id' => $item['id'], 'tag' => $activity['object_content'], 'uri' => $activity['target_id'], 'actor' => $activity['actor']]); } } @@ -687,15 +722,15 @@ class Processor public static function createActivity(array $activity, string $verb) { $activity['reply-to-id'] = $activity['object_id']; - $item = self::createItem($activity, false); + $item = self::createItem($activity, false); if (empty($item)) { - Logger::debug('Activity was not prepared', ['id' => $activity['object_id']]); + DI::logger()->debug('Activity was not prepared', ['id' => $activity['object_id']]); return; } - $item['verb'] = $verb; + $item['verb'] = $verb; $item['thr-parent'] = $activity['object_id']; - $item['gravity'] = Item::GRAVITY_ACTIVITY; + $item['gravity'] = Item::GRAVITY_ACTIVITY; unset($item['post-type']); $item['object-type'] = Activity\ObjectType::NOTE; @@ -716,6 +751,8 @@ class Processor */ private static function getUriIdForFeaturedCollection(array $activity) { + $activity['callstack'] = self::addToCallstack($activity['callstack'] ?? []); + $actor = APContact::getByURL($activity['actor']); if (empty($actor)) { return null; @@ -757,7 +794,7 @@ class Processor return; } - Logger::debug('Add post to featured collection', ['post' => $post]); + DI::logger()->debug('Add post to featured collection', ['post' => $post]); Post\Collection::add($post['uri-id'], Post\Collection::FEATURED, $post['author-id']); Queue::remove($activity); @@ -776,7 +813,7 @@ class Processor return; } - Logger::debug('Remove post from featured collection', ['post' => $post]); + DI::logger()->debug('Remove post from featured collection', ['post' => $post]); Post\Collection::remove($post['uri-id'], Post\Collection::FEATURED); Queue::remove($activity); @@ -793,10 +830,10 @@ class Processor */ public static function createEvent(array $activity, array $item): int { - $event['summary'] = HTML::toBBCode($activity['name'] ?: $activity['summary']); - $event['desc'] = HTML::toBBCode($activity['content'] ?? ''); + $event['summary'] = HTML::toBBCode($activity['name'] ?: $activity['summary']); + $event['desc'] = HTML::toBBCode($activity['content'] ?? ''); if (!empty($activity['start-time'])) { - $event['start'] = DateTimeFormat::utc($activity['start-time']); + $event['start'] = DateTimeFormat::utc($activity['start-time']); } if (!empty($activity['end-time'])) { $event['finish'] = DateTimeFormat::utc($activity['end-time']); @@ -822,7 +859,7 @@ class Processor $event_id = Event::store($event); - Logger::info('Event was stored', ['id' => $event_id]); + DI::logger()->info('Event was stored', ['id' => $event_id]); return $event_id; } @@ -839,17 +876,18 @@ class Processor { if (!empty($activity['mediatype']) && ($activity['mediatype'] == 'text/markdown')) { $item['title'] = strip_tags($activity['name'] ?? ''); - $content = Markdown::toBBCode($activity['content']); + $content = Markdown::toBBCode($activity['content'] ?? ''); } elseif (!empty($activity['mediatype']) && ($activity['mediatype'] == 'text/bbcode')) { - $item['title'] = $activity['name']; - $content = $activity['content']; + $item['title'] = $activity['name'] ?? ''; + $content = $activity['content'] ?? ''; } else { // By default assume "text/html" $item['title'] = HTML::toBBCode($activity['name'] ?? ''); - $content = HTML::toBBCode($activity['content'] ?? ''); + $content = HTML::toBBCode($activity['content'] ?? ''); } - $item['title'] = trim(BBCode::toPlaintext($item['title'])); + $item['title'] = trim(BBCode::toPlaintext($item['title'])); + $item['content-warning'] = HTML::toBBCode($activity['summary'] ?? ''); if (!empty($activity['languages'])) { $item['language'] = self::processLanguages($activity['languages']); @@ -866,18 +904,22 @@ class Processor if (!empty($activity['quote-url'])) { $id = Item::fetchByLink($activity['quote-url'], 0, ActivityPub\Receiver::COMPLETION_ASYNC); if ($id) { - $shared_item = Post::selectFirst(['uri-id'], ['id' => $id]); + $shared_item = Post::selectFirst(['uri-id'], ['id' => $id]); $item['quote-uri-id'] = $shared_item['uri-id']; + DI::logger()->debug('Quote is found', ['uri' => $item['uri'], 'uri-id' => $item['uri-id'], 'quote' => $activity['quote-url'], 'quote-uri-id' => $item['quote-uri-id']]); } elseif ($uri_id = ItemURI::getIdByURI($activity['quote-url'], false)) { - Logger::info('Quote was not fetched but the uri-id existed', ['guid' => $item['guid'], 'uri-id' => $item['uri-id'], 'quote' => $activity['quote-url'], 'uri-id' => $uri_id]); + DI::logger()->info('Quote was not fetched but the uri-id existed', ['uri' => $item['uri'], 'uri-id' => $item['uri-id'], 'quote' => $activity['quote-url'], 'quote-uri-id' => $uri_id]); $item['quote-uri-id'] = $uri_id; + } elseif (Queue::exists($activity['quote-url'], 'as:Create')) { + $item['quote-uri-id'] = ItemURI::getIdByURI($activity['quote-url']); + DI::logger()->info('Quote is queued but not processed yet', ['uri' => $item['uri'], 'uri-id' => $item['uri-id'], 'quote' => $activity['quote-url'], 'quote-uri-id' => $item['quote-uri-id']]); } else { - Logger::info('Quote was not fetched', ['guid' => $item['guid'], 'uri-id' => $item['uri-id'], 'quote' => $activity['quote-url']]); + DI::logger()->notice('Quote was not fetched', ['uri' => $item['uri'], 'uri-id' => $item['uri-id'], 'quote' => $activity['quote-url']]); } } if (!empty($activity['source'])) { - $item['body'] = $activity['source']; + $item['body'] = $activity['source']; $item['raw-body'] = $content; $quote_uri_id = Item::getQuoteUriId($item['body']); @@ -891,12 +933,11 @@ class Processor if (empty($activity['directmessage']) && ($parent_uri != $item['uri']) && ($item['gravity'] == Item::GRAVITY_COMMENT)) { $parent = Post::selectFirst(['id', 'uri-id', 'private', 'author-link', 'alias'], ['uri' => $parent_uri]); if (!DBA::isResult($parent)) { - Logger::warning('Unknown parent item.', ['uri' => $parent_uri]); + DI::logger()->warning('Unknown parent item.', ['uri' => $parent_uri]); return false; } $content = self::removeImplicitMentionsFromBody($content, $parent); } - $item['content-warning'] = HTML::toBBCode($activity['summary'] ?? ''); $item['raw-body'] = $item['body'] = $content; } @@ -904,7 +945,7 @@ class Processor foreach (Tag::getFromBody($item['body'], Tag::TAG_CHARACTER[Tag::EXCLUSIVE_MENTION]) as $tag) { $actor = APContact::getByURL($tag[2], false); if (($actor['type'] ?? 'Person') == 'Group') { - Logger::debug('Group post detected via exclusive mention.', ['mention' => $actor['url'], 'actor' => $activity['actor'], 'author' => $activity['author']]); + DI::logger()->debug('Group post detected via exclusive mention.', ['mention' => $actor['url'], 'actor' => $activity['actor'], 'author' => $activity['author']]); $item['isGroup'] = true; $item['group-link'] = $item['owner-link'] = $actor['url']; $item['owner-id'] = Contact::getIdForURL($actor['url']); @@ -926,7 +967,23 @@ class Processor $restrictions = []; } - // @todo Store restrictions + $item['restrictions'] = null; + foreach ($restrictions as $restriction) { + if ($restriction == Tag::CAN_REPLY) { + $item['restrictions'] = $item['restrictions'] | Item::CANT_REPLY; + } elseif ($restriction == Tag::CAN_LIKE) { + $item['restrictions'] = $item['restrictions'] | Item::CANT_LIKE; + } elseif ($restriction == Tag::CAN_ANNOUNCE) { + $item['restrictions'] = $item['restrictions'] | Item::CANT_ANNOUNCE; + } + } + + if (!empty($item['author-id'])) { + $author = Contact::selectFirstAccount(['ap-posting-restricted'], ['id' => $item['author-id']]); + if (!empty($author['ap-posting-restricted'])) { + $item['restrictions'] = $item['restrictions'] | Item::CANT_REPLY; + } + } $item['location'] = $activity['location']; @@ -969,7 +1026,7 @@ class Processor $path = implode("/", $parsed); - return $host_hash . '-'. hash('fnv164', $path) . '-'. hash('joaat', $path); + return $host_hash . '-' . hash('fnv164', $path) . '-' . hash('joaat', $path); } /** @@ -984,38 +1041,38 @@ class Processor // The checks are split to improve the support when searching why a message was accepted. if (count($activity['receiver']) != 1) { // The message has more than one receiver, so it is wanted. - Logger::debug('Message has got several receivers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Message has got several receivers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } if ($item['private'] == Item::PRIVATE) { // We only look at public posts here. Private posts are expected to be intentionally posted to the single receiver. - Logger::debug('Message is private - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Message is private - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } if (!empty($activity['from-relay'])) { // We check relay posts at another place. When it arrived here, the message is already checked. - Logger::debug('Message is a relay post that is already checked - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Message is a relay post that is already checked - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } if (in_array($activity['completion-mode'] ?? Receiver::COMPLETION_NONE, [Receiver::COMPLETION_MANUAL, Receiver::COMPLETION_ANNOUNCE])) { // Manual completions and completions caused by reshares are allowed without any further checks. - Logger::debug('Message is in completion mode - accepted', ['mode' => $activity['completion-mode'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Message is in completion mode - accepted', ['mode' => $activity['completion-mode'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } if ($item['gravity'] != Item::GRAVITY_PARENT) { // We cannot reliably check at this point if a comment or activity belongs to an accepted post or needs to be fetched // This can possibly be improved in the future. - Logger::debug('Message is no parent - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Message is no parent - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } $tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name'); if (Relay::isSolicitedPost($tags, $item['title'] . ' ' . ($item['content-warning'] ?? '') . ' ' . $item['body'], $item['author-id'], $item['uri'], Protocol::ACTIVITYPUB, $activity['thread-completion'] ?? 0)) { - Logger::debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } else { return false; @@ -1036,7 +1093,7 @@ class Processor return; } - $stored = false; + $stored = false; $success = false; ksort($activity['receiver']); @@ -1064,7 +1121,7 @@ class Processor $item['uid'] = $receiver; $type = $activity['reception_type'][$receiver] ?? Receiver::TARGET_UNKNOWN; - switch($type) { + switch ($type) { case Receiver::TARGET_TO: $item['post-reason'] = Item::PR_TO; break; @@ -1106,7 +1163,7 @@ class Processor } elseif (($item['post-reason'] == Item::PR_FOLLOWER) && !empty($activity['from-relay'])) { // When a post arrives via a relay and we follow the author, we have to override the causer. // Otherwise the system assumes that we follow the relay. (See "addRowInformation") - Logger::debug('Relay post for follower', ['receiver' => $receiver, 'guid' => $item['guid'], 'relay' => $activity['from-relay']]); + DI::logger()->debug('Relay post for follower', ['receiver' => $receiver, 'guid' => $item['guid'], 'relay' => $activity['from-relay']]); $item['causer-id'] = ($item['gravity'] == Item::GRAVITY_PARENT) ? $item['owner-id'] : $item['author-id']; } @@ -1131,20 +1188,22 @@ class Processor if (($receiver != 0) && ($item['gravity'] == Item::GRAVITY_PARENT) && !in_array($item['post-reason'], [Item::PR_FOLLOWER, Item::PR_TAG, Item::PR_TO, Item::PR_CC, Item::PR_AUDIENCE])) { if (!$item['isGroup']) { if ($item['post-reason'] == Item::PR_BCC) { - Logger::info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id'], 'url' => $item['uri']]); + DI::logger()->info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id'], 'url' => $item['uri']]); continue; } if ((DI::pConfig()->get($receiver, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE) && in_array($activity['thread-children-type'] ?? '', Receiver::ACTIVITY_TYPES)) { - Logger::info('Top level post from thread completion from a non sharer had been initiated via an activity, ignoring', - ['type' => $activity['thread-children-type'], 'user' => $item['uid'], 'causer' => $item['causer-link'], 'author' => $activity['author'], 'url' => $item['uri']]); + DI::logger()->info( + 'Top level post from thread completion from a non sharer had been initiated via an activity, ignoring', + ['type' => $activity['thread-children-type'], 'user' => $item['uid'], 'causer' => $item['causer-link'], 'author' => $activity['author'], 'url' => $item['uri']] + ); continue; } } $isGroup = false; - $user = User::getById($receiver, ['account-type']); + $user = User::getById($receiver, ['account-type']); if (!empty($user['account-type'])) { $isGroup = ($user['account-type'] == User::ACCOUNT_TYPE_COMMUNITY); } @@ -1152,11 +1211,11 @@ class Processor if ((DI::pConfig()->get($receiver, 'system', 'accept_only_sharer') == Item::COMPLETION_NONE) && ((!$isGroup && !$item['isGroup'] && ($activity['type'] != 'as:Announce')) || !Contact::isSharingByURL($activity['actor'], $receiver))) { - Logger::info('Actor is a non sharer, is no group or it is no announce', ['uid' => $receiver, 'actor' => $activity['actor'], 'url' => $item['uri'], 'type' => $activity['type']]); + DI::logger()->info('Actor is a non sharer, is no group or it is no announce', ['uid' => $receiver, 'actor' => $activity['actor'], 'url' => $item['uri'], 'type' => $activity['type']]); continue; } - Logger::info('Accepting post', ['uid' => $receiver, 'url' => $item['uri']]); + DI::logger()->info('Accepting post', ['uid' => $receiver, 'url' => $item['uri']]); } if (!self::hasParents($item, $receiver)) { @@ -1171,12 +1230,12 @@ class Processor $item_id = Item::insert($item); if ($item_id) { - Logger::info('Item insertion successful', ['user' => $item['uid'], 'item_id' => $item_id]); + DI::logger()->info('Item insertion successful', ['user' => $item['uid'], 'item_id' => $item_id]); $success = true; } else { - Logger::notice('Item insertion aborted', ['uri' => $item['uri'], 'uid' => $item['uid']]); + DI::logger()->notice('Item insertion aborted', ['uri' => $item['uri'], 'uid' => $item['uid']]); if (($item['uid'] == 0) && (count($activity['receiver']) > 1)) { - Logger::info('Public item was aborted. We skip for all users.', ['uri' => $item['uri']]); + DI::logger()->info('Public item was aborted. We skip for all users.', ['uri' => $item['uri']]); break; } } @@ -1189,7 +1248,7 @@ class Processor Queue::remove($activity); if ($success && Queue::hasChildren($item['uri']) && Post::exists(['uri' => $item['uri']])) { - Queue::processReplyByUri($item['uri']); + Queue::processReplyByUri($item['uri'], $activity); } // Store send a follow request for every reshare - but only when the item had been stored @@ -1197,10 +1256,14 @@ class Processor $author = APContact::getByURL($item['owner-link'], false); // We send automatic follow requests for reshared messages. (We don't need though for group posts) if ($author['type'] != 'Group') { - Logger::info('Send follow request', ['uri' => $item['uri'], 'stored' => $stored, 'to' => $item['author-link']]); + DI::logger()->info('Send follow request', ['uri' => $item['uri'], 'stored' => $stored, 'to' => $item['author-link']]); ActivityPub\Transmitter::sendFollowObject($item['uri'], $item['author-link']); } } + + if ($success) { + self::processReplies($activity, $item); + } } /** @@ -1249,18 +1312,18 @@ class Processor if (Post::exists(['uri-id' => $item['parent-uri-id'], 'uid' => $receiver])) { $has_parents = true; } elseif ($add_parent && Post::exists(['uri-id' => $item['parent-uri-id'], 'uid' => 0])) { - $stored = Item::storeForUserByUriId($item['parent-uri-id'], $receiver, $fields); + $stored = Item::storeForUserByUriId($item['parent-uri-id'], $receiver, $fields); $has_parents = (bool)$stored; if ($stored) { - Logger::notice('Inserted missing parent post', ['stored' => $stored, 'uid' => $receiver, 'parent' => $item['parent-uri']]); + DI::logger()->notice('Inserted missing parent post', ['stored' => $stored, 'uid' => $receiver, 'parent' => $item['parent-uri']]); } else { - Logger::notice('Parent could not be added.', ['uid' => $receiver, 'uri' => $item['uri'], 'parent' => $item['parent-uri']]); + DI::logger()->notice('Parent could not be added.', ['uid' => $receiver, 'uri' => $item['uri'], 'parent' => $item['parent-uri']]); return false; } } elseif ($add_parent) { - Logger::debug('Parent does not exist.', ['uid' => $receiver, 'uri' => $item['uri'], 'parent' => $item['parent-uri']]); + DI::logger()->debug('Parent does not exist.', ['uid' => $receiver, 'uri' => $item['uri'], 'parent' => $item['parent-uri']]); } else { - Logger::debug('Parent should not be added.', ['uid' => $receiver, 'gravity' => $item['gravity'], 'verb' => $item['verb'], 'guid' => $item['guid'], 'uri' => $item['uri'], 'parent' => $item['parent-uri']]); + DI::logger()->debug('Parent should not be added.', ['uid' => $receiver, 'gravity' => $item['gravity'], 'verb' => $item['verb'], 'guid' => $item['guid'], 'uri' => $item['uri'], 'parent' => $item['parent-uri']]); } } @@ -1268,17 +1331,17 @@ class Processor if (Post::exists(['uri-id' => $item['thr-parent-id'], 'uid' => $receiver])) { $has_parents = true; } elseif (($has_parents || $add_parent) && Post::exists(['uri-id' => $item['thr-parent-id'], 'uid' => 0])) { - $stored = Item::storeForUserByUriId($item['thr-parent-id'], $receiver, $fields); + $stored = Item::storeForUserByUriId($item['thr-parent-id'], $receiver, $fields); $has_parents = $has_parents || (bool)$stored; if ($stored) { - Logger::notice('Inserted missing thread parent post', ['stored' => $stored, 'uid' => $receiver, 'thread-parent' => $item['thr-parent']]); + DI::logger()->notice('Inserted missing thread parent post', ['stored' => $stored, 'uid' => $receiver, 'thread-parent' => $item['thr-parent']]); } else { - Logger::notice('Thread parent could not be added.', ['uid' => $receiver, 'uri' => $item['uri'], 'thread-parent' => $item['thr-parent']]); + DI::logger()->notice('Thread parent could not be added.', ['uid' => $receiver, 'uri' => $item['uri'], 'thread-parent' => $item['thr-parent']]); } } elseif ($add_parent) { - Logger::debug('Thread parent does not exist.', ['uid' => $receiver, 'uri' => $item['uri'], 'thread-parent' => $item['thr-parent']]); + DI::logger()->debug('Thread parent does not exist.', ['uid' => $receiver, 'uri' => $item['uri'], 'thread-parent' => $item['thr-parent']]); } else { - Logger::debug('Thread parent should not be added.', ['uid' => $receiver, 'gravity' => $item['gravity'], 'verb' => $item['verb'], 'guid' => $item['guid'], 'uri' => $item['uri'], 'thread-parent' => $item['thr-parent']]); + DI::logger()->debug('Thread parent should not be added.', ['uid' => $receiver, 'gravity' => $item['gravity'], 'verb' => $item['verb'], 'guid' => $item['guid'], 'uri' => $item['uri'], 'thread-parent' => $item['thr-parent']]); } } @@ -1299,6 +1362,7 @@ class Processor } $hash = substr($tag['name'], 0, 1); + $type = 0; if ($tag['type'] == 'Mention') { if (in_array($hash, [Tag::TAG_CHARACTER[Tag::MENTION], @@ -1340,12 +1404,12 @@ class Processor } elseif ($host = parse_url($receiver, PHP_URL_HOST)) { $name = $host; } else { - Logger::warning('Unable to coerce name from receiver', ['element' => $element, 'type' => $type, 'receiver' => $receiver]); + DI::logger()->warning('Unable to coerce name from receiver', ['element' => $element, 'type' => $type, 'receiver' => $receiver]); $name = ''; } $target = Tag::getTargetType($receiver); - Logger::debug('Got target type', ['type' => $target, 'url' => $receiver]); + DI::logger()->debug('Got target type', ['type' => $target, 'url' => $receiver]); Tag::store($uriid, $type, $name, $receiver, $target); } } @@ -1366,8 +1430,8 @@ class Processor } elseif ($host = parse_url($capability, PHP_URL_HOST)) { $name = $host; } else { - Logger::warning('Unable to coerce name from capability', ['element' => $element, 'type' => $type, 'capability' => $capability]); - $name = ''; + DI::logger()->warning('Unable to coerce name from capability', ['element' => $element, 'type' => $type, 'capability' => $capability]); + $name = ''; } $restricted = false; Tag::store($uriid, $type, $name, $capability); @@ -1386,37 +1450,37 @@ class Processor * @return int|bool New mail table row id or false on error * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function postMail(array $item): bool + private static function postMail(array $item) { if (($item['gravity'] != Item::GRAVITY_PARENT) && !DBA::exists('mail', ['uri' => $item['thr-parent'], 'uid' => $item['uid']])) { - Logger::info('Parent not found, mail will be discarded.', ['uid' => $item['uid'], 'uri' => $item['thr-parent']]); + DI::logger()->info('Parent not found, mail will be discarded.', ['uid' => $item['uid'], 'uri' => $item['thr-parent']]); return false; } if (!Contact::isFollower($item['contact-id'], $item['uid']) && !Contact::isSharing($item['contact-id'], $item['uid'])) { - Logger::info('Contact is not a sharer or follower, mail will be discarded.', ['item' => $item]); + DI::logger()->info('Contact is not a sharer or follower, mail will be discarded.', ['item' => $item]); return false; } - Logger::info('Direct Message', $item); + DI::logger()->info('Direct Message', $item); - $msg = []; + $msg = []; $msg['uid'] = $item['uid']; $msg['contact-id'] = $item['contact-id']; - $contact = Contact::getById($item['contact-id'], ['name', 'url', 'photo']); - $msg['from-name'] = $contact['name']; - $msg['from-url'] = $contact['url']; + $contact = Contact::getById($item['contact-id'], ['name', 'url', 'photo']); + $msg['from-name'] = $contact['name']; + $msg['from-url'] = $contact['url']; $msg['from-photo'] = $contact['photo']; - $msg['uri'] = $item['uri']; + $msg['uri'] = $item['uri']; $msg['created'] = $item['created']; $parent = DBA::selectFirst('mail', ['parent-uri', 'title'], ['uri' => $item['thr-parent']]); if (DBA::isResult($parent)) { $msg['parent-uri'] = $parent['parent-uri']; - $msg['title'] = $parent['title']; + $msg['title'] = $parent['title']; } else { $msg['parent-uri'] = $item['thr-parent']; @@ -1454,17 +1518,17 @@ class Processor */ public static function fetchFeaturedPosts(string $url) { - Logger::info('Fetch featured posts', ['contact' => $url]); + DI::logger()->info('Fetch featured posts', ['contact' => $url]); $apcontact = APContact::getByURL($url); if (empty($apcontact['featured'])) { - Logger::info('Contact does not have a featured collection', ['contact' => $url]); + DI::logger()->info('Contact does not have a featured collection', ['contact' => $url]); return; } $pcid = Contact::getIdForURL($url, 0, false); if (empty($pcid)) { - Logger::notice('Contact not found', ['contact' => $url]); + DI::logger()->notice('Contact not found', ['contact' => $url]); return; } @@ -1477,11 +1541,11 @@ class Processor $featured = ActivityPub::fetchItems($apcontact['featured']); if (empty($featured)) { - Logger::info('Contact does not have featured posts', ['contact' => $url]); + DI::logger()->info('Contact does not have featured posts', ['contact' => $url]); foreach ($old_featured as $uri_id) { Post\Collection::remove($uri_id, Post\Collection::FEATURED); - Logger::debug('Removed no longer featured post', ['uri-id' => $uri_id, 'contact' => $url]); + DI::logger()->debug('Removed no longer featured post', ['uri-id' => $uri_id, 'contact' => $url]); } return; } @@ -1499,10 +1563,10 @@ class Processor if (!empty($item['uri-id'])) { if (!$item['featured']) { Post\Collection::add($item['uri-id'], Post\Collection::FEATURED, $item['author-id']); - Logger::debug('Added featured post', ['uri-id' => $item['uri-id'], 'contact' => $url]); + DI::logger()->debug('Added featured post', ['uri-id' => $item['uri-id'], 'contact' => $url]); $new++; } else { - Logger::debug('Post already had been featured', ['uri-id' => $item['uri-id'], 'contact' => $url]); + DI::logger()->debug('Post already had been featured', ['uri-id' => $item['uri-id'], 'contact' => $url]); $old++; } @@ -1516,41 +1580,51 @@ class Processor foreach ($old_featured as $uri_id) { Post\Collection::remove($uri_id, Post\Collection::FEATURED); - Logger::debug('Removed no longer featured post', ['uri-id' => $uri_id, 'contact' => $url]); + DI::logger()->debug('Removed no longer featured post', ['uri-id' => $uri_id, 'contact' => $url]); } - Logger::info('Fetched featured posts', ['new' => $new, 'old' => $old, 'contact' => $url]); + DI::logger()->info('Fetched featured posts', ['new' => $new, 'old' => $old, 'contact' => $url]); } public static function fetchCachedActivity(string $url, int $uid): array { $cachekey = self::CACHEKEY_FETCH_ACTIVITY . $uid . ':' . hash('sha256', $url); - $object = DI::cache()->get($cachekey); + $object = DI::cache()->get($cachekey); if (!is_null($object)) { if (!empty($object)) { - Logger::debug('Fetch from cache', ['url' => $url, 'uid' => $uid]); + DI::logger()->debug('Fetch from cache', ['url' => $url, 'uid' => $uid]); } else { - Logger::debug('Fetch from negative cache', ['url' => $url, 'uid' => $uid]); + DI::logger()->debug('Fetch from negative cache', ['url' => $url, 'uid' => $uid]); } return $object; } $object = HTTPSignature::fetch($url, $uid); + + if (!empty($object)) { + $object = self::refetchObjectOnHostDifference($object, $url); + } + if (empty($object)) { - Logger::notice('Activity was not fetchable, aborting.', ['url' => $url, 'uid' => $uid]); + DI::logger()->notice('Activity was not fetchable, aborting.', ['url' => $url, 'uid' => $uid]); // We perform negative caching. DI::cache()->set($cachekey, [], Duration::FIVE_MINUTES); return []; } if (empty($object['id'])) { - Logger::notice('Activity has got not id, aborting. ', ['url' => $url, 'object' => $object]); + DI::logger()->notice('Activity has got not id, aborting. ', ['url' => $url, 'object' => $object]); return []; } + + if (!self::isValidObject($object)) { + return []; + } + DI::cache()->set($cachekey, $object, Duration::FIVE_MINUTES); - Logger::debug('Activity was fetched successfully', ['url' => $url, 'uid' => $uid]); + DI::logger()->debug('Activity was fetched successfully', ['url' => $url, 'uid' => $uid]); return $object; } @@ -1573,18 +1647,19 @@ class Processor return null; } + if (!empty($child['children']) && in_array($url, $child['children'])) { + DI::logger()->notice('id is already in the list of children', ['depth' => count($child['children']), 'children' => $child['children'], 'id' => $url]); + return null; + } + try { $curlResult = HTTPSignature::fetchRaw($url, $uid); } catch (\Exception $exception) { - Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]); + DI::logger()->notice('Error fetching url', ['url' => $url, 'exception' => $exception]); return ''; } - if (empty($curlResult)) { - return ''; - } - - $body = $curlResult->getBody(); + $body = $curlResult->getBodyString(); if (!$curlResult->isSuccess() || empty($body)) { if (in_array($curlResult->getReturnCode(), [403, 404, 406, 410])) { return null; @@ -1593,16 +1668,29 @@ class Processor } $object = json_decode($body, true); - if (empty($object) || !is_array($object)) { - $element = explode(';', $curlResult->getContentType()); - if (!in_array($element[0], ['application/activity+json', 'application/ld+json', 'application/json'])) { - Logger::debug('Unexpected content-type', ['url' => $url, 'content-type' => $curlResult->getContentType()]); - return null; - } - Logger::notice('Invalid JSON data', ['url' => $url, 'content-type' => $curlResult->getContentType(), 'body' => $body]); - return ''; + + if (!empty($object)) { + $object = self::refetchObjectOnHostDifference($object, $url); } + if (empty($object) || !is_array($object)) { + DI::logger()->notice('Invalid JSON data', ['url' => $url, 'content-type' => $curlResult->getContentType()]); + return null; + } + + if (!self::isValidObject($object)) { + return null; + } + + if (!HTTPSignature::isValidContentType($curlResult->getContentType(), $url)) { + return null; + } + + return self::processActivity($object, $url, $child, $relay_actor, $completion, $uid); + } + + private static function processActivity(array $object, string $url, array $child, string $relay_actor, int $completion, int $uid = 0): ?string + { $ldobject = JsonLD::compact($object); $signer = []; @@ -1623,7 +1711,7 @@ class Processor $signer[] = $object_actor; if (!empty($child['author'])) { - $actor = $child['author']; + $actor = $child['author']; $signer[] = $actor; } else { $actor = $object_actor; @@ -1637,9 +1725,12 @@ class Processor if (Item::searchByLink($object_id)) { return $object_id; } - Logger::debug('Fetch announced activity', ['type' => $type, 'id' => $object_id, 'actor' => $relay_actor, 'signer' => $signer]); + DI::logger()->debug('Fetch announced activity', ['type' => $type, 'id' => $object_id, 'actor' => $relay_actor, 'signer' => $signer]); - return self::fetchMissingActivity($object_id, $child, $relay_actor, $completion, $uid); + if (!self::alreadyKnown($object_id, $child['id'] ?? '')) { + $child['callstack'] = self::addToCallstack($child['callstack'] ?? []); + return self::fetchMissingActivity($object_id, $child, $relay_actor, $completion, $uid); + } } $activity = $object; $ldactivity = $ldobject; @@ -1651,6 +1742,17 @@ class Processor } $ldactivity['recursion-depth'] = !empty($child['recursion-depth']) ? $child['recursion-depth'] + 1 : 0; + $ldactivity['children'] = $child['children'] ?? []; + $ldactivity['callstack'] = $child['callstack'] ?? []; + // This check is mostly superfluous, since there are similar checks before. This covers the case, when the fetched id doesn't match the url + if (in_array($activity['id'], $ldactivity['children'])) { + DI::logger()->notice('Fetched id is already in the list of children. It will not be processed.', ['id' => $activity['id'], 'children' => $ldactivity['children'], 'depth' => count($ldactivity['children'])]); + return null; + } + if (!empty($child['id'])) { + $ldactivity['children'][] = $child['id']; + } + if ($object_actor != $actor) { Contact::updateByUrlIfNeeded($object_actor); @@ -1682,37 +1784,186 @@ class Processor } if (($completion == Receiver::COMPLETION_RELAY) && Queue::exists($url, 'as:Create')) { - Logger::info('Activity has already been queued.', ['url' => $url, 'object' => $activity['id']]); + DI::logger()->info('Activity has already been queued.', ['url' => $url, 'object' => $activity['id']]); } elseif (ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity), $uid, true, false, $signer, '', $completion)) { - Logger::info('Activity had been fetched and processed.', ['url' => $url, 'entry' => $child['entry-id'] ?? 0, 'completion' => $completion, 'object' => $activity['id']]); + DI::logger()->info('Activity had been fetched and processed.', ['url' => $url, 'entry' => $child['entry-id'] ?? 0, 'completion' => $completion, 'object' => $activity['id']]); } else { - Logger::info('Activity had been fetched and will be processed later.', ['url' => $url, 'entry' => $child['entry-id'] ?? 0, 'completion' => $completion, 'object' => $activity['id']]); + DI::logger()->info('Activity had been fetched and will be processed later.', ['url' => $url, 'entry' => $child['entry-id'] ?? 0, 'completion' => $completion, 'object' => $activity['id']]); } return $activity['id']; } + private static function fetchReplies(string $url, array $child) + { + $callstack_count = 0; + foreach ($child['callstack'] ?? [] as $function) { + if ($function == __FUNCTION__) { + ++$callstack_count; + } + } + + $callstack = array_slice(array_column(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), 'function'), 1); + $system_count = 0; + foreach ($callstack as $function) { + if ($function == __FUNCTION__) { + ++$system_count; + } + } + + $maximum_fetchreplies_depth = DI::config()->get('system', 'max_fetchreplies_depth'); + if (max($callstack_count, $system_count) == $maximum_fetchreplies_depth) { + DI::logger()->notice('Maximum callstack depth reached', ['max' => $maximum_fetchreplies_depth, 'count' => $callstack_count, 'system-count' => $system_count, 'replies' => $url, 'callstack' => $child['callstack'] ?? [], 'system' => $callstack]); + return; + } + + $child['callstack'] = self::addToCallstack($child['callstack'] ?? []); + + $replies = ActivityPub::fetchItems($url); + if (empty($replies)) { + DI::logger()->notice('No replies', ['replies' => $url]); + return; + } + DI::logger()->notice('Fetch replies - start', ['replies' => $url, 'callstack' => $child['callstack'], 'system' => $callstack]); + $fetched = 0; + foreach ($replies as $reply) { + $id = ''; + + if (is_array($reply)) { + $ldobject = JsonLD::compact($reply); + $id = JsonLD::fetchElement($ldobject, '@id'); + if (Processor::alreadyKnown($id, $child['id'] ?? '')) { + continue; + } + if (!empty($child['children']) && in_array($id, $child['children'])) { + DI::logger()->debug('Replies id is already in the list of children', ['depth' => count($child['children']), 'children' => $child['children'], 'id' => $id]); + continue; + } + if (parse_url($id, PHP_URL_HOST) == parse_url($url, PHP_URL_HOST)) { + DI::logger()->debug('Incluced activity will be processed', ['replies' => $url, 'id' => $id]); + self::processActivity($reply, $id, $child, '', Receiver::COMPLETION_REPLIES); + ++$fetched; + continue; + } + } elseif (is_string($reply)) { + $id = $reply; + } + if (!self::alreadyKnown($id, $child['id'] ?? '')) { + self::fetchMissingActivity($id, $child, '', Receiver::COMPLETION_REPLIES); + ++$fetched; + } + } + DI::logger()->notice('Fetch replies - done', ['fetched' => $fetched, 'total' => count($replies), 'replies' => $url]); + } + + public static function alreadyKnown(string $id, string $child): bool + { + if ($id == $child) { + DI::logger()->debug('Activity is currently processed', ['id' => $id, 'child' => $child]); + return true; + } elseif (Item::searchByLink($id)) { + DI::logger()->debug('Activity already exists', ['id' => $id, 'child' => $child]); + return true; + } elseif (Queue::exists($id, 'as:Create')) { + DI::logger()->debug('Activity is already queued', ['id' => $id, 'child' => $child]); + return true; + } + DI::logger()->debug('Activity is unknown', ['id' => $id, 'child' => $child]); + return false; + } + + private static function refetchObjectOnHostDifference(array $object, string $url): array + { + $ldobject = JsonLD::compact($object); + if (empty($ldobject)) { + DI::logger()->info('Invalid object', ['url' => $url]); + return $object; + } + + $id = JsonLD::fetchElement($ldobject, '@id'); + if (empty($id)) { + DI::logger()->info('No id found in object', ['url' => $url, 'object' => $object]); + return $object; + } + + $url_host = parse_url($url, PHP_URL_HOST); + $id_host = parse_url($id, PHP_URL_HOST); + + if ($id_host == $url_host) { + return $object; + } + + DI::logger()->notice('Refetch activity because of a host mismatch between requested and received id', ['url-host' => $url_host, 'id-host' => $id_host, 'url' => $url, 'id' => $id]); + return HTTPSignature::fetch($id); + } + + private static function isValidObject(array $object): bool + { + $ldobject = JsonLD::compact($object); + if (empty($ldobject)) { + DI::logger()->info('Invalid object'); + return false; + } + + $id = JsonLD::fetchElement($ldobject, '@id'); + if (empty($id)) { + DI::logger()->info('No id found in object'); + return false; + } + + $type = JsonLD::fetchElement($ldobject, '@type'); + $object_id = JsonLD::fetchElement($ldobject, 'as:object', '@id'); + $object_type = JsonLD::fetchElement($ldobject, 'as:object', '@type'); + $actor = JsonLD::fetchElement($ldobject, 'as:actor', '@id'); + $attributed_to = JsonLD::fetchElement($ldobject, 'as:attributedTo', '@id'); + + $id_host = parse_url($id, PHP_URL_HOST); + + if (!empty($actor) && !in_array($type, Receiver::CONTENT_TYPES) && !empty($object_id)) { + $actor_host = parse_url($actor, PHP_URL_HOST); + if ($actor_host != $id_host) { + DI::logger()->notice('Host mismatch between received id and actor', ['id-host' => $id_host, 'actor-host' => $actor_host, 'id' => $id, 'type' => $type, 'object-id' => $object_id, 'object_type' => $object_type, 'actor' => $actor, 'attributed_to' => $attributed_to]); + return false; + } + if (!empty($object_type)) { + $object_attributed_to = JsonLD::fetchElement($ldobject['as:object'], 'as:attributedTo', '@id'); + $attributed_to_host = parse_url($object_attributed_to, PHP_URL_HOST); + $object_id_host = parse_url($object_id, PHP_URL_HOST); + if (!empty($attributed_to_host) && ($attributed_to_host != $object_id_host)) { + DI::logger()->notice('Host mismatch between received object id and attributed actor', ['id-object-host' => $object_id_host, 'attributed-host' => $attributed_to_host, 'id' => $id, 'type' => $type, 'object-id' => $object_id, 'object_type' => $object_type, 'actor' => $actor, 'object_attributed_to' => $object_attributed_to]); + return false; + } + } + } elseif (!empty($attributed_to)) { + $attributed_to_host = parse_url($attributed_to, PHP_URL_HOST); + if ($attributed_to_host != $id_host) { + DI::logger()->notice('Host mismatch between received id and attributed actor', ['id-host' => $id_host, 'attributed-host' => $attributed_to_host, 'id' => $id, 'type' => $type, 'object-id' => $object_id, 'object_type' => $object_type, 'actor' => $actor, 'attributed_to' => $attributed_to]); + return false; + } + } + + return true; + } + private static function getActivityForObject(array $object, string $actor): array { if (!empty($object['published'])) { $published = $object['published']; - } elseif (!empty($child['published'])) { - $published = $child['published']; } else { $published = DateTimeFormat::utcNow(); } - $activity = []; + $activity = []; $activity['@context'] = $object['@context'] ?? ActivityPub::CONTEXT; unset($object['@context']); - $activity['id'] = $object['id']; - $activity['to'] = $object['to'] ?? []; - $activity['cc'] = $object['cc'] ?? []; - $activity['audience'] = $object['audience'] ?? []; - $activity['actor'] = $actor; - $activity['object'] = $object; + $activity['id'] = $object['id']; + $activity['to'] = $object['to'] ?? []; + $activity['cc'] = $object['cc'] ?? []; + $activity['audience'] = $object['audience'] ?? []; + $activity['actor'] = $actor; + $activity['object'] = $object; $activity['published'] = $published; - $activity['type'] = 'Create'; + $activity['type'] = 'Create'; return $activity; } @@ -1727,23 +1978,23 @@ class Processor { if (empty($activity['as:object'])) { $id = JsonLD::fetchElement($activity, '@id'); - Logger::info('No object field in activity - accepted', ['id' => $id]); + DI::logger()->info('No object field in activity - accepted', ['id' => $id]); return true; } $id = JsonLD::fetchElement($activity, 'as:object', '@id'); $replyto = JsonLD::fetchElement($activity['as:object'], 'as:inReplyTo', '@id'); - $uriid = ItemURI::getIdByURI($replyto ?? ''); + $uriid = ItemURI::getIdByURI($replyto ?? ''); if (Post::exists(['uri-id' => $uriid])) { - Logger::info('Post is a reply to an existing post - accepted', ['id' => $id, 'uri-id' => $uriid, 'replyto' => $replyto]); + DI::logger()->info('Post is a reply to an existing post - accepted', ['id' => $id, 'uri-id' => $uriid, 'replyto' => $replyto]); return true; } $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id'); - $authorid = Contact::getIdForURL($attributed_to); + $authorid = Contact::getIdForURL($attributed_to); - $content = JsonLD::fetchElement($activity['as:object'], 'as:name', '@value') ?? ''; + $content = JsonLD::fetchElement($activity['as:object'], 'as:name', '@value') ?? ''; $content .= ' ' . JsonLD::fetchElement($activity['as:object'], 'as:summary', '@value') ?? ''; $content .= ' ' . HTML::toBBCode(JsonLD::fetchElement($activity['as:object'], 'as:content', '@value') ?? ''); @@ -1758,7 +2009,7 @@ class Processor } $messageTags = []; - $tags = Receiver::processTags(JsonLD::fetchElementArray($activity['as:object'], 'as:tag') ?? []); + $tags = Receiver::processTags(JsonLD::fetchElementArray($activity['as:object'], 'as:tag') ?? []); if (!empty($tags)) { foreach ($tags as $tag) { if (($tag['type'] != 'Hashtag') && !strpos($tag['type'], ':Hashtag') || empty($tag['name'])) { @@ -1798,19 +2049,19 @@ class Processor */ public static function getPostLanguages(array $activity): array { - $content = JsonLD::fetchElement($activity, 'as:content') ?? ''; + $content = JsonLD::fetchElement($activity, 'as:content') ?? ''; $languages = JsonLD::fetchElementArray($activity, 'as:content', '@language') ?? []; if (empty($languages)) { return []; } - $iso639 = new \Matriphe\ISO639\ISO639; + $iso639 = new \Matriphe\ISO639\ISO639(); $result = []; foreach ($languages as $language) { if ($language == $content) { continue; - } + } $language = DI::l10n()->toISO6391($language); if (!in_array($language, array_column($iso639->allLanguages(), 0))) { continue; @@ -1848,7 +2099,7 @@ class Processor } $item = [ - 'author-id' => Contact::getIdForURL($activity['actor']), + 'author-id' => Contact::getIdForURL($activity['actor']), 'author-link' => $activity['actor'], ]; @@ -1869,10 +2120,9 @@ class Processor self::transmitPendingEvents($cid, $owner['uid']); } - if (empty($contact)) { - Contact::update(['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]); - } - Logger::notice('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); + Contact::update(['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]); + + DI::logger()->notice('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); Queue::remove($activity); } @@ -1886,7 +2136,7 @@ class Processor private static function transmitPendingEvents(int $cid, int $uid) { $account = DBA::selectFirst('account-user-view', ['ap-inbox', 'ap-sharedinbox'], ['id' => $cid]); - $inbox = $account['ap-sharedinbox'] ?: $account['ap-inbox']; + $inbox = $account['ap-sharedinbox'] ?: $account['ap-inbox']; $events = DBA::select('event', ['id'], ["`uid` = ? AND `start` > ? AND `type` != ?", $uid, DateTimeFormat::utcNow(), 'birthday']); while ($event = DBA::fetch($events)) { @@ -1915,7 +2165,7 @@ class Processor return; } - Logger::info('Updating profile', ['object' => $activity['object_id']]); + DI::logger()->info('Updating profile', ['object' => $activity['object_id']]); Contact::updateFromProbeByURL($activity['object_id']); Queue::remove($activity); } @@ -1930,13 +2180,13 @@ class Processor public static function deletePerson(array $activity) { if (empty($activity['object_id']) || empty($activity['actor'])) { - Logger::info('Empty object id or actor.'); + DI::logger()->info('Empty object id or actor.'); Queue::remove($activity); return; } if ($activity['object_id'] != $activity['actor']) { - Logger::info('Object id does not match actor.'); + DI::logger()->info('Object id does not match actor.'); Queue::remove($activity); return; } @@ -1947,7 +2197,7 @@ class Processor } DBA::close($contacts); - Logger::info('Deleted contact', ['object' => $activity['object_id']]); + DI::logger()->info('Deleted contact', ['object' => $activity['object_id']]); Queue::remove($activity); } @@ -1966,14 +2216,14 @@ class Processor } if ($activity['object_id'] != $activity['actor']) { - Logger::notice('Object is not the actor', ['activity' => $activity]); + DI::logger()->notice('Object is not the actor', ['activity' => $activity]); Queue::remove($activity); return; } $from = Contact::getByURL($activity['object_id'], false, ['uri-id']); if (empty($from['uri-id'])) { - Logger::info('Object not found', ['activity' => $activity]); + DI::logger()->info('Object not found', ['activity' => $activity]); Queue::remove($activity); return; } @@ -1981,7 +2231,7 @@ class Processor $contacts = DBA::select('contact', ['uid', 'url'], ["`uri-id` = ? AND `uid` != ? AND `rel` IN (?, ?)", $from['uri-id'], 0, Contact::FRIEND, Contact::SHARING]); while ($from_contact = DBA::fetch($contacts)) { $result = Contact::createFromProbeForUser($from_contact['uid'], $activity['target_id']); - Logger::debug('Follower added', ['from' => $from_contact, 'result' => $result]); + DI::logger()->debug('Follower added', ['from' => $from_contact, 'result' => $result]); } DBA::close($contacts); Queue::remove($activity); @@ -2008,7 +2258,7 @@ class Processor Contact\User::setIsBlocked($cid, $uid, true); - Logger::info('Contact blocked user', ['contact' => $cid, 'user' => $uid]); + DI::logger()->info('Contact blocked user', ['contact' => $cid, 'user' => $uid]); Queue::remove($activity); } @@ -2033,7 +2283,7 @@ class Processor Contact\User::setIsBlocked($cid, $uid, false); - Logger::info('Contact unblocked user', ['contact' => $cid, 'user' => $uid]); + DI::logger()->info('Contact unblocked user', ['contact' => $cid, 'user' => $uid]); Queue::remove($activity); } @@ -2048,14 +2298,14 @@ class Processor { $account = Contact::getByURL($activity['object_id'], null, ['id', 'gsid']); if (empty($account)) { - Logger::info('Unknown account', ['activity' => $activity]); + DI::logger()->info('Unknown account', ['activity' => $activity]); Queue::remove($activity); return; } $reporter_id = Contact::getIdForURL($activity['actor']); if (empty($reporter_id)) { - Logger::info('Unknown actor', ['activity' => $activity]); + DI::logger()->info('Unknown actor', ['activity' => $activity]); Queue::remove($activity); return; } @@ -2071,7 +2321,7 @@ class Processor $report = DI::reportFactory()->createFromReportsRequest(System::getRules(true), $reporter_id, $account['id'], $account['gsid'], $activity['content'], 'other', false, $uri_ids); DI::report()->save($report); - Logger::info('Stored report', ['reporter' => $reporter_id, 'account' => $account, 'comment' => $activity['content'], 'object_ids' => $activity['object_ids']]); + DI::logger()->info('Stored report', ['reporter' => $reporter_id, 'account' => $account, 'comment' => $activity['content'], 'object_ids' => $activity['object_ids']]); } /** @@ -2083,32 +2333,33 @@ class Processor */ public static function acceptFollowUser(array $activity) { + $check_id = false; + if (!empty($activity['object_actor'])) { - $uid = User::getIdForURL($activity['object_actor']); - $check_id = false; + $uid = User::getIdForURL($activity['object_actor']); } elseif (!empty($activity['receiver']) && (count($activity['receiver']) == 1)) { $uid = array_shift($activity['receiver']); $check_id = true; } if (empty($uid)) { - Logger::notice('User could not be detected', ['activity' => $activity]); + DI::logger()->notice('User could not be detected', ['activity' => $activity]); Queue::remove($activity); return; } $cid = Contact::getIdForURL($activity['actor'], $uid); if (empty($cid)) { - Logger::notice('No contact found', ['actor' => $activity['actor']]); + DI::logger()->notice('No contact found', ['actor' => $activity['actor']]); Queue::remove($activity); return; } $id = Transmitter::activityIDFromContact($cid); if ($id == $activity['object_id']) { - Logger::info('Successful id check', ['uid' => $uid, 'cid' => $cid]); + DI::logger()->info('Successful id check', ['uid' => $uid, 'cid' => $cid]); } else { - Logger::info('Unsuccessful id check', ['uid' => $uid, 'cid' => $cid, 'id' => $id, 'object_id' => $activity['object_id']]); + DI::logger()->info('Unsuccessful id check', ['uid' => $uid, 'cid' => $cid, 'id' => $id, 'object_id' => $activity['object_id']]); if ($check_id) { Queue::remove($activity); return; @@ -2126,7 +2377,7 @@ class Processor $condition = ['id' => $cid]; Contact::update($fields, $condition); - Logger::info('Accept contact request', ['contact' => $cid, 'user' => $uid]); + DI::logger()->info('Accept contact request', ['contact' => $cid, 'user' => $uid]); Queue::remove($activity); } @@ -2146,7 +2397,7 @@ class Processor $cid = Contact::getIdForURL($activity['actor'], $uid); if (empty($cid)) { - Logger::info('No contact found', ['actor' => $activity['actor']]); + DI::logger()->info('No contact found', ['actor' => $activity['actor']]); return; } @@ -2155,11 +2406,11 @@ class Processor $contact = Contact::getById($cid, ['rel']); if ($contact['rel'] == Contact::SHARING) { Contact::remove($cid); - Logger::info('Rejected contact request - contact removed', ['contact' => $cid, 'user' => $uid]); + DI::logger()->info('Rejected contact request - contact removed', ['contact' => $cid, 'user' => $uid]); } elseif ($contact['rel'] == Contact::FRIEND) { Contact::update(['rel' => Contact::FOLLOWER], ['id' => $cid]); } else { - Logger::info('Rejected contact request', ['contact' => $cid, 'user' => $uid]); + DI::logger()->info('Rejected contact request', ['contact' => $cid, 'user' => $uid]); } Queue::remove($activity); } @@ -2211,7 +2462,7 @@ class Processor $cid = Contact::getIdForURL($activity['actor'], $uid); if (empty($cid)) { - Logger::info('No contact found', ['actor' => $activity['actor']]); + DI::logger()->info('No contact found', ['actor' => $activity['actor']]); return; } @@ -2223,7 +2474,7 @@ class Processor } Contact::removeFollower($contact); - Logger::info('Undo following request', ['contact' => $cid, 'user' => $uid]); + DI::logger()->info('Undo following request', ['contact' => $cid, 'user' => $uid]); Queue::remove($activity); } @@ -2241,7 +2492,7 @@ class Processor return; } - Logger::info('Change existing contact', ['cid' => $cid, 'previous' => $contact['network']]); + DI::logger()->info('Change existing contact', ['cid' => $cid, 'previous' => $contact['network']]); Contact::updateFromProbe($cid); } @@ -2262,7 +2513,7 @@ class Processor $implicit_mentions = []; if (empty($parent_author['url'])) { - Logger::notice('Author public contact unknown.', ['author-link' => $parent['author-link'], 'parent-id' => $parent['id']]); + DI::logger()->notice('Author public contact unknown.', ['author-link' => $parent['author-link'], 'parent-id' => $parent['id']]); } else { $implicit_mentions[] = $parent_author['url']; $implicit_mentions[] = $parent_author['nurl']; @@ -2303,7 +2554,7 @@ class Processor $kept_mentions = []; // Extract one prepended mention at a time from the body - while(preg_match('#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is', $body, $matches)) { + while (preg_match('#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is', $body, $matches)) { if (!in_array($matches[2], $potential_mentions)) { $kept_mentions[] = $matches[1]; } @@ -2352,4 +2603,25 @@ class Processor return $body; } + + /** + * Add the current function to the callstack + * + * @param array $callstack + * @return array + */ + public static function addToCallstack(array $callstack): array + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $functions = array_slice(array_column($trace, 'function'), 1); + $function = array_shift($functions); + + if (in_array($function, $callstack)) { + DI::logger()->notice('Callstack already contains "' . $function . '"', ['callstack' => $callstack]); + } + + $callstack[] = $function; + + return $callstack; + } } diff --git a/src/Protocol/ActivityPub/Queue.php b/src/Protocol/ActivityPub/Queue.php index 3046c24580..ca6b692659 100644 --- a/src/Protocol/ActivityPub/Queue.php +++ b/src/Protocol/ActivityPub/Queue.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; @@ -64,8 +49,10 @@ class Queue } if (!empty($activity['context'])) { - $fields['conversation'] = $activity['context']; - } elseif (!empty($activity['conversation'])) { + $fields['context'] = $activity['context']; + } + + if (!empty($activity['conversation'])) { $fields['conversation'] = $activity['conversation']; } @@ -141,7 +128,7 @@ class Queue return; } - Logger::debug('Delete inbox-entry', ['id' => $entry['id']]); + DI::logger()->debug('Delete inbox-entry', ['id' => $entry['id']]); DBA::delete('inbox-entry', ['id' => $entry['id']]); @@ -187,10 +174,11 @@ class Queue * * @param integer $id * @param bool $fetch_parents + * @param array $parent * * @return bool */ - public static function process(int $id, bool $fetch_parents = true): bool + public static function process(int $id, bool $fetch_parents = true, array $parent = []): bool { $entry = DBA::selectFirst('inbox-entry', [], ['id' => $id]); if (empty($entry)) { @@ -198,23 +186,23 @@ class Queue } if (!self::isProcessable($id)) { - Logger::debug('Other queue entries need to be processed first.', ['id' => $id]); + DI::logger()->debug('Other queue entries need to be processed first.', ['id' => $id]); return false; } if (!empty($entry['wid'])) { - $worker = DI::app()->getQueue(); - $wid = $worker['id'] ?? 0; + $worker = DI::appHelper()->getQueue(); + $wid = $worker['id'] ?? 0; if ($entry['wid'] != $wid) { $workerqueue = DBA::selectFirst('workerqueue', ['pid'], ['id' => $entry['wid'], 'done' => false]); if (!empty($workerqueue['pid']) && posix_kill($workerqueue['pid'], 0)) { - Logger::notice('Entry is already processed via another process.', ['current' => $wid, 'processor' => $entry['wid']]); + DI::logger()->notice('Entry is already processed via another process.', ['current' => $wid, 'processor' => $entry['wid']]); return false; } } } - Logger::debug('Processing queue entry', ['id' => $entry['id'], 'type' => $entry['type'], 'object-type' => $entry['object-type'], 'uri' => $entry['object-id'], 'in-reply-to' => $entry['in-reply-to-id']]); + DI::logger()->debug('Processing queue entry', ['id' => $entry['id'], 'type' => $entry['type'], 'object-type' => $entry['object-type'], 'uri' => $entry['object-id'], 'in-reply-to' => $entry['in-reply-to-id']]); $activity = json_decode($entry['activity'], true); $type = $entry['type']; @@ -224,6 +212,14 @@ class Queue $activity['worker-id'] = $entry['wid']; $activity['recursion-depth'] = 0; + if (!empty($parent['children'])) { + $activity['children'] = array_merge($activity['children'] ?? [], $parent['children']); + } + + if (!empty($parent['callstack'])) { + $activity['callstack'] = array_merge($activity['callstack'] ?? [], $parent['callstack']); + } + if (empty($activity['thread-children-type'])) { $activity['thread-children-type'] = $type; } @@ -250,15 +246,34 @@ class Queue */ public static function processAll() { - $entries = DBA::select('inbox-entry', ['id', 'type', 'object-type', 'object-id', 'in-reply-to-id'], ["`trust` AND `wid` IS NULL"], ['order' => ['id' => true]]); + $expired_days = max(1, DI::config()->get('system', 'queue_expired_days')); + $max_retrial = max(3, DI::config()->get('system', 'queue_retrial')); + + $entries = DBA::select('inbox-entry', ['id', 'type', 'object-type', 'object-id', 'in-reply-to-id', 'received', 'trust', 'retrial'], ["`wid` IS NULL"], ['order' => ['retrial', 'id' => true]]); while ($entry = DBA::fetch($entries)) { - if (!self::isProcessable($entry['id'])) { + // We delete all entries that aren't associated with a worker entry after a given amount of days or retrials + if (($entry['retrial'] > $max_retrial) || ($entry['received'] < DateTimeFormat::utc('now - ' . $expired_days . ' days'))) { + self::deleteById($entry['id']); + } + if (!$entry['trust'] || !self::isProcessable($entry['id'])) { continue; } - Logger::debug('Process leftover entry', $entry); + DI::logger()->debug('Process leftover entry', $entry); self::process($entry['id'], false); } DBA::close($entries); + + // Optimizing this table only last seconds + if (DI::config()->get('system', 'optimize_tables')) { + DI::logger()->info('Optimize start'); + DBA::optimizeTable('inbox-entry'); + DI::logger()->info('Optimize end'); + } + } + + private static function retrial(int $id) + { + DBA::update('inbox-entry', ["`retrial` = `retrial` + 1"], ['id' => $id]); } public static function isProcessable(int $id): bool @@ -277,31 +292,46 @@ class Queue return true; } + if (!empty($entry['context'])) { + if (DBA::exists('post-thread', ['context-id' => ItemURI::getIdByURI($entry['context'], false)])) { + // We have got the context in the system, so the post can be processed + return true; + } + } + if (!empty($entry['conversation'])) { - $conv_id = ItemURI::getIdByURI($entry['conversation'], false); - if (DBA::exists('post-thread', ['conversation-id' => $conv_id])) { + if (DBA::exists('post-thread', ['conversation-id' => ItemURI::getIdByURI($entry['conversation'], false)])) { // We have got the conversation in the system, so the post can be processed return true; } } if (!empty($entry['object-id']) && !empty($entry['in-reply-to-id']) && ($entry['object-id'] != $entry['in-reply-to-id'])) { - if (DBA::exists('inbox-entry', ['object-id' => $entry['in-reply-to-id']])) { + if (DBA::exists('inbox-entry', ['object-id' => $entry['in-reply-to-id']])) { // This entry belongs to some other entry that should be processed first + self::retrial($id); return false; } - if (!Post::exists(['uri' => $entry['in-reply-to-id']])) { + if (!Processor::alreadyKnown($entry['in-reply-to-id'], '')) { // This entry belongs to some other entry that need to be fetched first if (Fetch::hasWorker($entry['in-reply-to-id'])) { - Logger::debug('Fetching of the activity is already queued', ['id' => $entry['activity-id'], 'reply-to-id' => $entry['in-reply-to-id']]); + DI::logger()->debug('Fetching of the activity is already queued', ['id' => $entry['activity-id'], 'reply-to-id' => $entry['in-reply-to-id']]); + self::retrial($id); return false; } Fetch::add($entry['in-reply-to-id']); $activity = json_decode($entry['activity'], true); + if (in_array($entry['in-reply-to-id'], $activity['children'] ?? [])) { + DI::logger()->notice('reply-to-id is already in the list of children', ['id' => $entry['in-reply-to-id'], 'children' => $activity['children'], 'depth' => count($activity['children'])]); + self::retrial($id); + return false; + } $activity['recursion-depth'] = 0; - $wid = Worker::add(Worker::PRIORITY_HIGH, 'FetchMissingActivity', $entry['in-reply-to-id'], $activity, '', Receiver::COMPLETION_ASYNC); + $activity['callstack'] = Processor::addToCallstack($activity['callstack'] ?? []); + $wid = Worker::add(Worker::PRIORITY_HIGH, 'FetchMissingActivity', $entry['in-reply-to-id'], $activity, '', Receiver::COMPLETION_ASYNC); Fetch::setWorkerId($entry['in-reply-to-id'], $wid); - Logger::debug('Fetch missing activity', ['wid' => $wid, 'id' => $entry['activity-id'], 'reply-to-id' => $entry['in-reply-to-id']]); + DI::logger()->debug('Fetch missing activity', ['wid' => $wid, 'id' => $entry['activity-id'], 'reply-to-id' => $entry['in-reply-to-id']]); + self::retrial($id); return false; } } @@ -309,42 +339,20 @@ class Queue return true; } - /** - * Clear old activities - * - * @return void - */ - public static function clear() - { - // We delete all entries that aren't associated with a worker entry after seven days. - // The other entries are deleted when the worker deferred for too long. - $entries = DBA::select('inbox-entry', ['id'], ["`wid` IS NULL AND `received` < ?", DateTimeFormat::utc('now - 7 days')]); - while ($entry = DBA::fetch($entries)) { - self::deleteById($entry['id']); - } - DBA::close($entries); - - // Optimizing this table only last seconds - if (DI::config()->get('system', 'optimize_tables')) { - Logger::info('Optimize start'); - DBA::optimizeTable('inbox-entry'); - Logger::info('Optimize end'); - } - } - /** * Process all activities that are children of a given post url * * @param string $uri + * @param array $parent * @return int */ - public static function processReplyByUri(string $uri): int + public static function processReplyByUri(string $uri, array $parent = []): int { - $count = 0; + $count = 0; $entries = DBA::select('inbox-entry', ['id'], ["`in-reply-to-id` = ? AND `object-id` != ?", $uri, $uri]); while ($entry = DBA::fetch($entries)) { $count += 1; - self::process($entry['id'], false); + self::process($entry['id'], false, $parent); } DBA::close($entries); return $count; @@ -405,7 +413,7 @@ class Queue { $entries = DBA::select('inbox-entry', ['id'], ["NOT `trust` AND `wid` IS NULL"], ['order' => ['id' => true]]); while ($entry = DBA::fetch($entries)) { - $data = self::reprepareActivityById($entry['id'], false); + $data = self::reprepareActivityById($entry['id']); if ($data['trust']) { DBA::update('inbox-entry', ['trust' => true], ['id' => $entry['id']]); } diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index 94233c8a89..c217d491ca 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; @@ -25,7 +11,6 @@ use Friendica\Content\Text\BBCode; use Friendica\Database\DBA; use Friendica\Content\Text\HTML; use Friendica\Content\Text\Markdown; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Core\Worker; @@ -61,18 +46,19 @@ use Friendica\Util\Strings; class Receiver { const PUBLIC_COLLECTION = 'as:Public'; - const ACCOUNT_TYPES = ['as:Person', 'as:Organization', 'as:Service', 'as:Group', 'as:Application']; - const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event', 'as:Audio', 'as:Page', 'as:Question']; + + const ACCOUNT_TYPES = ['as:Person', 'as:Organization', 'as:Service', 'as:Group', 'as:Application']; + const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event', 'as:Audio', 'as:Page', 'as:Question']; const ACTIVITY_TYPES = ['as:Like', 'as:Dislike', 'as:Accept', 'as:Reject', 'as:TentativeAccept', 'as:View', 'as:Read', 'litepub:EmojiReact']; - const TARGET_UNKNOWN = 0; - const TARGET_TO = 1; - const TARGET_CC = 2; - const TARGET_BTO = 3; - const TARGET_BCC = 4; + const TARGET_UNKNOWN = 0; + const TARGET_TO = 1; + const TARGET_CC = 2; + const TARGET_BTO = 3; + const TARGET_BCC = 4; const TARGET_FOLLOWER = 5; - const TARGET_ANSWER = 6; - const TARGET_GLOBAL = 7; + const TARGET_ANSWER = 6; + const TARGET_GLOBAL = 7; const TARGET_AUDIENCE = 8; const COMPLETION_NONE = 0; @@ -81,6 +67,7 @@ class Receiver const COMPLETION_MANUAL = 3; const COMPLETION_AUTO = 4; const COMPLETION_ASYNC = 5; + const COMPLETION_REPLIES = 6; /** * Checks incoming message from the inbox @@ -95,7 +82,7 @@ class Receiver { $activity = json_decode($body, true); if (empty($activity)) { - Logger::warning('Invalid body.'); + DI::logger()->warning('Invalid body.'); return; } @@ -106,7 +93,7 @@ class Receiver $apcontact = APContact::getByURL($actor); if (empty($apcontact)) { - Logger::notice('Unable to retrieve AP contact for actor - message is discarded', ['actor' => $actor]); + DI::logger()->notice('Unable to retrieve AP contact for actor - message is discarded', ['actor' => $actor]); return; } elseif (APContact::isRelay($apcontact) && self::isRelayPost($ldactivity)) { self::processRelayPost($ldactivity, $actor); @@ -117,52 +104,52 @@ class Receiver $sig_contact = HTTPSignature::getKeyIdContact($header); if (APContact::isRelay($sig_contact) && self::isRelayPost($ldactivity)) { - Logger::info('Message from a relay', ['url' => $sig_contact['url']]); + DI::logger()->info('Message from a relay', ['url' => $sig_contact['url']]); self::processRelayPost($ldactivity, $sig_contact['url']); return; } $http_signer = HTTPSignature::getSigner($body, $header); if ($http_signer === false) { - Logger::notice('Invalid HTTP signature, message will not be trusted.', ['uid' => $uid, 'actor' => $actor, 'header' => $header, 'body' => $body]); + DI::logger()->notice('Invalid HTTP signature, message will not be trusted.', ['uid' => $uid, 'actor' => $actor, 'header' => $header, 'body' => $body]); $signer = []; } elseif (empty($http_signer)) { - Logger::info('Signer is a tombstone. The message will be discarded, the signer account is deleted.'); + DI::logger()->info('Signer is a tombstone. The message will be discarded, the signer account is deleted.'); return; } else { - Logger::info('Valid HTTP signature', ['signer' => $http_signer]); + DI::logger()->info('Valid HTTP signature', ['signer' => $http_signer]); $signer = [$http_signer]; } - Logger::info('Message for user ' . $uid . ' is from actor ' . $actor); + DI::logger()->info('Message for user ' . $uid . ' is from actor ' . $actor); if ($http_signer === false) { $trust_source = false; } elseif (LDSignature::isSigned($activity)) { $ld_signer = LDSignature::getSigner($activity); if (empty($ld_signer)) { - Logger::info('Invalid JSON-LD signature from ' . $actor); + DI::logger()->info('Invalid JSON-LD signature from ' . $actor); } elseif ($ld_signer != $http_signer) { $signer[] = $ld_signer; } if (!empty($ld_signer && ($actor == $http_signer))) { - Logger::info('The HTTP and the JSON-LD signature belong to ' . $ld_signer); + DI::logger()->info('The HTTP and the JSON-LD signature belong to ' . $ld_signer); $trust_source = true; } elseif (!empty($ld_signer)) { - Logger::info('JSON-LD signature is signed by ' . $ld_signer); + DI::logger()->info('JSON-LD signature is signed by ' . $ld_signer); $trust_source = true; } elseif ($actor == $http_signer) { - Logger::info('Bad JSON-LD signature, but HTTP signer fits the actor.'); + DI::logger()->info('Bad JSON-LD signature, but HTTP signer fits the actor.'); $trust_source = true; } else { - Logger::info('Invalid JSON-LD signature and the HTTP signer is different.'); + DI::logger()->info('Invalid JSON-LD signature and the HTTP signer is different.'); $trust_source = false; } } elseif ($actor == $http_signer) { - Logger::info('Trusting post without JSON-LD signature, The actor fits the HTTP signer.'); + DI::logger()->info('Trusting post without JSON-LD signature, The actor fits the HTTP signer.'); $trust_source = true; } else { - Logger::info('No JSON-LD signature, different actor.'); + DI::logger()->info('No JSON-LD signature, different actor.'); $trust_source = false; } @@ -208,7 +195,7 @@ class Receiver { $type = JsonLD::fetchElement($activity, '@type'); if (!$type) { - Logger::notice('Empty type', ['activity' => $activity, 'actor' => $actor]); + DI::logger()->notice('Empty type', ['activity' => $activity, 'actor' => $actor]); return; } @@ -216,44 +203,43 @@ class Receiver $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); if (empty($object_id)) { - Logger::notice('No object id found', ['type' => $type, 'object_type' => $object_type, 'actor' => $actor, 'activity' => $activity]); + DI::logger()->notice('No object id found', ['type' => $type, 'object_type' => $object_type, 'actor' => $actor, 'activity' => $activity]); return; } $contact = Contact::getByURL($actor); if (empty($contact)) { - Logger::info('Relay contact not found', ['actor' => $actor]); + DI::logger()->info('Relay contact not found', ['actor' => $actor]); return; } if (!in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) { - Logger::notice('Relay is no sharer', ['actor' => $actor]); + DI::logger()->notice('Relay is no sharer', ['actor' => $actor]); return; } - Logger::debug('Process post from relay server', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'actor' => $actor]); + DI::logger()->debug('Process post from relay server', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'actor' => $actor]); $item_id = Item::searchByLink($object_id); if ($item_id) { - Logger::info('Relayed message already exists', ['id' => $object_id, 'item' => $item_id, 'actor' => $actor]); + DI::logger()->info('Relayed message already exists', ['id' => $object_id, 'item' => $item_id, 'actor' => $actor]); return; } if (!DI::config()->get('system', 'decoupled_receiver')) { $id = Processor::fetchMissingActivity($object_id, [], $actor, self::COMPLETION_RELAY); if (!empty($id)) { - Logger::notice('Relayed message is fetched', ['result' => $id, 'id' => $object_id, 'actor' => $actor]); + DI::logger()->notice('Relayed message is fetched', ['result' => $id, 'id' => $object_id, 'actor' => $actor]); } else { - Logger::notice('Relayed message had not been fetched', ['id' => $object_id, 'actor' => $actor, 'activity' => $activity]); + DI::logger()->notice('Relayed message had not been fetched', ['id' => $object_id, 'actor' => $actor, 'activity' => $activity]); } } elseif (!Fetch::hasWorker($object_id)) { - Logger::notice('Fetching is done by worker.', ['id' => $object_id]); + DI::logger()->notice('Fetching is done by worker.', ['id' => $object_id]); Fetch::add($object_id); - $activity['recursion-depth'] = 0; $wid = Worker::add(Worker::PRIORITY_HIGH, 'FetchMissingActivity', $object_id, [], $actor, self::COMPLETION_RELAY); Fetch::setWorkerId($object_id, $wid); } else { - Logger::debug('Activity will already be fetched via a worker.', ['url' => $object_id]); + DI::logger()->debug('Activity will already be fetched via a worker.', ['url' => $object_id]); } } @@ -264,7 +250,7 @@ class Receiver * @param string $object_id Object ID of the provided object * @param integer $uid User ID * - * @return string with object type or NULL + * @return string|null string with object type or NULL * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ @@ -277,12 +263,15 @@ class Receiver } } - if (Post::exists(['uri' => $object_id, 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]])) { + $type = JsonLD::fetchElement($activity, '@type'); + + // Several activities are only done on content types, so we can assume "Note" here. + if (Post::exists(['uri' => $object_id, 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]) || (in_array($type, ['as:Like', 'as:Dislike', 'litepub:EmojiReact', 'as:Announce', 'as:View']))) { // We just assume "note" since it doesn't make a difference for the further processing return 'as:Note'; } - $profile = APContact::getByURL($object_id); + $profile = APContact::getByURL($object_id, false); if (!empty($profile['type'])) { APContact::unmarkForArchival($profile); return 'as:' . $profile['type']; @@ -291,7 +280,7 @@ class Receiver $data = Processor::fetchCachedActivity($object_id, $uid); if (!empty($data)) { $object = JsonLD::compact($data); - $type = JsonLD::fetchElement($object, '@type'); + $type = JsonLD::fetchElement($object, '@type'); if (!empty($type)) { return $type; } @@ -315,9 +304,10 @@ class Receiver */ public static function prepareObjectData(array $activity, int $uid, bool $push, bool &$trust_source, string $original_actor = ''): array { - $id = JsonLD::fetchElement($activity, '@id'); - $type = JsonLD::fetchElement($activity, '@type'); - $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); + $id = JsonLD::fetchElement($activity, '@id'); + $type = JsonLD::fetchElement($activity, '@type'); + $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_type = ''; if (!empty($object_id) && in_array($type, ['as:Create', 'as:Update'])) { $fetch_id = $object_id; @@ -343,25 +333,25 @@ class Receiver $fetched_type = JsonLD::fetchElement($object, '@type'); if (($fetched_id == $id) && !empty($fetched_type) && ($fetched_type == $type)) { - Logger::info('Activity had been fetched successfully', ['id' => $id]); + DI::logger()->info('Activity had been fetched successfully', ['id' => $id]); $trust_source = true; - $activity = $object; + $activity = $object; } elseif (($fetched_id == $object_id) && !empty($fetched_type) && ($fetched_type == $object_type)) { - Logger::info('Fetched data is the object instead of the activity', ['id' => $id]); + DI::logger()->info('Fetched data is the object instead of the activity', ['id' => $id]); $trust_source = true; unset($object['@context']); $activity['as:object'] = $object; } else { - Logger::info('Activity id is not equal', ['id' => $id, 'fetched' => $fetched_id]); + DI::logger()->info('Activity id is not equal', ['id' => $id, 'fetched' => $fetched_id]); } } else { - Logger::info('Activity could not been fetched', ['id' => $id]); + DI::logger()->info('Activity could not been fetched', ['id' => $id]); } } $actor = JsonLD::fetchElement($activity, 'as:actor', '@id'); if (empty($actor)) { - Logger::info('Empty actor', ['activity' => $activity]); + DI::logger()->info('Empty actor', ['activity' => $activity]); return []; } @@ -369,9 +359,9 @@ class Receiver // Fetch all receivers from to, cc, bto and bcc $receiverdata = self::getReceivers($activity, $original_actor ?: $actor, [], false, $push || $fetched); - $receivers = $reception_types = []; + $receivers = $reception_types = []; foreach ($receiverdata as $key => $data) { - $receivers[$key] = $data['uid']; + $receivers[$key] = $data['uid']; $reception_types[$data['uid']] = $data['type'] ?? self::TARGET_UNKNOWN; } @@ -380,9 +370,10 @@ class Receiver // When it is a delivery to a personal inbox we add that user to the receivers if (!empty($uid)) { $additional = [$uid => $uid]; - $receivers = array_replace($receivers, $additional); + $receivers = array_replace($receivers, $additional); if (empty($activity['thread-completion']) && (empty($reception_types[$uid]) || in_array($reception_types[$uid], [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL]))) { $reception_types[$uid] = self::TARGET_BCC; + $owner = User::getOwnerDataById($uid); if (!empty($owner['url'])) { $urls['as:bcc'][] = $owner['url']; @@ -396,12 +387,12 @@ class Receiver $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); if (empty($object_id)) { - Logger::info('No object found'); + DI::logger()->info('No object found'); return []; } if (!is_string($object_id)) { - Logger::info('Invalid object id', ['object' => $object_id]); + DI::logger()->info('Invalid object id', ['object' => $object_id]); return []; } @@ -410,19 +401,21 @@ class Receiver // Any activities on account types must not be altered if (in_array($type, ['as:Flag'])) { $object_data = []; - $object_data['id'] = JsonLD::fetchElement($activity, '@id'); - $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); + + $object_data['id'] = JsonLD::fetchElement($activity, '@id'); + $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); $object_data['object_ids'] = JsonLD::fetchElementArray($activity, 'as:object', '@id'); - $object_data['content'] = JsonLD::fetchElement($activity, 'as:content', '@type'); + $object_data['content'] = JsonLD::fetchElement($activity, 'as:content', '@type'); } elseif (in_array($object_type, self::ACCOUNT_TYPES)) { $object_data = []; - $object_data['id'] = JsonLD::fetchElement($activity, '@id'); - $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); - $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id'); + + $object_data['id'] = JsonLD::fetchElement($activity, '@id'); + $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id'); $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object'); - $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); + $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); if (!$trust_source && ($type == 'as:Delete')) { - $apcontact = APContact::getByURL($object_data['object_id'], true); + $apcontact = APContact::getByURL($object_data['object_id'], true); $trust_source = empty($apcontact) || ($apcontact['type'] == 'Tombstone') || $apcontact['suspended']; } } elseif (in_array($type, ['as:Create', 'as:Update', 'as:Invite']) || strpos($type, '#emojiReaction')) { @@ -430,7 +423,7 @@ class Receiver // We can receive "#emojiReaction" when fetching content from Hubzilla systems $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $fetch_uid); if (empty($object_data)) { - Logger::info("Object data couldn't be processed"); + DI::logger()->info("Object data couldn't be processed"); return []; } @@ -446,24 +439,27 @@ class Receiver // Create a mostly empty array out of the activity data (instead of the object). // This way we later don't have to check for the existence of each individual array element. $object_data = self::processObject($activity, $original_actor); - $object_data['name'] = $type; - $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id'); - $object_data['object_id'] = $object_id; + + $object_data['name'] = $type; + $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id'); + $object_data['object_id'] = $object_id; $object_data['object_type'] = ''; // Since we don't fetch the object, we don't know the type } elseif (in_array($type, ['as:Add', 'as:Remove', 'as:Move'])) { $object_data = []; - $object_data['id'] = JsonLD::fetchElement($activity, '@id'); - $object_data['target_id'] = JsonLD::fetchElement($activity, 'as:target', '@id'); - $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); - $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); + + $object_data['id'] = JsonLD::fetchElement($activity, '@id'); + $object_data['target_id'] = JsonLD::fetchElement($activity, 'as:target', '@id'); + $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); $object_data['object_content'] = JsonLD::fetchElement($activity['as:object'], 'as:content', '@type'); } else { $object_data = []; - $object_data['id'] = JsonLD::fetchElement($activity, '@id'); - $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); - $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id'); + + $object_data['id'] = JsonLD::fetchElement($activity, '@id'); + $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id'); $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object'); - $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); + $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); // An Undo is done on the object of an object, so we need that type as well if (($type == 'as:Undo') && !empty($object_data['object_object'])) { @@ -492,16 +488,16 @@ class Receiver } } - $object_data['type'] = $type; - $object_data['actor'] = $actor; - $object_data['item_receiver'] = $receivers; - $object_data['receiver'] = array_replace($object_data['receiver'] ?? [], $receivers); + $object_data['type'] = $type; + $object_data['actor'] = $actor; + $object_data['item_receiver'] = $receivers; + $object_data['receiver'] = array_replace($object_data['receiver'] ?? [], $receivers); $object_data['reception_type'] = array_replace($object_data['reception_type'] ?? [], $reception_types); - $account = Contact::selectFirstAccount(['platform'], ['nurl' => Strings::normaliseLink($actor)]); + $account = Contact::selectFirstAccount(['platform'], ['nurl' => Strings::normaliseLink($actor)]); $platform = $account['platform'] ?? ''; - Logger::info('Processing', ['type' => $object_data['type'], 'object_type' => $object_data['object_type'], 'id' => $object_data['id'], 'actor' => $actor, 'platform' => $platform]); + DI::logger()->info('Processing', ['type' => $object_data['type'], 'object_type' => $object_data['object_type'], 'id' => $object_data['id'], 'actor' => $actor, 'platform' => $platform]); return $object_data; } @@ -565,6 +561,7 @@ class Receiver $user = User::getById(array_key_first($receivers), ['language']); $l10n = DI::l10n()->withLang($user['language']); + $object_data['name'] = $l10n->t('Chat'); $mail = DBA::selectFirst('mail', ['uri'], ['uid' => array_key_first($receivers), 'title' => $object_data['name']], ['order' => ['id' => true]]); @@ -573,7 +570,7 @@ class Receiver } $object_data['directmessage'] = true; - Logger::debug('Got Misskey Chat'); + DI::logger()->debug('Got Misskey Chat'); return $object_data; } @@ -602,6 +599,8 @@ class Receiver * @param boolean $trust_source Do we trust the source? * @param boolean $push Message had been pushed to our system * @param array $signer The signer of the post + * @param string $http_signer + * @param int $completion * * @return bool * @@ -612,35 +611,45 @@ class Receiver { $type = JsonLD::fetchElement($activity, '@type'); if (!$type) { - Logger::info('Empty type', ['activity' => $activity]); + DI::logger()->info('Empty type', ['activity' => $activity]); return true; } if (!DI::config()->get('system', 'process_view') && ($type == 'as:View')) { - Logger::info('View activities are ignored.', ['signer' => $signer, 'http_signer' => $http_signer]); + DI::logger()->info('View activities are ignored.', ['signer' => $signer, 'http_signer' => $http_signer]); return true; } if (!JsonLD::fetchElement($activity, 'as:object', '@id')) { - Logger::info('Empty object', ['activity' => $activity]); + DI::logger()->info('Empty object', ['activity' => $activity]); return true; } $actor = JsonLD::fetchElement($activity, 'as:actor', '@id'); - if (empty($actor)) { - Logger::info('Empty actor', ['activity' => $activity]); + if ($actor === null || $actor === '') { + DI::logger()->info('Empty actor', ['activity' => $activity]); return true; } if (is_array($activity['as:object'])) { $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id'); + $published = JsonLD::fetchElement($activity['as:object'], 'as:published', '@value'); + $object_type = JsonLD::fetchElement($activity['as:object'], '@type'); + $id = JsonLD::fetchElement($activity, '@id'); + $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); + + if (!empty($published) && $object_id !== null && in_array($type, ['as:Create', 'as:Update']) && in_array($object_type, self::CONTENT_TYPES) + && ($push || ($completion != self::COMPLETION_MANUAL)) && DI::contentItem()->isTooOld($published) && !Post::exists(['uri' => $object_id])) { + DI::logger()->debug('Activity is too old. It will not be processed', ['push' => $push, 'completion' => $completion, 'type' => $type, 'object-type' => $object_type, 'published' => $published, 'id' => $id, 'object-id' => $object_id]); + return true; + } } else { $attributed_to = ''; } // Test the provided signatures against the actor and "attributedTo" if ($trust_source) { - if (!empty($attributed_to) && !empty($actor)) { + if ($attributed_to !== false && $attributed_to !== '') { $trust_source = (in_array($actor, $signer) && in_array($attributed_to, $signer)); } else { $trust_source = in_array($actor, $signer); @@ -652,26 +661,25 @@ class Receiver // For announced "create" activities we remove the middle layer. // For the rest (like, dislike, update, ...) we just process the activity directly. $original_actor = ''; - $object_type = JsonLD::fetchElement($activity['as:object'] ?? [], '@type'); if (($type == 'as:Announce') && !empty($object_type) && !in_array($object_type, self::CONTENT_TYPES) && self::isGroup($actor)) { $object_object_type = JsonLD::fetchElement($activity['as:object']['as:object'] ?? [], '@type'); if (in_array($object_type, ['as:Create']) && in_array($object_object_type, self::CONTENT_TYPES)) { - Logger::debug('Replace "create" activity with inner object', ['type' => $object_type, 'object_type' => $object_object_type]); + DI::logger()->debug('Replace "create" activity with inner object', ['type' => $object_type, 'object_type' => $object_object_type]); $activity['as:object'] = $activity['as:object']['as:object']; } elseif (in_array($object_type, array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) { - Logger::debug('Change announced activity to activity', ['type' => $object_type]); + DI::logger()->debug('Change announced activity to activity', ['type' => $object_type]); $original_actor = $actor; - $type = $object_type; - $activity = $activity['as:object']; + $type = $object_type; + $activity = $activity['as:object']; } else { - Logger::info('Unhandled announced activity', ['type' => $object_type, 'object_type' => $object_object_type]); + DI::logger()->info('Unhandled announced activity', ['type' => $object_type, 'object_type' => $object_object_type]); } } // $trust_source is called by reference and is set to true if the content was retrieved successfully $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source, $original_actor); if (empty($object_data)) { - Logger::info('No object data found', ['activity' => $activity]); + DI::logger()->info('No object data found', ['activity' => $activity]); return true; } @@ -698,24 +706,27 @@ class Receiver } if ($type == 'as:Announce') { - $object_data['object_activity'] = $activity; + $object_data['object_activity'] = $activity; } if (($type == 'as:Create') && $trust_source && !in_array($completion, [self::COMPLETION_MANUAL, self::COMPLETION_ANNOUNCE])) { if (self::hasArrived($object_data['object_id'])) { - Logger::info('The activity already arrived.', ['id' => $object_data['object_id']]); + DI::logger()->info('The activity already arrived.', ['id' => $object_data['object_id']]); return true; } self::addArrivedId($object_data['object_id']); if (Queue::exists($object_data['object_id'], $type)) { - Logger::info('The activity is already added.', ['id' => $object_data['object_id']]); + DI::logger()->info('The activity is already added.', ['id' => $object_data['object_id']]); return true; } } elseif (($type == 'as:Create') && $trust_source && !self::hasArrived($object_data['object_id'])) { self::addArrivedId($object_data['object_id']); } + $object_data['children'] = $activity['children'] ?? []; + $object_data['callstack'] = $activity['callstack'] ?? []; + $decouple = DI::config()->get('system', 'decoupled_receiver') && !in_array($completion, [self::COMPLETION_MANUAL, self::COMPLETION_ANNOUNCE]) && empty($object_data['directmessage']); if ($decouple && ($trust_source || DI::config()->get('debug', 'ap_inbox_store_untrusted'))) { @@ -723,7 +734,7 @@ class Receiver } if (!$trust_source) { - Logger::info('Activity trust could not be achieved.', ['id' => $object_data['object_id'], 'type' => $type, 'signer' => $signer, 'actor' => $actor, 'attributedTo' => $attributed_to]); + DI::logger()->info('Activity trust could not be achieved.', ['id' => $object_data['object_id'], 'type' => $type, 'signer' => $signer, 'actor' => $actor, 'attributedTo' => $attributed_to]); return true; } @@ -731,11 +742,11 @@ class Receiver if (Queue::isProcessable($object_data['entry-id'])) { // We delay by 5 seconds to allow to accumulate all receivers $delayed = date(DateTimeFormat::MYSQL, time() + 5); - Logger::debug('Initiate processing', ['id' => $object_data['entry-id'], 'uri' => $object_data['object_id']]); + DI::logger()->debug('Initiate processing', ['id' => $object_data['entry-id'], 'uri' => $object_data['object_id']]); $wid = Worker::add(['priority' => Worker::PRIORITY_HIGH, 'delayed' => $delayed], 'ProcessQueue', $object_data['entry-id']); Queue::setWorkerId($object_data['entry-id'], $wid); } else { - Logger::debug('Other queue entries need to be processed first.', ['id' => $object_data['entry-id']]); + DI::logger()->debug('Other queue entries need to be processed first.', ['id' => $object_data['entry-id']]); } return false; } @@ -815,17 +826,17 @@ class Receiver case 'as:Announce': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - if (!Item::searchByLink($object_data['object_id'], $uid)) { + if (!Processor::alreadyKnown($object_data['object_id'], '')) { if (ActivityPub\Processor::fetchMissingActivity($object_data['object_id'], [], $object_data['actor'], self::COMPLETION_ANNOUNCE, $uid)) { - Logger::debug('Created announced id', ['uid' => $uid, 'id' => $object_data['object_id']]); + DI::logger()->debug('Created announced id', ['uid' => $uid, 'id' => $object_data['object_id']]); Queue::remove($object_data); } else { - Logger::debug('Announced id was not created', ['uid' => $uid, 'id' => $object_data['object_id']]); + DI::logger()->debug('Announced id was not created', ['uid' => $uid, 'id' => $object_data['object_id']]); Queue::remove($object_data); return true; } } else { - Logger::info('Announced id already exists', ['uid' => $uid, 'id' => $object_data['object_id']]); + DI::logger()->info('Announced id already exists', ['uid' => $uid, 'id' => $object_data['object_id']]); Queue::remove($object_data); } @@ -945,7 +956,7 @@ class Receiver if (!empty($object_data['object_actor'])) { ActivityPub\Processor::acceptFollowUser($object_data); } else { - Logger::notice('Unhandled "accept follow" message.', ['object_data' => $object_data]); + DI::logger()->notice('Unhandled "accept follow" message.', ['object_data' => $object_data]); } } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::createActivity($object_data, Activity::ATTEND); @@ -1035,7 +1046,7 @@ class Receiver break; default: - Logger::info('Unknown activity: ' . $type . ' ' . $object_data['object_type']); + DI::logger()->info('Unknown activity: ' . $type . ' ' . $object_data['object_type']); return false; } return true; @@ -1073,7 +1084,7 @@ class Receiver $tempfile = tempnam(System::getTempPath(), $file); file_put_contents($tempfile, json_encode(['activity' => $activity, 'body' => $body, 'uid' => $uid, 'trust_source' => $trust_source, 'push' => $push, 'signer' => $signer, 'object_data' => $object_data], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); - Logger::notice('Unknown activity stored', ['type' => $type, 'object_type' => $object_data['object_type'], 'object_object_type' => $object_data['object_object_type'] ?? '', 'file' => $tempfile]); + DI::logger()->notice('Unknown activity stored', ['type' => $type, 'object_type' => $object_data['object_type'], 'object_object_type' => $object_data['object_object_type'] ?? '', 'file' => $tempfile]); } /** @@ -1086,7 +1097,7 @@ class Receiver */ private static function getBestUserForActivity(array $activity, string $actor): int { - $uid = 0; + $uid = 0; $actor = $actor ?: JsonLD::fetchElement($activity, 'as:actor', '@id') ?? ''; $receivers = self::getReceivers($activity, $actor, [], false, false); @@ -1123,7 +1134,7 @@ class Receiver foreach ($receiver_list as $receiver) { if ($receiver == 'Public') { - Logger::warning('Not compacted public collection found', ['activity' => $activity]); + DI::logger()->warning('Not compacted public collection found', ['activity' => $activity]); $receiver = ActivityPub::PUBLIC_COLLECTION; } if ($receiver == self::PUBLIC_COLLECTION) { @@ -1173,18 +1184,17 @@ class Receiver if (!empty($actor)) { $profile = APContact::getByURL($actor); $followers = $profile['followers'] ?? ''; - $isGroup = ($profile['type'] ?? '') == 'Group'; - if ($push) { - Contact::updateByUrlIfNeeded($actor); - } - Logger::info('Got actor and followers', ['actor' => $actor, 'followers' => $followers]); + $isGroup = ($profile['type'] ?? '') == 'Group'; + DI::logger()->info('Got actor and followers', ['actor' => $actor, 'followers' => $followers]); } else { - Logger::info('Empty actor', ['activity' => $activity]); + DI::logger()->info('Empty actor', ['activity' => $activity]); $followers = ''; - $isGroup = false; + $isGroup = false; } $parent_followers = ''; + $parent_profile = []; + $parent = Post::selectFirstPost(['parent-author-link'], ['uri' => $reply]); if (!empty($parent['parent-author-link'])) { $parent_profile = APContact::getByURL($parent['parent-author-link']); @@ -1225,7 +1235,7 @@ class Receiver // Fetching all directly addressed receivers $condition = ['self' => true, 'nurl' => Strings::normaliseLink($receiver)]; - $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition); + $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition); if (!DBA::isResult($contact)) { continue; } @@ -1233,9 +1243,11 @@ class Receiver // Check if the potential receiver is following the actor // Exception: The receiver is targetted via "to" or this is a comment if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) { - $networks = Protocol::FEDERATED; - $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], - 'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']]; + $networks = Protocol::FEDERATED; + $condition = [ + 'nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], + 'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid'] + ]; // Group posts are only accepted from group contacts if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) { @@ -1272,6 +1284,12 @@ class Receiver } } + if (empty($receivers) && !empty($parent['parent-author-link'])) { + $uid = User::getIdForURL($parent['parent-author-link']); + + $receivers[$uid] = ['uid' => $uid, 'type' => self::TARGET_BTO]; + } + if (!empty($reply) && (!empty($receivers[0]) || !empty($receivers[-1]))) { $parents = Post::select(['uid'], DBA::mergeConditions(['uri' => $reply], ["`uid` != ?", 0])); while ($parent = Post::fetch($parents)) { @@ -1280,8 +1298,6 @@ class Receiver DBA::close($parents); } - self::switchContacts($receivers, $actor); - // "birdsitelive" is a service that mirrors tweets into the fediverse // These posts can be fetched without authentication, but are not marked as public // We treat them as unlisted posts to be able to handle them. @@ -1290,11 +1306,11 @@ class Receiver if (empty($receivers) && $fetch_unlisted && Contact::isPlatform($actor, 'birdsitelive')) { $receivers[0] = ['uid' => 0, 'type' => self::TARGET_GLOBAL]; $receivers[-1] = ['uid' => -1, 'type' => self::TARGET_GLOBAL]; - Logger::notice('Post from "birdsitelive" is set to "unlisted"', ['id' => JsonLD::fetchElement($activity, '@id')]); + DI::logger()->notice('Post from "birdsitelive" is set to "unlisted"', ['id' => JsonLD::fetchElement($activity, '@id')]); } elseif (empty($receivers) && in_array($activity_type, ['as:Delete', 'as:Undo'])) { $receivers[0] = ['uid' => 0, 'type' => self::TARGET_GLOBAL]; } elseif (empty($receivers)) { - Logger::notice('Post has got no receivers', ['fetch_unlisted' => $fetch_unlisted, 'actor' => $actor, 'id' => JsonLD::fetchElement($activity, '@id'), 'type' => $activity_type]); + DI::logger()->notice('Post has got no receivers', ['fetch_unlisted' => $fetch_unlisted, 'actor' => $actor, 'id' => JsonLD::fetchElement($activity, '@id'), 'type' => $activity_type]); } return $receivers; @@ -1313,11 +1329,13 @@ class Receiver */ private static function getReceiverForActor(array $tags, array $receivers, int $target_type, array $profile): array { - $basecondition = ['rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER], - 'network' => Protocol::FEDERATED, 'archive' => false, 'pending' => false]; + $basecondition = [ + 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER], + 'network' => Protocol::FEDERATED, 'archive' => false, 'pending' => false + ]; $condition = DBA::mergeConditions($basecondition, ["`uri-id` = ? AND `uid` != ?", $profile['uri-id'], 0]); - $contacts = DBA::select('contact', ['uid', 'rel'], $condition); + $contacts = DBA::select('contact', ['uid', 'rel'], $condition); while ($contact = DBA::fetch($contacts)) { if (empty($receivers[$contact['uid']]) && self::isValidReceiverForActor($contact, $tags)) { $receivers[$contact['uid']] = ['uid' => $contact['uid'], 'type' => $target_type]; @@ -1364,62 +1382,6 @@ class Receiver return false; } - /** - * Switches existing contacts to ActivityPub - * - * @param integer $cid Contact ID - * @param integer $uid User ID - * @param string $url Profile URL - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function switchContact(int $cid, int $uid, string $url) - { - if (DBA::exists('contact', ['id' => $cid, 'network' => Protocol::ACTIVITYPUB])) { - Logger::info('Contact is already ActivityPub', ['id' => $cid, 'uid' => $uid, 'url' => $url]); - return; - } - - if (Contact::updateFromProbe($cid)) { - Logger::info('Update was successful', ['id' => $cid, 'uid' => $uid, 'url' => $url]); - } - - // Send a new follow request to be sure that the connection still exists - if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB])) { - Logger::info('Contact had been switched to ActivityPub. Sending a new follow request.', ['uid' => $uid, 'url' => $url]); - ActivityPub\Transmitter::sendActivity('Follow', $url, $uid); - } - } - - /** - * @TODO Fix documentation and type-hints - * - * @param $receivers - * @param $actor - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function switchContacts($receivers, $actor) - { - if (empty($actor)) { - return; - } - - foreach ($receivers as $receiver) { - $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]); - if (DBA::isResult($contact)) { - self::switchContact($contact['id'], $receiver['uid'], $actor); - } - - $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]); - if (DBA::isResult($contact)) { - self::switchContact($contact['id'], $receiver['uid'], $actor); - } - } - } - /** * @TODO Fix documentation and type-hints * @@ -1445,7 +1407,7 @@ class Receiver // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field. $objectId = Item::getURIByLink($object_data['object_id']); if (!empty($objectId) && ($object_data['object_id'] != $objectId)) { - Logger::notice('Fix wrong object-id', ['received' => $object_data['object_id'], 'correct' => $objectId]); + DI::logger()->notice('Fix wrong object-id', ['received' => $object_data['object_id'], 'correct' => $objectId]); $object_data['object_id'] = $objectId; } } @@ -1474,37 +1436,37 @@ class Receiver $data = Processor::fetchCachedActivity($object_id, $uid); if (!empty($data)) { $object = JsonLD::compact($data); - Logger::info('Fetched content for ' . $object_id); + DI::logger()->info('Fetched content for ' . $object_id); } else { - Logger::info('Empty content for ' . $object_id . ', check if content is available locally.'); + DI::logger()->info('Empty content for ' . $object_id . ', check if content is available locally.'); $item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri' => $object_id]); if (!DBA::isResult($item)) { - Logger::info('Object with url ' . $object_id . ' was not found locally.'); + DI::logger()->info('Object with url ' . $object_id . ' was not found locally.'); return false; } - Logger::info('Using already stored item for url ' . $object_id); - $data = ActivityPub\Transmitter::createNote($item); + DI::logger()->info('Using already stored item for url ' . $object_id); + $data = ActivityPub\Transmitter::createNote($item); $object = JsonLD::compact($data); } $id = JsonLD::fetchElement($object, '@id'); if (empty($id)) { - Logger::info('Empty id'); + DI::logger()->info('Empty id'); return false; } if ($id != $object_id) { - Logger::info('Fetched id differs from provided id', ['provided' => $object_id, 'fetched' => $id]); + DI::logger()->info('Fetched id differs from provided id', ['provided' => $object_id, 'fetched' => $id]); return false; } } else { - Logger::info('Using original object for url ' . $object_id); + DI::logger()->info('Using original object for url ' . $object_id); } $type = JsonLD::fetchElement($object, '@type'); if (empty($type)) { - Logger::info('Empty type'); + DI::logger()->info('Empty type'); return false; } @@ -1518,7 +1480,7 @@ class Receiver return $object_data; } - Logger::info('Unhandled object type: ' . $type); + DI::logger()->info('Unhandled object type: ' . $type); return false; } @@ -1561,9 +1523,10 @@ class Receiver } $element = [ - 'type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type') ?? ''), - 'href' => JsonLD::fetchElement($tag, 'as:href', '@id'), - 'name' => JsonLD::fetchElement($tag, 'as:name', '@value') + 'type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type') ?? ''), + 'href' => JsonLD::fetchElement($tag, 'as:href', '@id'), + 'name' => JsonLD::fetchElement($tag, 'as:name', '@value'), + 'mediaType' => JsonLD::fetchElement($tag, 'as:mediaType', '@value') ]; if (empty($element['type'])) { @@ -1594,10 +1557,9 @@ class Receiver continue; } - $url = JsonLD::fetchElement($emoji['as:icon'], 'as:url', '@id'); $element = [ 'name' => JsonLD::fetchElement($emoji, 'as:name', '@value'), - 'href' => $url + 'href' => JsonLD::fetchElement($emoji['as:icon'], 'as:url', '@id') ]; $emojilist[] = $element; @@ -1623,7 +1585,7 @@ class Receiver foreach ($attachments as $attachment) { switch (JsonLD::fetchElement($attachment, '@type')) { case 'as:Page': - $pageUrl = null; + $pageUrl = null; $pageImage = null; $urls = JsonLD::fetchElementArray($attachment, 'as:url'); @@ -1634,7 +1596,7 @@ class Receiver continue; } - $href = JsonLD::fetchElement($url, 'as:href', '@id'); + $href = JsonLD::fetchElement($url, 'as:href', '@id'); $mediaType = JsonLD::fetchElement($url, 'as:mediaType', '@value'); if (Strings::startsWith($mediaType, 'image')) { $pageImage = $href; @@ -1652,12 +1614,12 @@ class Receiver ]; break; case 'as:Image': - $mediaType = JsonLD::fetchElement($attachment, 'as:mediaType', '@value'); - $imageFullUrl = JsonLD::fetchElement($attachment, 'as:url', '@id'); + $mediaType = JsonLD::fetchElement($attachment, 'as:mediaType', '@value'); + $imageFullUrl = JsonLD::fetchElement($attachment, 'as:url', '@id'); $imagePreviewUrl = null; // Multiple URLs? if (!$imageFullUrl && ($urls = JsonLD::fetchElementArray($attachment, 'as:url'))) { - $imageVariants = []; + $imageVariants = []; $previewVariants = []; foreach ($urls as $url) { // Scalar URL, no discrimination possible @@ -1700,22 +1662,22 @@ class Receiver } $attachlist[] = [ - 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')), + 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')), 'mediaType' => $mediaType, - 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'), - 'url' => $imageFullUrl, - 'image' => $imagePreviewUrl !== $imageFullUrl ? $imagePreviewUrl : null, + 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'), + 'url' => $imageFullUrl, + 'image' => $imagePreviewUrl !== $imageFullUrl ? $imagePreviewUrl : null, ]; break; default: $attachlist[] = [ - 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')), + 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')), 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'), - 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'), - 'url' => JsonLD::fetchElement($attachment, 'as:url', '@id') ?? JsonLD::fetchElement($attachment, 'as:href', '@id'), - 'height' => JsonLD::fetchElement($attachment, 'as:height', '@value'), - 'width' => JsonLD::fetchElement($attachment, 'as:width', '@value'), - 'image' => JsonLD::fetchElement($attachment, 'as:image', '@id') + 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'), + 'url' => JsonLD::fetchElement($attachment, 'as:url', '@id') ?? JsonLD::fetchElement($attachment, 'as:href', '@id'), + 'height' => JsonLD::fetchElement($attachment, 'as:height', '@value'), + 'width' => JsonLD::fetchElement($attachment, 'as:width', '@value'), + 'image' => JsonLD::fetchElement($attachment, 'as:image', '@id') ]; } } @@ -1736,10 +1698,10 @@ class Receiver if (!empty($object['as:oneOf'])) { $question['multiple'] = false; - $options = JsonLD::fetchElementArray($object, 'as:oneOf') ?? []; + $options = JsonLD::fetchElementArray($object, 'as:oneOf') ?? []; } elseif (!empty($object['as:anyOf'])) { $question['multiple'] = true; - $options = JsonLD::fetchElementArray($object, 'as:anyOf') ?? []; + $options = JsonLD::fetchElementArray($object, 'as:anyOf') ?? []; } else { return []; } @@ -1896,6 +1858,7 @@ class Receiver } $size = (int)JsonLD::fetchElement($url, 'pt:size', '@value'); + $attachments[] = ['type' => $filetype, 'mediaType' => $mediatype, 'url' => $href, 'height' => $height, 'size' => $size, 'name' => '']; } elseif (in_array($mediatype, ['application/x-bittorrent', 'application/x-bittorrent;x-scheme-handler/magnet'])) { $height = (int)JsonLD::fetchElement($url, 'as:height', '@value'); @@ -1932,29 +1895,34 @@ class Receiver $object_data = self::getObjectDataFromActivity($object); - $receiverdata = self::getReceivers($object, $actor ?: $object_data['actor'] ?? '', $object_data['tags'], true, false); - $receivers = $reception_types = []; - foreach ($receiverdata as $key => $data) { - $receivers[$key] = $data['uid']; - $reception_types[$data['uid']] = $data['type'] ?? 0; - } - $object_data['receiver_urls'] = self::getReceiverURL($object); - $object_data['receiver'] = $receivers; - $object_data['reception_type'] = $reception_types; + $object_data['receiver'] = []; + $object_data['reception_type'] = []; + $object_data['unlisted'] = false; + + $receiverdata = self::getReceivers($object, $actor ?: $object_data['actor'] ?? '', $object_data['tags'], true, false); + + foreach ($receiverdata as $key => $data) { + if ($data['uid'] !== -1) { + $object_data['reception_type'][$data['uid']] = $data['type'] ?? 0; + } + + if ($key !== -1) { + $object_data['receiver'][$key] = $data['uid']; + } else { + $object_data['unlisted'] = true; + } + } if (!empty($object['pixelfed:capabilities'])) { $object_data['capabilities'] = self::getCapabilities($object); } - $object_data['unlisted'] = in_array(-1, $object_data['receiver']); - unset($object_data['receiver'][-1]); - unset($object_data['reception_type'][-1]); - return $object_data; } - private static function getCapabilities($object) { + private static function getCapabilities($object) + { $capabilities = []; foreach (['pixelfed:canAnnounce', 'pixelfed:canLike', 'pixelfed:canReply'] as $element) { $capabilities_list = JsonLD::fetchElementArray($object['pixelfed:capabilities'], $element, '@id'); @@ -1976,8 +1944,9 @@ class Receiver public static function getObjectDataFromActivity(array $object): array { $object_data = []; + $object_data['object_type'] = JsonLD::fetchElement($object, '@type'); - $object_data['id'] = JsonLD::fetchElement($object, '@id'); + $object_data['id'] = JsonLD::fetchElement($object, '@id'); $object_data['reply-to-id'] = JsonLD::fetchElement($object, 'as:inReplyTo', '@id'); // An empty "id" field is translated to "./" by the compactor, so we have to check for this content @@ -1995,13 +1964,13 @@ class Receiver // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field. $replyToId = Item::getURIByLink($object_data['reply-to-id']); if (!empty($replyToId) && ($object_data['reply-to-id'] != $replyToId)) { - Logger::notice('Fix wrong reply-to', ['received' => $object_data['reply-to-id'], 'correct' => $replyToId]); + DI::logger()->notice('Fix wrong reply-to', ['received' => $object_data['reply-to-id'], 'correct' => $replyToId]); $object_data['reply-to-id'] = $replyToId; } } $object_data['published'] = JsonLD::fetchElement($object, 'as:published', '@value'); - $object_data['updated'] = JsonLD::fetchElement($object, 'as:updated', '@value'); + $object_data['updated'] = JsonLD::fetchElement($object, 'as:updated', '@value'); if (empty($object_data['updated'])) { $object_data['updated'] = $object_data['published']; @@ -2025,36 +1994,37 @@ class Receiver $location = BBCode::toPlaintext($location); } - $object_data['sc:identifier'] = JsonLD::fetchElement($object, 'sc:identifier', '@value'); - $object_data['diaspora:guid'] = JsonLD::fetchElement($object, 'diaspora:guid', '@value'); - $object_data['diaspora:comment'] = JsonLD::fetchElement($object, 'diaspora:comment', '@value'); - $object_data['diaspora:like'] = JsonLD::fetchElement($object, 'diaspora:like', '@value'); - $object_data['actor'] = $object_data['author'] = $actor; - $element = JsonLD::fetchElement($object, 'as:context', '@id'); - $object_data['context'] = $element != './' ? $element : null; - $element = JsonLD::fetchElement($object, 'ostatus:conversation', '@id'); - $object_data['conversation'] = $element != './' ? $element : null; - $object_data['sensitive'] = JsonLD::fetchElement($object, 'as:sensitive'); - $object_data['name'] = JsonLD::fetchElement($object, 'as:name', '@value'); - $object_data['summary'] = JsonLD::fetchElement($object, 'as:summary', '@value'); - $object_data['content'] = JsonLD::fetchElement($object, 'as:content', '@value'); - $object_data['mediatype'] = JsonLD::fetchElement($object, 'as:mediaType', '@value'); - $object_data = self::getSource($object, $object_data); - $object_data['start-time'] = JsonLD::fetchElement($object, 'as:startTime', '@value'); - $object_data['end-time'] = JsonLD::fetchElement($object, 'as:endTime', '@value'); - $object_data['location'] = $location; - $object_data['latitude'] = JsonLD::fetchElement($object, 'as:location', 'as:latitude', '@type', 'as:Place'); - $object_data['latitude'] = JsonLD::fetchElement($object_data, 'latitude', '@value'); - $object_data['longitude'] = JsonLD::fetchElement($object, 'as:location', 'as:longitude', '@type', 'as:Place'); - $object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value'); - $object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment') ?? []); - $object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag') ?? []); - $object_data['emojis'] = self::processEmojis(JsonLD::fetchElementArray($object, 'as:tag', null, '@type', 'toot:Emoji') ?? []); - $object_data['languages'] = self::processLanguages(JsonLD::fetchElementArray($object, 'sc:inLanguage') ?? []); + $object_data['sc:identifier'] = JsonLD::fetchElement($object, 'sc:identifier', '@value'); + $object_data['diaspora:guid'] = JsonLD::fetchElement($object, 'diaspora:guid', '@value'); + $object_data['diaspora:comment'] = JsonLD::fetchElement($object, 'diaspora:comment', '@value'); + $object_data['diaspora:like'] = JsonLD::fetchElement($object, 'diaspora:like', '@value'); + $object_data['actor'] = $object_data['author'] = $actor; + $element = JsonLD::fetchElement($object, 'as:context', '@id'); + $object_data['context'] = $element != './' ? $element : null; + $element = JsonLD::fetchElement($object, 'ostatus:conversation', '@id'); + $object_data['conversation'] = $element != './' ? $element : null; + $object_data['sensitive'] = JsonLD::fetchElement($object, 'as:sensitive'); + $object_data['name'] = JsonLD::fetchElement($object, 'as:name', '@value'); + $object_data['summary'] = JsonLD::fetchElement($object, 'as:summary', '@value'); + $object_data['content'] = JsonLD::fetchElement($object, 'as:content', '@value'); + $object_data['mediatype'] = JsonLD::fetchElement($object, 'as:mediaType', '@value'); + $object_data = self::getSource($object, $object_data); + $object_data['start-time'] = JsonLD::fetchElement($object, 'as:startTime', '@value'); + $object_data['end-time'] = JsonLD::fetchElement($object, 'as:endTime', '@value'); + $object_data['location'] = $location; + $object_data['latitude'] = JsonLD::fetchElement($object, 'as:location', 'as:latitude', '@type', 'as:Place'); + $object_data['latitude'] = JsonLD::fetchElement($object_data, 'latitude', '@value'); + $object_data['longitude'] = JsonLD::fetchElement($object, 'as:location', 'as:longitude', '@type', 'as:Place'); + $object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value'); + $object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment') ?? []); + $object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag') ?? []); + $object_data['emojis'] = self::processEmojis(JsonLD::fetchElementArray($object, 'as:tag', null, '@type', 'toot:Emoji') ?? []); + $object_data['languages'] = self::processLanguages(JsonLD::fetchElementArray($object, 'sc:inLanguage') ?? []); $object_data['transmitted-languages'] = Processor::getPostLanguages($object); - $object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application'); - $object_data['generator'] = JsonLD::fetchElement($object_data, 'generator', '@value'); - $object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url', '@id'); + $object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application'); + $object_data['generator'] = JsonLD::fetchElement($object_data, 'generator', '@value'); + $object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url', '@id'); + $object_data['replies'] = JsonLD::fetchElement($object, 'as:replies', '@id'); // Special treatment for Hubzilla links if (is_array($object_data['alternate-url'])) { @@ -2071,7 +2041,7 @@ class Receiver if (in_array($object_data['object_type'], ['as:Audio', 'as:Video'])) { $object_data['alternate-url'] = self::extractAlternateUrl($object['as:url'] ?? []) ?: $object_data['alternate-url']; - $object_data['attachments'] = array_merge($object_data['attachments'], self::processAttachmentUrls($object['as:url'] ?? [])); + $object_data['attachments'] = array_merge($object_data['attachments'], self::processAttachmentUrls($object['as:url'] ?? [])); } $object_data['can-comment'] = JsonLD::fetchElement($object, 'pt:commentsEnabled', '@value'); @@ -2080,12 +2050,24 @@ class Receiver } // Support for quoted posts (Pleroma, Fedibird and Misskey) - $object_data['quote-url'] = JsonLD::fetchElement($object, 'as:quoteUrl', '@value'); + $object_data['quote-url'] = JsonLD::fetchElement($object, 'as:quoteUrl', '@id'); + if (empty($object_data['quote-url'])) { + $object_data['quote-url'] = JsonLD::fetchElement($object, 'as:quoteUrl', '@value'); + } + if (empty($object_data['quote-url'])) { + $object_data['quote-url'] = JsonLD::fetchElement($object, 'fedibird:quoteUri', '@id'); + } if (empty($object_data['quote-url'])) { $object_data['quote-url'] = JsonLD::fetchElement($object, 'fedibird:quoteUri', '@value'); } if (empty($object_data['quote-url'])) { - $object_data['quote-url'] = JsonLD::fetchElement($object, 'misskey:_misskey_quote', '@value'); + $object_data['quote-url'] = JsonLD::fetchElement($object, 'misskey:_misskey_quote', '@id'); + } + + foreach ($object_data['tags'] as $tag) { + if (HTTPSignature::isValidContentType($tag['mediaType'] ?? '', $tag['href'] ?? '')) { + $object_data['quote-url'] = $tag['href']; + } } // Misskey adds some data to the standard "content" value for quoted posts for backwards compatibility. diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 94655eead1..712b8954d4 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\ActivityPub; @@ -26,14 +12,12 @@ use Friendica\Content\Feature; use Friendica\Content\Smilies; use Friendica\Content\Text\BBCode; use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\Contact; -use Friendica\Model\GServer; use Friendica\Model\Item; use Friendica\Model\Photo; use Friendica\Model\Post; @@ -96,7 +80,7 @@ class Transmitter } foreach ($relays as $relay) { - $contact = Contact::getByURLForUser($relay['url'], $item['uid'], false, ['id']); + $contact = Contact::getByURLForUser($relay['url'], $item['uid'], false, ['id']); $inboxes[$relay['batch']][] = $contact['id'] ?? 0; } return $inboxes; @@ -116,7 +100,7 @@ class Transmitter } $activity_id = self::activityIDFromContact($contact['id']); - $success = self::sendActivity('Follow', $url, 0, $activity_id); + $success = self::sendActivity('Follow', $url, 0, $activity_id); if ($success) { Contact::update(['rel' => Contact::FRIEND], ['id' => $contact['id']]); } @@ -162,19 +146,19 @@ class Transmitter public static function getContacts(array $owner, array $rel, string $module, int $page = null, string $requester = null, bool $nocache = false): array { if (empty($page)) { - $cachekey = self::CACHEKEY_CONTACTS . $module . ':'. $owner['uid']; - $result = DI::cache()->get($cachekey); + $cachekey = self::CACHEKEY_CONTACTS . $module . ':' . $owner['uid']; + $result = DI::cache()->get($cachekey); if (!$nocache && !is_null($result)) { return $result; } } $parameters = [ - 'rel' => $rel, - 'uid' => $owner['uid'], - 'self' => false, + 'rel' => $rel, + 'uid' => $owner['uid'], + 'self' => false, 'deleted' => false, - 'hidden' => false, + 'hidden' => false, 'archive' => false, 'pending' => false, 'blocked' => false, @@ -186,9 +170,9 @@ class Transmitter $modulePath = '/' . $module . '/'; - $data = ['@context' => ActivityPub::CONTEXT]; - $data['id'] = DI::baseUrl() . $modulePath . $owner['nickname']; - $data['type'] = 'OrderedCollection'; + $data = ['@context' => ActivityPub::CONTEXT]; + $data['id'] = DI::baseUrl() . $modulePath . $owner['nickname']; + $data['type'] = 'OrderedCollection'; $data['totalItems'] = $total; if (!empty($page)) { @@ -215,7 +199,7 @@ class Transmitter $data['first'] = DI::baseUrl() . $modulePath . $owner['nickname'] . '?page=1'; } else { $data['type'] = 'OrderedCollectionPage'; - $list = []; + $list = []; $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]); while ($contact = DBA::fetch($contacts)) { @@ -254,7 +238,7 @@ class Transmitter { if (empty($page)) { $cachekey = self::CACHEKEY_FEATURED . $owner['uid']; - $result = DI::cache()->get($cachekey); + $result = DI::cache()->get($cachekey); if (!$nocache && !is_null($result)) { return $result; } @@ -262,11 +246,13 @@ class Transmitter $owner_cid = Contact::getIdForURL($owner['url'], 0, false); - $condition = ["`uri-id` IN (SELECT `uri-id` FROM `collection-view` WHERE `cid` = ? AND `type` = ?)", - $owner_cid, Post\Collection::FEATURED]; + $condition = [ + "`uri-id` IN (SELECT `uri-id` FROM `collection-view` WHERE `cid` = ? AND `type` = ?)", + $owner_cid, Post\Collection::FEATURED + ]; $condition = DBA::mergeConditions($condition, [ - 'uid' => $owner['uid'], + 'uid' => $owner['uid'], 'author-id' => $owner_cid, 'private' => [Item::PUBLIC, Item::UNLISTED], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT], @@ -279,9 +265,9 @@ class Transmitter $count = Post::count($condition); - $data = ['@context' => ActivityPub::CONTEXT]; - $data['id'] = DI::baseUrl() . '/featured/' . $owner['nickname']; - $data['type'] = 'OrderedCollection'; + $data = ['@context' => ActivityPub::CONTEXT]; + $data['id'] = DI::baseUrl() . '/featured/' . $owner['nickname']; + $data['type'] = 'OrderedCollection'; $data['totalItems'] = $count; if (!empty($page)) { @@ -292,7 +278,7 @@ class Transmitter $items = Post::select(['id'], $condition, ['limit' => 20, 'order' => ['created' => true]]); } else { $data['type'] = 'OrderedCollectionPage'; - $items = Post::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]); + $items = Post::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]); } $list = []; @@ -322,16 +308,17 @@ class Transmitter } /** - * Return the service array containing information the used software and it's url + * Return the service array containing information the used software and its url * * @return array with service data */ public static function getService(): array { return [ - 'type' => 'Service', - 'name' => App::PLATFORM . " '" . App::CODENAME . "' " . App::VERSION . '-' . DB_UPDATE_VERSION, - 'url' => (string)DI::baseUrl() + 'id' => (string)DI::baseUrl() . '/friendica', + 'type' => 'Application', + 'name' => App::PLATFORM . " '" . App::CODENAME . "' " . App::VERSION . '-' . DB_UPDATE_VERSION, + 'url' => (string)DI::baseUrl(), ]; } @@ -352,7 +339,7 @@ class Transmitter throw new HTTPException\NotFoundException('User not found.'); } - $data = ['@context' => ActivityPub::CONTEXT]; + $data = ['@context' => ActivityPub::CONTEXT]; $data['id'] = $owner['url']; if (!empty($owner['guid'])) { @@ -373,11 +360,13 @@ class Transmitter } $data['preferredUsername'] = $owner['nick']; - $data['name'] = $full ? $owner['name'] : $owner['nick']; + $data['name'] = $full ? $owner['name'] : $owner['nick']; if ($full && !empty($owner['country-name'] . $owner['region'] . $owner['locality'])) { - $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $owner['country-name'], - 'vcard:region' => $owner['region'], 'vcard:locality' => $owner['locality']]; + $data['vcard:hasAddress'] = [ + '@type' => 'vcard:Home', 'vcard:country-name' => $owner['country-name'], + 'vcard:region' => $owner['region'], 'vcard:locality' => $owner['locality'] + ]; } if ($full && !empty($owner['about'])) { @@ -395,12 +384,14 @@ class Transmitter } } - $data['url'] = $owner['url']; + $data['url'] = $owner['url']; $data['manuallyApprovesFollowers'] = in_array($owner['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]); - $data['discoverable'] = (bool)$owner['net-publish']; - $data['publicKey'] = ['id' => $owner['url'] . '#main-key', - 'owner' => $owner['url'], - 'publicKeyPem' => $owner['pubkey']]; + $data['discoverable'] = (bool)$owner['net-publish'] && $full; + $data['publicKey'] = [ + 'id' => $owner['url'] . '#main-key', + 'owner' => $owner['url'], + 'publicKeyPem' => $owner['pubkey'] + ]; $data['endpoints'] = ['sharedInbox' => DI::baseUrl() . '/inbox']; if ($full && $uid != 0) { $data['icon'] = ['type' => 'Image', 'url' => User::getAvatarUrl($owner)]; @@ -429,8 +420,8 @@ class Transmitter foreach (DI::profileField()->selectByContactId(0, $uid) as $profile_field) { $custom_fields[] = [ - 'type' => 'PropertyValue', - 'name' => $profile_field->label, + 'type' => 'PropertyValue', + 'name' => $profile_field->label, 'value' => BBCode::convertForUriId($owner['uri-id'], $profile_field->value) ]; }; @@ -455,7 +446,7 @@ class Transmitter private static function getActorArrayByCid(int $cid): array { $contact = Contact::getById($cid); - $data = [ + $data = [ 'id' => $contact['url'], 'type' => $data['type'] = ActivityPub::ACCOUNT_TYPES[$contact['contact-type']], 'url' => $contact['alias'], @@ -482,12 +473,12 @@ class Transmitter public static function getDeletedUser(string $username): array { return [ - '@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/profile/' . $username, - 'type' => 'Tombstone', + '@context' => ActivityPub::CONTEXT, + 'id' => DI::baseUrl() . '/profile/' . $username, + 'type' => 'Tombstone', 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), - 'updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), - 'deleted' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), + 'updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), + 'deleted' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), ]; } @@ -512,17 +503,17 @@ class Transmitter } $permissions = [ - 'to' => [$parent['author-link']], - 'cc' => [], - 'bto' => [], - 'bcc' => [], + 'to' => [$parent['author-link']], + 'cc' => [], + 'bto' => [], + 'bcc' => [], 'audience' => [], ]; $parent_profile = APContact::getByURL($parent['author-link']); $item_profile = APContact::getByURL($item['author-link']); - $exclude[] = $item['author-link']; + $exclude[] = $item['author-link']; if ($item['gravity'] == Item::GRAVITY_PARENT) { $exclude[] = $item['owner-link']; @@ -580,11 +571,12 @@ class Transmitter $exclusive = false; $mention = false; $audience = []; + $owner = false; // Check if we should always deliver our stuff via BCC if (!empty($item['uid'])) { $owner = User::getOwnerDataById($item['uid']); - if (!empty($owner)) { + if (is_array($owner)) { $always_bcc = $owner['hide-friends']; $is_group = ($owner['account-type'] == User::ACCOUNT_TYPE_COMMUNITY); @@ -599,7 +591,7 @@ class Transmitter $parent = Post::selectFirst(['causer-link', 'post-reason'], ['id' => $item['parent']]); if (!empty($parent) && ($parent['post-reason'] == Item::PR_ANNOUNCEMENT) && !empty($parent['causer-link'])) { - $profile = APContact::getByURL($parent['causer-link'], false); + $profile = APContact::getByURL($parent['causer-link'], false); $is_group_thread = isset($profile['type']) && $profile['type'] == 'Group'; } else { $is_group_thread = false; @@ -615,7 +607,7 @@ class Transmitter } $profile = APContact::getByURL($tag['url'], false); if (!empty($profile) && ($profile['type'] == 'Group')) { - $audience[] = $tag['url']; + $audience[] = $tag['url']; $is_group_thread = true; } } @@ -644,15 +636,7 @@ class Transmitter $audience[] = $owner['url']; } - if (self::isAnnounce($item) || self::isAPPost($last_id)) { - // Will be activated in a later step - $networks = Protocol::FEDERATED; - } else { - // For now only send to these contacts: - $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS]; - } - - $data = ['to' => [], 'cc' => [], 'bcc' => [] , 'audience' => $audience]; + $data = ['to' => [], 'cc' => [], 'bto' => [], 'bcc' => [], 'audience' => $audience]; if ($item['gravity'] == Item::GRAVITY_PARENT) { $actor_profile = APContact::getByURL($item['owner-link']); @@ -711,7 +695,7 @@ class Transmitter $cid = Contact::getIdForURL($term['url'], $item['uid']); if (!empty($cid) && in_array($cid, $receiver_list)) { $contact = DBA::selectFirst('contact', ['url', 'network', 'protocol', 'gsid'], ['id' => $cid, 'network' => Protocol::FEDERATED]); - if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) { + if (!DBA::isResult($contact)) { continue; } @@ -748,7 +732,7 @@ class Transmitter } $contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]); - if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) { + if (!DBA::isResult($contact)) { continue; } @@ -764,7 +748,7 @@ class Transmitter } if (!empty($item['parent']) && (!$exclusive || ($item['private'] == Item::PRIVATE))) { - if ($item['private'] == Item::PRIVATE) { + if ($item['private'] == Item::PRIVATE || $item['gravity'] == Item::GRAVITY_ACTIVITY) { $condition = ['parent' => $item['parent'], 'uri-id' => $item['thr-parent-id']]; } else { $condition = ['parent' => $item['parent']]; @@ -806,49 +790,29 @@ class Transmitter if (($profile['type'] == 'Group') || ($parent['uri'] == $item['thr-parent'])) { $data['to'][] = $profile['url']; } else { - $data['cc'][] = $profile['url']; + $data['bto'][] = $profile['url']; } } } DBA::close($parents); } - $data['to'] = array_unique($data['to']); - $data['cc'] = array_unique($data['cc']); - $data['bcc'] = array_unique($data['bcc']); - $data['audience'] = array_unique($data['audience']); - - if (($key = array_search($item['author-link'], $data['to'])) !== false) { - unset($data['to'][$key]); - } - - if (($key = array_search($item['author-link'], $data['cc'])) !== false) { - unset($data['cc'][$key]); - } - - if (($key = array_search($item['author-link'], $data['bcc'])) !== false) { - unset($data['bcc'][$key]); - } - - foreach ($data['to'] as $to) { - if (($key = array_search($to, $data['cc'])) !== false) { - unset($data['cc'][$key]); - } - - if (($key = array_search($to, $data['bcc'])) !== false) { - unset($data['bcc'][$key]); + if (!empty($item['quote-uri-id']) && in_array($item['private'], [Item::PUBLIC, Item::UNLISTED])) { + $quoted = Post::selectFirst(['author-link'], ['uri-id' => $item['quote-uri-id']]); + if (!empty($quoted['author-link'])) { + $profile = APContact::getByURL($quoted['author-link'], false); + if (!empty($profile)) { + $data['cc'][] = $profile['url']; + } } } - foreach ($data['cc'] as $cc) { - if (($key = array_search($cc, $data['bcc'])) !== false) { - unset($data['bcc'][$key]); - } - } + $data = self::filterReceiverData($data, $item['author-link']); - $receivers = ['to' => array_values($data['to']), 'cc' => array_values($data['cc']), 'bcc' => array_values($data['bcc']), 'audience' => array_values($data['audience'])]; + $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']); } @@ -861,6 +825,63 @@ class Transmitter 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($author_link, $data['to'])) !== false) { + unset($data['to'][$key]); + } + + if (($key = array_search($author_link, $data['cc'])) !== false) { + unset($data['cc'][$key]); + } + + if (($key = array_search($author_link, $data['bto'])) !== false) { + unset($data['bto'][$key]); + } + + if (($key = array_search($author_link, $data['bcc'])) !== false) { + unset($data['bcc'][$key]); + } + + foreach ($data['to'] as $to) { + if (($key = array_search($to, $data['cc'])) !== false) { + unset($data['cc'][$key]); + } + + if (($key = array_search($to, $data['bto'])) !== false) { + unset($data['bto'][$key]); + } + + if (($key = array_search($to, $data['bcc'])) !== false) { + unset($data['bcc'][$key]); + } + } + + foreach ($data['cc'] as $cc) { + if (($key = array_search($cc, $data['bto'])) !== false) { + unset($data['bto'][$key]); + } + + if (($key = array_search($cc, $data['bcc'])) !== false) { + unset($data['bcc'][$key]); + } + } + + foreach ($data['bcc'] as $cc) { + if (($key = array_search($cc, $data['bto'])) !== false) { + unset($data['bto'][$key]); + } + } + + return $data; + } + /** * Store the receivers for the given item * @@ -874,7 +895,7 @@ class Transmitter return; } - foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bcc' => Tag::BCC, 'audience' => Tag::AUDIENCE] as $element => $type) { + foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bto' => Tag::BTO, 'bcc' => Tag::BCC, 'audience' => Tag::AUDIENCE] as $element => $type) { if (!empty($receivers[$element])) { foreach ($receivers[$element] as $receiver) { if ($receiver == ActivityPub::PUBLIC_COLLECTION) { @@ -890,29 +911,26 @@ class Transmitter /** * Get a list of receivers for the provided uri-id - * - * @param array $item - * @param boolean $blindcopy - * @return void */ - public static function getReceiversForUriId(int $uri_id, bool $blindcopy) + public static function getReceiversForUriId(int $uri_id, bool $blindcopy): array { - $tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BCC, Tag::AUDIENCE]); + $tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC, Tag::AUDIENCE]); if (empty($tags)) { - Logger::debug('No receivers found', ['uri-id' => $uri_id]); + DI::logger()->debug('No receivers found', ['uri-id' => $uri_id]); $post = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $uri_id, 'origin' => true]); if (!empty($post)) { ActivityPub\Transmitter::storeReceiversForItem($post); - $tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BCC, Tag::AUDIENCE]); - Logger::debug('Receivers are created', ['uri-id' => $uri_id, 'receivers' => count($tags)]); + $tags = Tag::getByURIId($uri_id, [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC, Tag::AUDIENCE]); + DI::logger()->debug('Receivers are created', ['uri-id' => $uri_id, 'receivers' => count($tags)]); } else { - Logger::debug('Origin item not found', ['uri-id' => $uri_id]); + DI::logger()->debug('Origin item not found', ['uri-id' => $uri_id]); } } $receivers = [ 'to' => [], 'cc' => [], + 'bto' => [], 'bcc' => [], 'audience' => [], ]; @@ -925,6 +943,9 @@ class Transmitter case Tag::CC: $receivers['cc'][] = $receiver['url']; break; + case Tag::BTO: + $receivers['bto'][] = $receiver['url']; + break; case Tag::BCC: $receivers['bcc'][] = $receiver['url']; break; @@ -935,6 +956,7 @@ class Transmitter } if (!$blindcopy) { + unset($receivers['bto']); unset($receivers['bcc']); } @@ -958,94 +980,68 @@ class Transmitter return DBA::exists('inbox-status', ['url' => $url, 'archive' => true]); } - /** - * Check if a given contact should be delivered via AP - * - * @param array $contact Contact array - * @param array $networks Array with networks - * @return bool Whether the used protocol matches ACTIVITYPUB - * @throws Exception - */ - private static function isAPContact(array $contact, array $networks): bool - { - if (in_array($contact['network'], $networks) || ($contact['protocol'] == Protocol::ACTIVITYPUB)) { - return true; - } - - return GServer::getProtocol($contact['gsid'] ?? 0) == Post\DeliveryData::ACTIVITYPUB; - } - /** * Fetches a list of inboxes of followers of a given user * * @param integer $uid User ID - * @param boolean $all_ap Retrieve all AP enabled inboxes * @return array of follower inboxes * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function fetchTargetInboxesforUser(int $uid, bool $all_ap = false): array + public static function fetchTargetInboxesforUser(int $uid): array { - $inboxes = []; - - $isGroup = false; - if (!empty($item['uid'])) { - $profile = User::getOwnerDataById($item['uid']); - if (!empty($profile)) { - $isGroup = $profile['account-type'] == User::ACCOUNT_TYPE_COMMUNITY; - } - } - - if ($all_ap) { - // Will be activated in a later step - $networks = Protocol::FEDERATED; - } else { - // For now only send to these contacts: - $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS]; - } - $condition = [ - 'uid' => $uid, - 'self' => false, - 'archive' => false, - 'pending' => false, - 'blocked' => false, - 'network' => Protocol::FEDERATED, - 'contact-type' => [Contact::TYPE_UNKNOWN, Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION], + 'uid' => $uid, + 'self' => false, + 'archive' => false, + 'pending' => false, + 'blocked' => false, + 'network' => Protocol::FEDERATED, ]; if (!empty($uid)) { $condition['rel'] = [Contact::FOLLOWER, Contact::FRIEND]; } - $contacts = DBA::select('contact', ['id', 'url', 'network', 'protocol', 'gsid'], $condition); - while ($contact = DBA::fetch($contacts)) { - if (!self::isAPContact($contact, $networks)) { + return self::addInboxesForCondition($condition, []); + } + + /** + * Fetch inboxes for a list of contacts + * + * @param array $recipients + * @param array $inboxes + * @return array + */ + public static function addInboxesForRecipients(array $recipients, array $inboxes): array + { + return self::addInboxesForCondition(['id' => $recipients], $inboxes); + } + + /** + * Get a list of inboxes for a given contact condition + * + * @param array $condition + * @param array $inboxes + * @return array + */ + private static function addInboxesForCondition(array $condition, array $inboxes): array + { + $condition = DBA::mergeConditions($condition, ["(`ap-inbox` IS NOT NULL OR `ap-sharedinbox` IS NOT NULL)"]); + + $accounts = DBA::select('account-user-view', ['id', 'url', 'ap-inbox', 'ap-sharedinbox'], $condition); + while ($account = DBA::fetch($accounts)) { + if (!empty($account['ap-sharedinbox']) && !Contact::isLocal($account['url'])) { + $target = $account['ap-sharedinbox']; + } elseif (!empty($account['ap-inbox'])) { + $target = $account['ap-inbox']; + } else { continue; } - - if ($isGroup && ($contact['network'] == Protocol::DFRN)) { - continue; - } - - if (Network::isUrlBlocked($contact['url'])) { - continue; - } - - $profile = APContact::getByURL($contact['url'], false); - if (!empty($profile)) { - if (empty($profile['sharedinbox']) || Contact::isLocal($contact['url'])) { - $target = $profile['inbox']; - } else { - $target = $profile['sharedinbox']; - } - if (!self::archivedInbox($target)) { - $inboxes[$target][] = $contact['id']; - } + if (!Transmitter::archivedInbox($target) && (empty($inboxes[$target]) || !in_array($account['id'], $inboxes[$target]))) { + $inboxes[$target][] = $account['id']; } } - DBA::close($contacts); - return $inboxes; } @@ -1084,7 +1080,7 @@ class Transmitter continue; } - $blindcopy = in_array($element, ['bto', 'bcc']); + $blindcopy = in_array($element, ['bcc']); foreach ($permissions[$element] as $receiver) { if (empty($receiver) || Network::isUrlBlocked($receiver)) { @@ -1092,7 +1088,7 @@ class Transmitter } if ($item_profile && ($receiver == $item_profile['followers']) && ($uid == $profile_uid)) { - $inboxes = array_merge_recursive($inboxes, self::fetchTargetInboxesforUser($uid, true)); + $inboxes = array_merge_recursive($inboxes, self::fetchTargetInboxesforUser($uid)); } else { $profile = APContact::getByURL($receiver, false); if (!empty($profile)) { @@ -1159,14 +1155,15 @@ class Transmitter // - Moving the title into the "summary" field that is used as a "content warning" if (!$use_title) { - $mail['body'] = '[abstract]' . $mail['title'] . "[/abstract]\n" . $mail['body']; - $mail['title'] = ''; + $mail['content-warning'] = $mail['title']; + $mail['title'] = ''; + } else { + $mail['content-warning'] = ''; } - - $mail['content-warning'] = ''; + $mail['sensitive'] = false; $mail['author-link'] = $mail['owner-link'] = $mail['from-url']; $mail['owner-id'] = $mail['author-id']; - $mail['allow_cid'] = '<'.$mail['contact-id'].'>'; + $mail['allow_cid'] = '<' . $mail['contact-id'] . '>'; $mail['allow_gid'] = ''; $mail['deny_cid'] = ''; $mail['deny_gid'] = ''; @@ -1177,7 +1174,7 @@ class Transmitter $mail['parent-uri'] = $reply['uri']; $mail['parent-uri-id'] = $reply['uri-id']; $mail['parent-author-id'] = Contact::getIdForURL($reply['from-url'], 0, false); - $mail['gravity'] = ($mail['reply'] ? Item::GRAVITY_COMMENT: Item::GRAVITY_PARENT); + $mail['gravity'] = ($mail['reply'] ? Item::GRAVITY_COMMENT : Item::GRAVITY_PARENT); $mail['event-type'] = ''; $mail['language'] = ''; $mail['parent'] = 0; @@ -1208,12 +1205,12 @@ class Transmitter $data = []; } - $data['id'] = $mail['uri'] . '/Create'; - $data['type'] = 'Create'; - $data['actor'] = $mail['author-link']; - $data['published'] = DateTimeFormat::utc($mail['created'] . '+00:00', DateTimeFormat::ATOM); + $data['id'] = $mail['uri'] . '/Create'; + $data['type'] = 'Create'; + $data['actor'] = $mail['author-link']; + $data['published'] = DateTimeFormat::utc($mail['created'] . '+00:00', DateTimeFormat::ATOM); $data['instrument'] = self::getService(); - $data = array_merge($data, self::createPermissionBlockForItem($mail, true)); + $data = array_merge($data, self::createPermissionBlockForItem($mail, true)); if (empty($data['to']) && !empty($data['cc'])) { $data['to'] = $data['cc']; @@ -1227,7 +1224,7 @@ class Transmitter unset($data['bcc']); unset($data['audience']); - $object['to'] = $data['to']; + $object['to'] = $data['to']; $object['tag'] = [['type' => 'Mention', 'href' => $object['to'][0], 'name' => '']]; unset($object['cc']); @@ -1339,7 +1336,7 @@ class Transmitter if (!$api_mode) { $condition['parent-network'] = Protocol::NATIVE_SUPPORT; } - Logger::info('Fetching activity', $condition); + DI::logger()->info('Fetching activity', $condition); $item = Post::selectFirst(Item::DELIVER_FIELDLIST, $condition); if (!DBA::isResult($item)) { return false; @@ -1350,8 +1347,6 @@ class Transmitter /** * Creates an activity array for a given URI-Id and uid * - * @param integer $uri_id - * @param integer $uid * @param boolean $object_mode true = Create the object, false = create the activity with the object * @param boolean $api_mode true = used for the API * @param boolean $announce_activity true = the announced object is the activity, false = we announce the object link @@ -1364,7 +1359,7 @@ class Transmitter if (!$api_mode) { $condition['parent-network'] = Protocol::NATIVE_SUPPORT; } - Logger::info('Fetching activity', $condition); + DI::logger()->info('Fetching activity', $condition); $item = Post::selectFirst(Item::DELIVER_FIELDLIST, $condition, ['order' => ['uid' => true]]); if (!DBA::isResult($item)) { return false; @@ -1376,7 +1371,6 @@ class Transmitter /** * Creates an activity array for a given item id * - * @param integer $item_id * @param boolean $object_mode true = Create the object, false = create the activity with the object * @param boolean $api_mode true = used for the API * @param boolean $announce_activity true = the announced object is the activity, false = we announce the object link @@ -1389,24 +1383,24 @@ class Transmitter $data = Post\Activity::getByURIId($item['uri-id']); if (!$item['origin'] && !empty($data)) { if (!$object_mode) { - Logger::info('Return stored conversation', ['item' => $item['id']]); + DI::logger()->info('Return stored conversation', ['item' => $item['id']]); return $data; } elseif (!empty($data['object'])) { - Logger::info('Return stored conversation object', ['item' => $item['id']]); + DI::logger()->info('Return stored conversation object', ['item' => $item['id']]); return $data['object']; } } } - if (!$api_mode && !$item['origin']) { - Logger::debug('Post is not ours and is not stored', ['id' => $item['id'], 'uri-id' => $item['uri-id']]); + if (!$api_mode && !$item['deleted'] && !$item['origin']) { + DI::logger()->debug('Post is not ours and is not stored', ['id' => $item['id'], 'uri-id' => $item['uri-id']]); return false; } $type = self::getTypeOfItem($item); if (!$object_mode) { - $data = ['@context' => $context ?? ActivityPub::CONTEXT]; + $data = ['@context' => ActivityPub::CONTEXT]; if ($item['deleted'] && ($item['gravity'] == Item::GRAVITY_ACTIVITY)) { $type = 'Undo'; @@ -1418,7 +1412,8 @@ class Transmitter } if ($type == 'Delete') { - $data['id'] = Item::newURI($item['guid']) . '/' . $type;; + $data['id'] = Item::newURI($item['guid']) . '/' . $type; + ; } elseif (($item['gravity'] == Item::GRAVITY_ACTIVITY) && ($type != 'Undo')) { $data['id'] = $item['uri']; } else { @@ -1454,7 +1449,7 @@ class Transmitter } elseif ($data['type'] == 'Announce') { if ($item['verb'] == ACTIVITY::ANNOUNCE) { if ($announce_activity) { - $anounced_item = Post::selectFirst(['uid'], ['uri-id' => $item['thr-parent-id'], 'origin' => true]); + $anounced_item = Post::selectFirst(['uid'], ['uri-id' => $item['thr-parent-id'], 'origin' => true]); $data['object'] = self::createActivityFromUriId($item['thr-parent-id'], $anounced_item['uid'] ?? 0); unset($data['object']['@context']); } else { @@ -1481,7 +1476,7 @@ class Transmitter $uid = $item['uid']; } - Logger::info('Fetched activity', ['item' => $item['id'], 'uid' => $uid]); + DI::logger()->info('Fetched activity', ['item' => $item['id'], 'uid' => $uid]); // We only sign our own activities if (!$api_mode && !$object_mode && $item['origin']) { @@ -1520,7 +1515,7 @@ class Transmitter } if (!empty($coord['lat']) && !empty($coord['lon'])) { - $location['latitude'] = $coord['lat']; + $location['latitude'] = $coord['lat']; $location['longitude'] = $coord['lon']; } @@ -1543,7 +1538,7 @@ class Transmitter 'name' => $name, 'icon' => [ 'type' => 'Image', - 'url' => $url, + 'url' => $url, ], ]; } @@ -1565,7 +1560,7 @@ class Transmitter $terms = Tag::getByURIId($item['uri-id'], [Tag::HASHTAG, Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); foreach ($terms as $term) { if ($term['type'] == Tag::HASHTAG) { - $url = DI::baseUrl() . '/search?tag=' . urlencode($term['name']); + $url = DI::baseUrl() . '/search?tag=' . urlencode($term['name']); $tags[] = ['type' => 'Hashtag', 'href' => $url, 'name' => '#' . $term['name']]; } else { $contact = Contact::getByURL($term['url'], false, ['addr']); @@ -1588,15 +1583,14 @@ class Transmitter $tags[] = ['type' => 'Mention', 'href' => $announce['actor']['url'], 'name' => '@' . $announce['actor']['addr']]; } - // @see https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-e232.md + // @see https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md if (!empty($quote_url)) { - // Currently deactivated because of compatibility issues with Pleroma - //$tags[] = [ - // 'type' => 'Link', - // 'mediaType' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - // 'href' => $quote_url, - // 'name' => '♲ ' . BBCode::convertForUriId($item['uri-id'], $quote_url, BBCode::ACTIVITYPUB) - //]; + $tags[] = [ + 'type' => 'Link', + 'mediaType' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'href' => $quote_url, + 'name' => 'RE: ' . $quote_url, + ]; } return $tags; @@ -1621,10 +1615,12 @@ class Transmitter } $urls[] = $attachment['url']; - $attach = ['type' => 'Document', + $attach = [ + 'type' => 'Document', 'mediaType' => $attachment['mimetype'], - 'url' => $attachment['url'], - 'name' => $attachment['description']]; + 'url' => $attachment['url'], + 'name' => $attachment['description'] + ]; if (!empty($attachment['height'])) { $attach['height'] = $attachment['height']; @@ -1697,18 +1693,6 @@ class Transmitter }); } - /** - * Returns if the post contains sensitive content ("nsfw") - * - * @param integer $uri_id URI id - * @return boolean Whether URI id was found - * @throws \Exception - */ - private static function isSensitive(int $uri_id): bool - { - return DBA::exists('tag-view', ['uri-id' => $uri_id, 'name' => 'nsfw', 'type' => Tag::HASHTAG]); - } - /** * Creates event data * @@ -1718,9 +1702,9 @@ class Transmitter */ private static function createEvent(array $item): array { - $event = []; - $event['name'] = $item['event-summary']; - $event['content'] = BBCode::convertForUriId($item['uri-id'], $item['event-desc'], BBCode::ACTIVITYPUB); + $event = []; + $event['name'] = $item['event-summary']; + $event['content'] = BBCode::convertForUriId($item['uri-id'], $item['event-desc'], BBCode::ACTIVITYPUB); $event['startTime'] = DateTimeFormat::utc($item['event-start'], 'c'); if (!$item['event-nofinish']) { @@ -1728,7 +1712,7 @@ class Transmitter } if (!empty($item['event-location'])) { - $item['location'] = $item['event-location']; + $item['location'] = $item['event-location']; $event['location'] = self::createLocation($item); } @@ -1758,7 +1742,7 @@ class Transmitter // But to not risk compatibility issues we currently perform the changes only for communities. if ($item['gravity'] == Item::GRAVITY_PARENT) { $isCommunityPost = !empty(Tag::getByURIId($item['uri-id'], [Tag::EXCLUSIVE_MENTION])); - $links = Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]); + $links = Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]); if ($isCommunityPost && (count($links) == 1)) { $link = $links[0]['url']; } @@ -1766,11 +1750,35 @@ class Transmitter $isCommunityPost = false; } + $title = $item['title']; + $summary = $item['content-warning'] ?: BBCode::toPlaintext(BBCode::getAbstract($item['body'], Protocol::ACTIVITYPUB)); + $type = ''; + if ($item['event-type'] == 'event') { $type = 'Event'; - } elseif (!empty($item['title'])) { + } elseif (!empty($title)) { if (!$isCommunityPost || empty($link)) { - $type = 'Article'; + switch (DI::pConfig()->get($item['uid'], 'system', 'article_mode') ?? ActivityPub::ARTICLE_DEFAULT) { + case ActivityPub::ARTICLE_DEFAULT: + $type = 'Article'; + break; + case ActivityPub::ARTICLE_USE_SUMMARY: + $type = 'Note'; + if (!$summary) { + $summary = $title; + } else { + $item['raw-body'] = '[h4][b]' . $title . "[/b][/h4]\n\n" . $item['raw-body']; + $item['body'] = '[h4][b]' . $title . "[/b][/h4]\n\n" . $item['body']; + } + $title = ''; + break; + case ActivityPub::ARTICLE_EMBED_TITLE: + $type = 'Note'; + $item['raw-body'] = '[b]' . $title . "[/b]\n\n" . $item['raw-body']; + $item['body'] = '[b]' . $title . "[/b]\n\n" . $item['body']; + $title = ''; + break; + } } else { // "Page" is used by Lemmy for posts that contain an external link $type = 'Page'; @@ -1783,16 +1791,14 @@ class Transmitter $type = 'Tombstone'; } - $data = []; - $data['id'] = $item['uri']; + $data = []; + $data['id'] = $item['uri']; $data['type'] = $type; if ($item['deleted']) { return $data; } - $data['summary'] = BBCode::toPlaintext(BBCode::getAbstract($item['body'], Protocol::ACTIVITYPUB)); - if ($item['uri'] != $item['thr-parent']) { $data['inReplyTo'] = $item['thr-parent']; } else { @@ -1800,7 +1806,7 @@ class Transmitter } $data['diaspora:guid'] = $item['guid']; - $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM); + $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM); if ($item['created'] != $item['edited']) { $data['updated'] = DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM); @@ -1812,14 +1818,22 @@ class Transmitter } else { $data['attributedTo'] = $item['author-link']; } - $data['sensitive'] = self::isSensitive($item['uri-id']); + $data['sensitive'] = (bool)$item['sensitive']; - if (!empty($item['conversation']) && ($item['conversation'] != './')) { - $data['conversation'] = $data['context'] = $item['conversation']; + if (!empty($item['context']) && ($item['context'] != './')) { + $data['context'] = $item['context']; } - if (!empty($item['title'])) { - $data['name'] = BBCode::toPlaintext($item['title'], false); + if (!empty($item['conversation']) && ($item['conversation'] != './')) { + $data['conversation'] = $item['conversation']; + } + + if (!empty($title)) { + $data['name'] = BBCode::toPlaintext($title, false); + } + + if (!empty($summary)) { + $data['summary'] = $summary; } $permission_block = self::getReceiversForUriId($item['uri-id'], false); @@ -1828,7 +1842,7 @@ class Transmitter $item = Post\Media::addHTMLAttachmentToItem($item); - $body = $item['body']; + $body = $item['body']; $emojis = []; if ($type == 'Note') { $body = $item['raw-body'] ?? self::removePictures($body); @@ -1848,7 +1862,7 @@ class Transmitter * } */ - if (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) { + if (empty($item['uid']) || !Feature::isEnabled($item['uid'], Feature::EXPLICIT_MENTIONS)) { $body = self::prependMentions($body, $item['uri-id'], $item['author-link']); } @@ -1872,9 +1886,10 @@ class Transmitter if (!empty($item['quote-uri-id']) && ($item['quote-uri-id'] != $item['uri-id'])) { if (Post::exists(['uri-id' => $item['quote-uri-id'], 'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN]])) { - $real_quote = true; - $data['quoteUrl'] = $item['quote-uri']; - $body = DI::contentItem()->addShareLink($body, $item['quote-uri-id']); + $real_quote = true; + $data['_misskey_content'] = BBCode::removeSharedData($body); + $data['quoteUrl'] = $item['quote-uri']; + $body = DI::contentItem()->addShareLink($body, $item['quote-uri-id']); } else { $body = DI::contentItem()->addSharedPost($item, $body); } @@ -1915,7 +1930,7 @@ class Transmitter } $data['attachment'] = self::createAttachmentList($item); - $data['tag'] = array_merge(self::createTagList($item, $data['quoteUrl'] ?? ''), $emojis); + $data['tag'] = array_merge(self::createTagList($item, $data['quoteUrl'] ?? ''), $emojis); if (empty($data['location']) && (!empty($item['coord']) || !empty($item['location']))) { $data['location'] = self::createLocation($item); @@ -1973,10 +1988,10 @@ class Transmitter $target = XML::parseString($item['target']); $activity['diaspora:guid'] = $item['guid']; - $activity['actor'] = $item['author-link']; - $activity['target'] = (string)$target->id; - $activity['summary'] = BBCode::toPlaintext($item['body']); - $activity['object'] = ['id' => (string)$object->id, 'type' => 'tag', 'name' => (string)$object->title, 'content' => (string)$object->content]; + $activity['actor'] = $item['author-link']; + $activity['target'] = (string)$target->id; + $activity['summary'] = BBCode::toPlaintext($item['body']); + $activity['object'] = ['id' => (string)$object->id, 'type' => 'tag', 'name' => (string)$object->title, 'content' => (string)$object->content]; return $activity; } @@ -1994,23 +2009,23 @@ class Transmitter private static function createAnnounce(array $item, array $activity, bool $api_mode = false): array { $orig_body = $item['body']; - $announce = self::getAnnounceArray($item); + $announce = self::getAnnounceArray($item); if (empty($announce)) { - $activity['type'] = 'Create'; + $activity['type'] = 'Create'; $activity['object'] = self::createNote($item, $api_mode); return $activity; } if (empty($announce['comment'])) { // Pure announce, without a quote - $activity['type'] = 'Announce'; + $activity['type'] = 'Announce'; $activity['object'] = $announce['object']['uri']; return $activity; } // Quote - $activity['type'] = 'Create'; - $item['body'] = $announce['comment'] . "\n" . $announce['object']['plink']; + $activity['type'] = 'Create'; + $item['body'] = $announce['comment'] . "\n" . $announce['object']['plink']; $activity['object'] = self::createNote($item, $api_mode); /// @todo Finally decide how to implement this in AP. This is a possible way: @@ -2069,18 +2084,19 @@ class Transmitter * Creates an activity id for a given contact id * * @param integer $cid Contact ID of target + * @param integer $uid Optional user id. if empty, the contact uid is used. * * @return bool|string activity id */ - public static function activityIDFromContact(int $cid) + public static function activityIDFromContact(int $cid, int $uid = 0) { $contact = DBA::selectFirst('contact', ['uid', 'id', 'created'], ['id' => $cid]); if (!DBA::isResult($contact)) { return false; } - $hash = hash('ripemd128', $contact['uid'].'-'.$contact['id'].'-'.$contact['created']); - $uuid = substr($hash, 0, 8). '-' . substr($hash, 8, 4) . '-' . substr($hash, 12, 4) . '-' . substr($hash, 16, 4) . '-' . substr($hash, 20, 12); + $hash = hash('ripemd128', $uid ?: $contact['uid'] . '-' . $contact['id'] . '-' . $contact['created']); + $uuid = substr($hash, 0, 8) . '-' . substr($hash, 8, 4) . '-' . substr($hash, 12, 4) . '-' . substr($hash, 16, 4) . '-' . substr($hash, 20, 12); return DI::baseUrl() . '/activity/' . $uuid; } @@ -2098,20 +2114,20 @@ class Transmitter $suggestion = DI::fsuggest()->selectOneById($suggestion_id); $data = [ - '@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'Announce', - 'actor' => $owner['url'], - 'object' => $suggestion->url, - 'content' => $suggestion->note, + '@context' => ActivityPub::CONTEXT, + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Announce', + 'actor' => $owner['url'], + 'object' => $suggestion->url, + 'content' => $suggestion->note, 'instrument' => self::getService(), - 'to' => [ActivityPub::PUBLIC_COLLECTION], - 'cc' => [] + 'to' => [ActivityPub::PUBLIC_COLLECTION], + 'cc' => [] ]; $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile deletion for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + DI::logger()->info('Deliver profile deletion for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); return HTTPSignature::transmit($signed, $inbox, $owner); } @@ -2126,20 +2142,20 @@ class Transmitter public static function sendProfileRelocation(array $owner, string $inbox): bool { $data = [ - '@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'dfrn:relocate', - 'actor' => $owner['url'], - 'object' => $owner['url'], - 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), + '@context' => ActivityPub::CONTEXT, + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'dfrn:relocate', + 'actor' => $owner['url'], + 'object' => $owner['url'], + 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), 'instrument' => self::getService(), - 'to' => [ActivityPub::PUBLIC_COLLECTION], - 'cc' => [] + 'to' => [ActivityPub::PUBLIC_COLLECTION], + 'cc' => [] ]; $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile relocation for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + DI::logger()->info('Deliver profile relocation for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); return HTTPSignature::transmit($signed, $inbox, $owner); } @@ -2154,23 +2170,25 @@ class Transmitter public static function sendProfileDeletion(array $owner, string $inbox): bool { if (empty($owner['uprvkey'])) { - Logger::error('No private key for owner found, the deletion message cannot be processed.', ['user' => $owner['uid']]); + DI::logger()->error('No private key for owner found, the deletion message cannot be processed.', ['user' => $owner['uid']]); return false; } - $data = ['@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'Delete', - 'actor' => $owner['url'], - 'object' => $owner['url'], - 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), + $data = [ + '@context' => ActivityPub::CONTEXT, + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Delete', + 'actor' => $owner['url'], + 'object' => $owner['url'], + 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), 'instrument' => self::getService(), - 'to' => [ActivityPub::PUBLIC_COLLECTION], - 'cc' => []]; + 'to' => [ActivityPub::PUBLIC_COLLECTION], + 'cc' => [] + ]; $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile deletion for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + DI::logger()->info('Deliver profile deletion for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); return HTTPSignature::transmit($signed, $inbox, $owner); } @@ -2188,19 +2206,21 @@ class Transmitter { $profile = APContact::getByURL($owner['url']); - $data = ['@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'Update', - 'actor' => $owner['url'], - 'object' => self::getProfile($owner['uid']), - 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), + $data = [ + '@context' => ActivityPub::CONTEXT, + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Update', + 'actor' => $owner['url'], + 'object' => self::getProfile($owner['uid']), + 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), 'instrument' => self::getService(), - 'to' => [$profile['followers']], - 'cc' => []]; + 'to' => [$profile['followers']], + 'cc' => [] + ]; $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile update for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + DI::logger()->info('Deliver profile update for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); return HTTPSignature::transmit($signed, $inbox, $owner); } @@ -2220,13 +2240,13 @@ class Transmitter { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { - Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); + DI::logger()->warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); return false; } $owner = User::getOwnerDataById($uid); if (empty($owner)) { - Logger::warning('No user found for actor, aborting', ['uid' => $uid]); + DI::logger()->warning('No user found for actor, aborting', ['uid' => $uid]); return false; } @@ -2235,16 +2255,16 @@ class Transmitter } $data = [ - '@context' => ActivityPub::CONTEXT, - 'id' => $id, - 'type' => $activity, - 'actor' => $owner['url'], - 'object' => $profile['url'], + '@context' => ActivityPub::CONTEXT, + 'id' => $id, + 'type' => $activity, + 'actor' => $owner['url'], + 'object' => $profile['url'], 'instrument' => self::getService(), - 'to' => [$profile['url']], + 'to' => [$profile['url']], ]; - Logger::info('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid); + DI::logger()->info('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid); $signed = LDSignature::sign($data, $owner); return HTTPSignature::transmit($signed, $profile['inbox'], $owner); @@ -2266,7 +2286,7 @@ class Transmitter { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { - Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); + DI::logger()->warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); return false; } @@ -2274,33 +2294,35 @@ class Transmitter // We need to use some user as a sender. It doesn't care who it will send. We will use an administrator account. $admin = User::getFirstAdmin(['uid']); if (!$admin) { - Logger::warning('No available admin user for transmission', ['target' => $target]); + DI::logger()->warning('No available admin user for transmission', ['target' => $target]); return false; } $uid = $admin['uid']; } - $condition = ['verb' => Activity::FOLLOW, 'uid' => 0, 'parent-uri' => $object, - 'author-id' => Contact::getPublicIdByUserId($uid)]; + $condition = [ + 'verb' => Activity::FOLLOW, 'uid' => 0, 'parent-uri' => $object, + 'author-id' => Contact::getPublicIdByUserId($uid) + ]; if (Post::exists($condition)) { - Logger::info('Follow for ' . $object . ' for user ' . $uid . ' does already exist.'); + DI::logger()->info('Follow for ' . $object . ' for user ' . $uid . ' does already exist.'); return false; } $owner = User::getOwnerDataById($uid); $data = [ - '@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'Follow', - 'actor' => $owner['url'], - 'object' => $object, + '@context' => ActivityPub::CONTEXT, + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Follow', + 'actor' => $owner['url'], + 'object' => $object, 'instrument' => self::getService(), - 'to' => [$profile['url']], + 'to' => [$profile['url']], ]; - Logger::info('Sending follow ' . $object . ' to ' . $target . ' for user ' . $uid); + DI::logger()->info('Sending follow ' . $object . ' to ' . $target . ' for user ' . $uid); $signed = LDSignature::sign($data, $owner); return HTTPSignature::transmit($signed, $profile['inbox'], $owner); @@ -2320,32 +2342,32 @@ class Transmitter { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { - Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); + DI::logger()->warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); return; } $owner = User::getOwnerDataById($uid); if (!$owner) { - Logger::notice('No user found for actor', ['uid' => $uid]); + DI::logger()->notice('No user found for actor', ['uid' => $uid]); return; } $data = [ '@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'Accept', - 'actor' => $owner['url'], - 'object' => [ - 'id' => $id, - 'type' => 'Follow', - 'actor' => $profile['url'], + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Accept', + 'actor' => $owner['url'], + 'object' => [ + 'id' => $id, + 'type' => 'Follow', + 'actor' => $profile['url'], 'object' => $owner['url'] ], 'instrument' => self::getService(), - 'to' => [$profile['url']], + 'to' => [$profile['url']], ]; - Logger::debug('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id); + DI::logger()->debug('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id); $signed = LDSignature::sign($data, $owner); HTTPSignature::transmit($signed, $profile['inbox'], $owner); @@ -2365,26 +2387,26 @@ class Transmitter { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { - Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); + DI::logger()->warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); return false; } $data = [ '@context' => ActivityPub::CONTEXT, - 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), - 'type' => 'Reject', - 'actor' => $owner['url'], - 'object' => [ - 'id' => $objectId, - 'type' => 'Follow', - 'actor' => $profile['url'], + 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Reject', + 'actor' => $owner['url'], + 'object' => [ + 'id' => $objectId, + 'type' => 'Follow', + 'actor' => $profile['url'], 'object' => $owner['url'] ], 'instrument' => self::getService(), - 'to' => [$profile['url']], + 'to' => [$profile['url']], ]; - Logger::debug('Sending reject to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); + DI::logger()->debug('Sending reject to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); $signed = LDSignature::sign($data, $owner); return HTTPSignature::transmit($signed, $profile['inbox'], $owner); @@ -2405,7 +2427,7 @@ class Transmitter { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { - Logger::warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); + DI::logger()->warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); return false; } @@ -2418,20 +2440,67 @@ class Transmitter $data = [ '@context' => ActivityPub::CONTEXT, - 'id' => $objectId, - 'type' => 'Undo', - 'actor' => $owner['url'], - 'object' => [ - 'id' => $object_id, - 'type' => 'Follow', - 'actor' => $owner['url'], + 'id' => $objectId, + 'type' => 'Undo', + 'actor' => $owner['url'], + 'object' => [ + 'id' => $object_id, + 'type' => 'Follow', + 'actor' => $owner['url'], 'object' => $profile['url'] ], 'instrument' => self::getService(), - 'to' => [$profile['url']], + 'to' => [$profile['url']], ]; - Logger::info('Sending undo to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); + DI::logger()->info('Sending undo to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); + + $signed = LDSignature::sign($data, $owner); + return HTTPSignature::transmit($signed, $profile['inbox'], $owner); + } + + /** + * Transmits a message that we don't want to block this contact anymore + * + * @param string $target Target profile + * @param integer $cid Contact id + * @param array $owner Sender owner-view record + * @return bool success + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + * @throws \Exception + */ + public static function sendContactUnblock(string $target, int $cid, array $owner): bool + { + $profile = APContact::getByURL($target); + if (empty($profile['inbox'])) { + DI::logger()->warning('No inbox found for target', ['target' => $target, 'profile' => $profile]); + return false; + } + + $object_id = self::activityIDFromContact($cid, $owner['uid']); + if (empty($object_id)) { + return false; + } + + $objectId = DI::baseUrl() . '/activity/' . System::createGUID(); + + $data = [ + '@context' => ActivityPub::CONTEXT, + 'id' => $objectId, + 'type' => 'Undo', + 'actor' => $owner['url'], + 'object' => [ + 'id' => $object_id, + 'type' => 'Block', + 'actor' => $owner['url'], + 'object' => $profile['url'] + ], + 'instrument' => self::getService(), + 'to' => [$profile['url']], + ]; + + DI::logger()->info('Sending undo to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); $signed = LDSignature::sign($data, $owner); return HTTPSignature::transmit($signed, $profile['inbox'], $owner); @@ -2441,17 +2510,17 @@ class Transmitter * Prepends mentions (@) to $body variable * * @param string $body HTML code - * @param int $uriId * @param string $authorLink Author link * @return string HTML code with prepended mentions */ - private static function prependMentions(string $body, int $uriid, string $authorLink): string + private static function prependMentions(string $body, int $uriId, string $authorLink): string { $mentions = []; - foreach (Tag::getByURIId($uriid, [Tag::IMPLICIT_MENTION]) as $tag) { + foreach (Tag::getByURIId($uriId, [Tag::IMPLICIT_MENTION]) as $tag) { $profile = Contact::getByURL($tag['url'], false, ['addr', 'contact-type', 'nick']); - if (!empty($profile['addr']) + if ( + !empty($profile['addr']) && $profile['contact-type'] != Contact::TYPE_COMMUNITY && !strstr($body, $profile['addr']) && !strstr($body, $tag['url']) diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index 1ab12089de..56cafc1a97 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; @@ -27,7 +13,6 @@ use DOMNode; use DOMXPath; use Friendica\App; use Friendica\Content\Text\BBCode; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Database\DBA; use Friendica\DI; @@ -44,6 +29,7 @@ use Friendica\Model\Post; use Friendica\Model\Profile; use Friendica\Model\Tag; use Friendica\Model\User; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Network\HTTPException; use Friendica\Network\Probe; use Friendica\Util\Crypto; @@ -59,7 +45,6 @@ use GuzzleHttp\Psr7\Uri; */ class DFRN { - const TOP_LEVEL = 0; // Top level posting const REPLY = 1; // Regular reply that is stored locally const REPLY_RC = 2; // Reply that will be relayed @@ -78,28 +63,27 @@ class DFRN public static function getImporter(int $cid, int $uid = 0): array { $condition = ['id' => $cid, 'blocked' => false, 'pending' => false]; - $contact = DBA::selectFirst('contact', [], $condition); + $contact = DBA::selectFirst('contact', [], $condition); if (!DBA::isResult($contact)) { return []; } - $contact['cpubkey'] = $contact['pubkey']; - $contact['cprvkey'] = $contact['prvkey']; + $contact['cpubkey'] = $contact['pubkey']; + $contact['cprvkey'] = $contact['prvkey']; $contact['senderName'] = $contact['name']; if ($uid != 0) { $condition = ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]; - $user = DBA::selectFirst('user', [], $condition); + $user = DBA::selectFirst('user', [], $condition); if (!DBA::isResult($user)) { return []; } $user['importer_uid'] = $user['uid']; - $user['uprvkey'] = $user['prvkey']; + $user['uprvkey'] = $user['prvkey']; } else { $user = ['importer_uid' => 0, 'uprvkey' => '', 'timezone' => 'UTC', - 'nickname' => '', 'sprvkey' => '', 'spubkey' => '', - 'page-flags' => 0, 'account-type' => 0, 'prvnets' => 0]; + 'nickname' => '', 'page-flags' => 0, 'account-type' => 0, 'prvnets' => 0]; } return array_merge($contact, $user); @@ -120,7 +104,7 @@ class DFRN */ public static function entries(array $items, array $owner): string { - $doc = new DOMDocument('1.0', 'utf-8'); + $doc = new DOMDocument('1.0', 'utf-8'); $doc->formatOutput = true; $root = self::addHeader($doc, $owner, 'dfrn:owner', '', false); @@ -133,7 +117,7 @@ class DFRN // These values aren't sent when sending from the queue. /// @todo Check if we can set these values from the queue or if they are needed at all. $item['entry:comment-allow'] = ($item['entry:comment-allow'] ?? '') ?: true; - $item['entry:cid'] = $item['entry:cid'] ?? 0; + $item['entry:cid'] = $item['entry:cid'] ?? 0; $entry = self::entry($doc, 'text', $item, $owner, $item['entry:comment-allow'], $item['entry:cid']); if (isset($entry)) { @@ -181,9 +165,9 @@ class DFRN $owner = ['uid' => 0, 'nick' => 'feed-item']; } - $doc = new DOMDocument('1.0', 'utf-8'); + $doc = new DOMDocument('1.0', 'utf-8'); $doc->formatOutput = true; - $type = 'html'; + $type = 'html'; if ($conversation) { $root = $doc->createElementNS(ActivityNamespace::ATOM1, 'feed'); @@ -225,12 +209,12 @@ class DFRN */ public static function mail(array $mail, array $owner): string { - $doc = new DOMDocument('1.0', 'utf-8'); + $doc = new DOMDocument('1.0', 'utf-8'); $doc->formatOutput = true; $root = self::addHeader($doc, $owner, 'dfrn:owner', '', false); - $mailElement = $doc->createElement('dfrn:mail'); + $mailElement = $doc->createElement('dfrn:mail'); $senderElement = $doc->createElement('dfrn:sender'); XML::addElement($doc, $senderElement, 'dfrn:name', $owner['name']); @@ -262,7 +246,7 @@ class DFRN */ public static function fsuggest(array $item, array $owner): string { - $doc = new DOMDocument('1.0', 'utf-8'); + $doc = new DOMDocument('1.0', 'utf-8'); $doc->formatOutput = true; $root = self::addHeader($doc, $owner, 'dfrn:owner', '', false); @@ -304,13 +288,11 @@ class DFRN $profilephotos = Photo::selectToArray(['resource-id', 'scale', 'type'], ['profile' => true, 'uid' => $uid], ['order' => ['scale']]); $photos = []; - $ext = Images::supportedTypes(); - foreach ($profilephotos as $p) { - $photos[$p['scale']] = DI::baseUrl() . '/photo/' . $p['resource-id'] . '-' . $p['scale'] . '.' . $ext[$p['type']]; + $photos[$p['scale']] = DI::baseUrl() . '/photo/' . $p['resource-id'] . '-' . $p['scale'] . Images::getExtensionByMimeType($p['type']); } - $doc = new DOMDocument('1.0', 'utf-8'); + $doc = new DOMDocument('1.0', 'utf-8'); $doc->formatOutput = true; $root = self::addHeader($doc, $owner, 'dfrn:owner', '', false); @@ -378,23 +360,8 @@ class DFRN $attributes = ['rel' => 'alternate', 'type' => 'text/html', 'href' => $alternatelink]; XML::addElement($doc, $root, 'link', '', $attributes); - - if ($public) { - // DFRN itself doesn't uses this. But maybe someone else wants to subscribe to the public feed. - OStatus::addHubLink($doc, $root, $owner['nick']); - - $attributes = ['rel' => 'salmon', 'href' => DI::baseUrl() . '/salmon/' . $owner['nick']]; - XML::addElement($doc, $root, 'link', '', $attributes); - - $attributes = ['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => DI::baseUrl() . '/salmon/' . $owner['nick']]; - XML::addElement($doc, $root, 'link', '', $attributes); - - $attributes = ['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => DI::baseUrl() . '/salmon/' . $owner['nick']]; - XML::addElement($doc, $root, 'link', '', $attributes); - } - // For backward compatibility we keep this element - if ($owner['page-flags'] == User::PAGE_FLAGS_COMMUNITY) { + if (in_array($owner['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN])) { XML::addElement($doc, $root, 'dfrn:community', 1); } @@ -443,12 +410,12 @@ class DFRN if (DBA::isResult($profile)) { $tmp_dob = substr($profile['dob'], 5); if (intval($tmp_dob)) { - $y = DateTimeFormat::timezoneNow($tz, 'Y'); - $bd = $y . '-' . $tmp_dob . ' 00:00'; + $y = DateTimeFormat::timezoneNow($tz, 'Y'); + $bd = $y . '-' . $tmp_dob . ' 00:00'; $t_dob = strtotime($bd); - $now = strtotime(DateTimeFormat::timezoneNow($tz)); + $now = strtotime(DateTimeFormat::timezoneNow($tz)); if ($t_dob < $now) { - $bd = $y + 1 . '-' . $tmp_dob . ' 00:00'; + $bd = (int) $y + 1 . '-' . $tmp_dob . ' 00:00'; } $birthday = DateTimeFormat::convert($bd, 'UTC', $tz, DateTimeFormat::ATOM); } @@ -489,11 +456,11 @@ class DFRN XML::addElement($doc, $author, 'dfrn:handle', $owner['addr'], $attributes); $attributes = [ - 'rel' => 'photo', - 'type' => 'image/jpeg', - 'media:width' => Proxy::PIXEL_SMALL, + 'rel' => 'photo', + 'type' => 'image/jpeg', + 'media:width' => Proxy::PIXEL_SMALL, 'media:height' => Proxy::PIXEL_SMALL, - 'href' => User::getAvatarUrl($owner, Proxy::SIZE_SMALL), + 'href' => User::getAvatarUrl($owner, Proxy::SIZE_SMALL), ]; if (!$public || !$hide) { @@ -521,9 +488,11 @@ class DFRN } // Only show contact details when we are allowed to - $profile = DBA::selectFirst('owner-view', + $profile = DBA::selectFirst( + 'owner-view', ['about', 'name', 'homepage', 'nickname', 'timezone', 'locality', 'region', 'country-name', 'pub_keywords', 'xmpp', 'dob'], - ['uid' => $owner['uid'], 'hidewall' => false]); + ['uid' => $owner['uid'], 'hidewall' => false] + ); if (DBA::isResult($profile)) { XML::addElement($doc, $author, 'poco:displayName', $profile['name']); XML::addElement($doc, $author, 'poco:updated', $namdate); @@ -610,20 +579,20 @@ class DFRN /// - Check real image type and image size /// - Check which of these elements we should use $attributes = [ - 'rel' => 'photo', - 'type' => 'image/jpeg', - 'media:width' => 80, + 'rel' => 'photo', + 'type' => 'image/jpeg', + 'media:width' => 80, 'media:height' => 80, - 'href' => $contact['photo'], + 'href' => $contact['photo'], ]; XML::addElement($doc, $author, 'link', '', $attributes); $attributes = [ - 'rel' => 'avatar', - 'type' => 'image/jpeg', - 'media:width' => 80, + 'rel' => 'avatar', + 'type' => 'image/jpeg', + 'media:width' => 80, 'media:height' => 80, - 'href' => $contact['photo'], + 'href' => $contact['photo'], ]; XML::addElement($doc, $author, 'link', '', $attributes); } @@ -638,64 +607,64 @@ class DFRN * @param string $element Element name for the activity * @param string $activity activity value * @param int $uriid Uri-Id of the post - * @return DOMElement XML activity object + * @return DOMElement|false XML activity object or false on error * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @todo Find proper type-hints */ private static function createActivity(DOMDocument $doc, string $element, string $activity, int $uriid) { - if ($activity) { - $entry = $doc->createElement($element); - - $r = XML::parseString($activity); - if (!$r) { - return false; - } - - if ($r->type) { - XML::addElement($doc, $entry, "activity:object-type", $r->type); - } - - if ($r->id) { - XML::addElement($doc, $entry, "id", $r->id); - } - - if ($r->title) { - XML::addElement($doc, $entry, "title", $r->title); - } - - if ($r->link) { - if (substr($r->link, 0, 1) == '<') { - if (strstr($r->link, '&') && (! strstr($r->link, '&'))) { - $r->link = str_replace('&', '&', $r->link); - } - - $r->link = preg_replace('/\/', '', $r->link); - - // XML does need a single element as root element so we add a dummy element here - $data = XML::parseString("" . $r->link . ""); - if (is_object($data)) { - foreach ($data->link as $link) { - $attributes = []; - foreach ($link->attributes() as $parameter => $value) { - $attributes[$parameter] = $value; - } - XML::addElement($doc, $entry, "link", "", $attributes); - } - } - } else { - $attributes = ["rel" => "alternate", "type" => "text/html", "href" => $r->link]; - XML::addElement($doc, $entry, "link", "", $attributes); - } - } - if ($r->content) { - XML::addElement($doc, $entry, "content", BBCode::convertForUriId($uriid, $r->content, BBCode::EXTERNAL), ["type" => "html"]); - } - - return $entry; + if (!$activity) { + return false; } - return false; + $entry = $doc->createElement($element); + + $r = XML::parseString($activity); + if (!$r) { + return false; + } + + if ($r->type) { + XML::addElement($doc, $entry, "activity:object-type", $r->type); + } + + if ($r->id) { + XML::addElement($doc, $entry, "id", $r->id); + } + + if ($r->title) { + XML::addElement($doc, $entry, "title", $r->title); + } + + if ($r->link) { + if (substr($r->link, 0, 1) == '<') { + if (strstr($r->link, '&') && (! strstr($r->link, '&'))) { + $r->link = str_replace('&', '&', $r->link); + } + + $r->link = preg_replace('/\/', '', $r->link); + + // XML does need a single element as root element so we add a dummy element here + $data = XML::parseString("" . $r->link . ""); + if (is_object($data)) { + foreach ($data->link as $link) { + $attributes = []; + foreach ($link->attributes() as $parameter => $value) { + $attributes[$parameter] = $value; + } + XML::addElement($doc, $entry, "link", "", $attributes); + } + } + } else { + $attributes = ["rel" => "alternate", "type" => "text/html", "href" => $r->link]; + XML::addElement($doc, $entry, "link", "", $attributes); + } + } + if ($r->content) { + XML::addElement($doc, $entry, "content", BBCode::convertForUriId($uriid, $r->content, BBCode::EXTERNAL), ["type" => "html"]); + } + + return $entry; } /** @@ -712,8 +681,8 @@ class DFRN { foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) { $attributes = ['rel' => 'enclosure', - 'href' => $attachment['url'], - 'type' => $attachment['mimetype']]; + 'href' => $attachment['url'], + 'type' => $attachment['mimetype']]; if (!empty($attachment['size'])) { $attributes['length'] = intval($attachment['size']); @@ -747,7 +716,7 @@ class DFRN $mentioned = []; if (!$item['parent']) { - Logger::warning('Item without parent found.', ['type' => $type, 'item' => $item]); + DI::logger()->warning('Item without parent found.', ['type' => $type, 'item' => $item]); return null; } @@ -803,8 +772,8 @@ class DFRN if ($item['gravity'] != Item::GRAVITY_PARENT) { $parent = Post::selectFirst(['guid', 'plink'], ['uri' => $item['thr-parent'], 'uid' => $item['uid']]); if (DBA::isResult($parent)) { - $attributes = ["ref" => $item['thr-parent'], "type" => "text/html", - "href" => $parent['plink'], + $attributes = ["ref" => $item['thr-parent'], "type" => "text/html", + "href" => $parent['plink'], "dfrn:diaspora_guid" => $parent['guid']]; XML::addElement($doc, $entry, "thr:in-reply-to", "", $attributes); } @@ -813,7 +782,7 @@ class DFRN // Add conversation data. This is used for OStatus $attributes = [ 'href' => $item['conversation'], - 'ref' => $item['conversation'], + 'ref' => $item['conversation'], ]; XML::addElement($doc, $entry, 'ostatus:conversation', $item['conversation'], $attributes); @@ -838,7 +807,7 @@ class DFRN 'link', '', [ - 'rel' => 'alternate', + 'rel' => 'alternate', 'type' => 'text/html', 'href' => DI::baseUrl() . '/display/' . $item['guid'] ], @@ -920,7 +889,7 @@ class DFRN foreach ($mentioned as $mention) { $condition = ['uid' => $owner['uid'], 'nurl' => Strings::normaliseLink($mention)]; - $contact = DBA::selectFirst('contact', ['contact-type'], $condition); + $contact = DBA::selectFirst('contact', ['contact-type'], $condition); if (DBA::isResult($contact) && ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) { XML::addElement( @@ -929,9 +898,9 @@ class DFRN 'link', '', [ - 'rel' => 'mentioned', + 'rel' => 'mentioned', 'ostatus:object-type' => Activity\ObjectType::GROUP, - 'href' => $mention, + 'href' => $mention, ], ); } else { @@ -941,9 +910,9 @@ class DFRN 'link', '', [ - 'rel' => 'mentioned', + 'rel' => 'mentioned', 'ostatus:object-type' => Activity\ObjectType::PERSON, - 'href' => $mention, + 'href' => $mention, ], ); } @@ -969,14 +938,14 @@ class DFRN { if (!$public_batch) { if (empty($contact['addr'])) { - Logger::notice('Empty contact handle for ' . $contact['id'] . ' - ' . $contact['url'] . ' - trying to update it.'); + DI::logger()->notice('Empty contact handle for ' . $contact['id'] . ' - ' . $contact['url'] . ' - trying to update it.'); if (Contact::updateFromProbe($contact['id'])) { - $new_contact = DBA::selectFirst('contact', ['addr'], ['id' => $contact['id']]); + $new_contact = DBA::selectFirst('contact', ['addr'], ['id' => $contact['id']]); $contact['addr'] = $new_contact['addr']; } if (empty($contact['addr'])) { - Logger::notice('Unable to find contact handle for ' . $contact['id'] . ' - ' . $contact['url']); + DI::logger()->notice('Unable to find contact handle for ' . $contact['id'] . ' - ' . $contact['url']); return -21; } } @@ -984,7 +953,7 @@ class DFRN try { $pubkey = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr']))->pubKey; } catch (HTTPException\NotFoundException|\InvalidArgumentException $e) { - Logger::notice('Unable to find contact details for ' . $contact['id'] . ' - ' . $contact['addr']); + DI::logger()->notice('Unable to find contact details for ' . $contact['id'] . ' - ' . $contact['addr']); return -22; } } else { @@ -995,24 +964,30 @@ class DFRN // Create the endpoint for public posts. This is some WIP and should later be added to the probing if ($public_batch && empty($contact['batch'])) { - $parts = parse_url($contact['notify']); + $parts = parse_url($contact['notify']); $path_parts = explode('/', $parts['path']); array_pop($path_parts); - $parts['path'] = implode('/', $path_parts); + $parts['path'] = implode('/', $path_parts); $contact['batch'] = (string)Uri::fromParts($parts); } $dest_url = ($public_batch ? $contact['batch'] : $contact['notify']); if (empty($dest_url)) { - Logger::info('Empty destination', ['public' => $public_batch, 'contact' => $contact]); + DI::logger()->info('Empty destination', ['public' => $public_batch, 'contact' => $contact]); return -24; } $content_type = ($public_batch ? 'application/magic-envelope+xml' : 'application/json'); - $postResult = DI::httpClient()->post($dest_url, $envelope, ['Content-Type' => $content_type]); - $xml = $postResult->getBody(); + try { + $postResult = DI::httpClient()->post($dest_url, $envelope, ['Content-Type' => $content_type], 0, HttpClientRequest::DFRN); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return -25; + } + + $xml = $postResult->getBodyString(); $curl_stat = $postResult->getReturnCode(); if (!empty($contact['gsid']) && ($postResult->isTimeout() || empty($curl_stat))) { @@ -1020,7 +995,7 @@ class DFRN } if (empty($curl_stat) || empty($xml)) { - Logger::notice('Empty answer from ' . $contact['id'] . ' - ' . $dest_url); + DI::logger()->notice('Empty answer from ' . $contact['id'] . ' - ' . $dest_url); return -9; // timed out } @@ -1029,8 +1004,8 @@ class DFRN } if (strpos($xml, 'notice('No valid XML returned from ' . $contact['id'] . ' - ' . $dest_url); + DI::logger()->debug('Returned XML: ' . $xml); return 3; } @@ -1042,10 +1017,11 @@ class DFRN if (!empty($contact['gsid'])) { GServer::setReachableById($contact['gsid'], Protocol::DFRN); + Item::incrementOutbound(Protocol::DFRN); } if (!empty($res->message)) { - Logger::info('Transmit to ' . $dest_url . ' returned status '.$res->status.' - '.$res->message); + DI::logger()->info('Transmit to ' . $dest_url . ' returned status '.$res->status.' - '.$res->message); } return intval($res->status); @@ -1068,7 +1044,7 @@ class DFRN */ private static function fetchauthor(\DOMXPath $xpath, \DOMNode $context, array $importer, string $element, bool $onlyfetch, string $xml = ''): array { - $author = []; + $author = []; $author["name"] = XML::getFirstNodeValue($xpath, $element."/atom:name/text()", $context); $author["link"] = XML::getFirstNodeValue($xpath, $element."/atom:uri/text()", $context); @@ -1085,15 +1061,15 @@ class DFRN if (DBA::isResult($contact_old)) { $author["contact-id"] = $contact_old["id"]; - $author["network"] = $contact_old["network"]; + $author["network"] = $contact_old["network"]; } else { - Logger::info('Contact not found', ['condition' => $condition]); + DI::logger()->info('Contact not found', ['condition' => $condition]); $author["contact-unknown"] = true; - $contact = Contact::getByURL($author["link"], null, ["id", "network"]); - $author["contact-id"] = $contact["id"] ?? $importer["id"]; - $author["network"] = $contact["network"] ?? $importer["network"]; - $onlyfetch = true; + $contact = Contact::getByURL($author["link"], null, ["id", "network"]); + $author["contact-id"] = $contact["id"] ?? $importer["id"]; + $author["network"] = $contact["network"] ?? $importer["network"]; + $onlyfetch = true; } // Until now we aren't serving different sizes - but maybe later @@ -1101,7 +1077,7 @@ class DFRN /// @todo check if "avatar" or "photo" would be the best field in the specification $avatars = $xpath->query($element . "/atom:link[@rel='avatar']", $context); foreach ($avatars as $avatar) { - $href = ""; + $href = ""; $width = 0; foreach ($avatar->attributes as $attributes) { /// @TODO Rewrite these similar if() to one switch @@ -1136,12 +1112,12 @@ class DFRN } if (empty($author['avatar'])) { - Logger::notice('Empty author: ' . $xml); + DI::logger()->notice('Empty author: ' . $xml); $author['avatar'] = ''; } if (DBA::isResult($contact_old) && !$onlyfetch) { - Logger::info("Check if contact details for contact " . $contact_old["id"] . " (" . $contact_old["nick"] . ") have to be updated."); + DI::logger()->info("Check if contact details for contact " . $contact_old["id"] . " (" . $contact_old["nick"] . ") have to be updated."); $poco = ["url" => $contact_old["url"], "network" => $contact_old["network"]]; @@ -1202,7 +1178,7 @@ class DFRN // If the "hide" element is present then the profile isn't searchable. $hide = intval(XML::getFirstNodeValue($xpath, $element . "/dfrn:hide/text()", $context) == "true"); - Logger::info("Hidden status for contact " . $contact_old["url"] . ": " . $hide); + DI::logger()->info("Hidden status for contact " . $contact_old["url"] . ": " . $hide); // If the contact isn't searchable then set the contact to "hidden". // Problem: This can be manually overridden by the user. @@ -1211,7 +1187,7 @@ class DFRN } // Save the keywords into the contact table - $tags = []; + $tags = []; $tagelements = $xpath->evaluate($element . "/poco:tags/text()", $context); foreach ($tagelements as $tag) { $tags[$tag->nodeValue] = $tag->nodeValue; @@ -1237,7 +1213,7 @@ class DFRN if (!in_array($value, ["", "0000-00-00", DBA::NULL_DATE])) { $bdyear = date("Y"); - $value = str_replace(["0000", "0001"], $bdyear, $value); + $value = str_replace(["0000", "0001"], $bdyear, $value); if (strtotime($value) < time()) { $value = str_replace($bdyear, $bdyear + 1, $value); @@ -1253,10 +1229,10 @@ class DFRN } $fields = ['name' => $contact['name'], 'nick' => $contact['nick'], 'about' => $contact['about'], - 'location' => $contact['location'], 'addr' => $contact['addr'], 'keywords' => $contact['keywords'], - 'bdyear' => $contact['bdyear'], 'bd' => $contact['bd'], 'hidden' => $contact['hidden'], - 'xmpp' => $contact['xmpp'], 'name-date' => DateTimeFormat::utc($contact['name-date']), - 'unsearchable' => $contact['hidden'], 'uri-date' => DateTimeFormat::utc($contact['uri-date'])]; + 'location' => $contact['location'], 'addr' => $contact['addr'], 'keywords' => $contact['keywords'], + 'bdyear' => $contact['bdyear'], 'bd' => $contact['bd'], 'hidden' => $contact['hidden'], + 'xmpp' => $contact['xmpp'], 'name-date' => DateTimeFormat::utc($contact['name-date']), + 'unsearchable' => $contact['hidden'], 'uri-date' => DateTimeFormat::utc($contact['uri-date'])]; Contact::update($fields, ['id' => $contact['id'], 'network' => $contact['network']], $contact_old); @@ -1292,7 +1268,7 @@ class DFRN return ""; } - $obj_doc = new DOMDocument("1.0", "utf-8"); + $obj_doc = new DOMDocument("1.0", "utf-8"); $obj_doc->formatOutput = true; $obj_element = $obj_doc->createElementNS( ActivityNamespace::ATOM1, $element); @@ -1342,19 +1318,19 @@ class DFRN */ private static function processMail(DOMXPath $xpath, DOMNode $mail, array $importer) { - Logger::info("Processing mails"); + DI::logger()->info("Processing mails"); - $msg = []; - $msg['uid'] = $importer['importer_uid']; - $msg['from-name'] = XML::getFirstValue($xpath, 'dfrn:sender/dfrn:name/text()', $mail); - $msg['from-url'] = XML::getFirstValue($xpath, 'dfrn:sender/dfrn:uri/text()', $mail); + $msg = []; + $msg['uid'] = $importer['importer_uid']; + $msg['from-name'] = XML::getFirstValue($xpath, 'dfrn:sender/dfrn:name/text()', $mail); + $msg['from-url'] = XML::getFirstValue($xpath, 'dfrn:sender/dfrn:uri/text()', $mail); $msg['from-photo'] = XML::getFirstValue($xpath, 'dfrn:sender/dfrn:avatar/text()', $mail); $msg['contact-id'] = $importer['id']; - $msg['uri'] = XML::getFirstValue($xpath, 'dfrn:id/text()', $mail); + $msg['uri'] = XML::getFirstValue($xpath, 'dfrn:id/text()', $mail); $msg['parent-uri'] = XML::getFirstValue($xpath, 'dfrn:in-reply-to/text()', $mail); - $msg['created'] = DateTimeFormat::utc(XML::getFirstValue($xpath, 'dfrn:sentdate/text()', $mail)); - $msg['title'] = XML::getFirstValue($xpath, 'dfrn:subject/text()', $mail); - $msg['body'] = XML::getFirstValue($xpath, 'dfrn:content/text()', $mail); + $msg['created'] = DateTimeFormat::utc(XML::getFirstValue($xpath, 'dfrn:sentdate/text()', $mail)); + $msg['title'] = XML::getFirstValue($xpath, 'dfrn:subject/text()', $mail); + $msg['body'] = XML::getFirstValue($xpath, 'dfrn:content/text()', $mail); Mail::insert($msg); } @@ -1370,10 +1346,10 @@ class DFRN */ private static function processSuggestion(DOMXPath $xpath, DOMNode $suggestion, array $importer) { - Logger::info('Processing suggestions'); + DI::logger()->info('Processing suggestions'); - $url = $xpath->evaluate('string(dfrn:url[1]/text())', $suggestion); - $cid = Contact::getIdForURL($url); + $url = $xpath->evaluate('string(dfrn:url[1]/text())', $suggestion); + $cid = Contact::getIdForURL($url); $note = $xpath->evaluate('string(dfrn:note[1]/text())', $suggestion); return self::addSuggestion($importer['importer_uid'], $cid, $importer['id'], $note); @@ -1389,8 +1365,8 @@ class DFRN */ private static function addSuggestion(int $uid, int $cid, int $from_cid, string $note = ''): bool { - $owner = User::getOwnerDataById($uid); - $contact = Contact::getById($cid); + $owner = User::getOwnerDataById($uid); + $contact = Contact::getById($cid); $from_contact = Contact::getById($from_cid); if (DBA::exists('contact', ['nurl' => Strings::normaliseLink($contact['url']), 'uid' => $uid])) { @@ -1402,15 +1378,15 @@ class DFRN return false; } - $suggest = []; - $suggest['uid'] = $uid; - $suggest['cid'] = $from_cid; - $suggest['url'] = $contact['url']; - $suggest['name'] = $contact['name']; - $suggest['photo'] = $contact['photo']; + $suggest = []; + $suggest['uid'] = $uid; + $suggest['cid'] = $from_cid; + $suggest['url'] = $contact['url']; + $suggest['name'] = $contact['name']; + $suggest['photo'] = $contact['photo']; $suggest['request'] = $contact['request']; - $suggest['title'] = ''; - $suggest['body'] = $note; + $suggest['title'] = ''; + $suggest['body'] = $note; DI::intro()->save(DI::introFactory()->createNew( $suggest['uid'], @@ -1446,23 +1422,23 @@ class DFRN */ private static function processRelocation(DOMXPath $xpath, DOMNode $relocation, array $importer): bool { - Logger::info("Processing relocations"); + DI::logger()->info("Processing relocations"); /// @TODO Rewrite this to one statement - $relocate = []; - $relocate['uid'] = $importer['importer_uid']; - $relocate['cid'] = $importer['id']; - $relocate['url'] = $xpath->query('dfrn:url/text()', $relocation)->item(0)->nodeValue; - $relocate['addr'] = $xpath->query('dfrn:addr/text()', $relocation)->item(0)->nodeValue; - $relocate['name'] = $xpath->query('dfrn:name/text()', $relocation)->item(0)->nodeValue; - $relocate['avatar'] = $xpath->query('dfrn:avatar/text()', $relocation)->item(0)->nodeValue; - $relocate['photo'] = $xpath->query('dfrn:photo/text()', $relocation)->item(0)->nodeValue; - $relocate['thumb'] = $xpath->query('dfrn:thumb/text()', $relocation)->item(0)->nodeValue; - $relocate['micro'] = $xpath->query('dfrn:micro/text()', $relocation)->item(0)->nodeValue; - $relocate['request'] = $xpath->query('dfrn:request/text()', $relocation)->item(0)->nodeValue; - $relocate['confirm'] = $xpath->query('dfrn:confirm/text()', $relocation)->item(0)->nodeValue; - $relocate['notify'] = $xpath->query('dfrn:notify/text()', $relocation)->item(0)->nodeValue; - $relocate['poll'] = $xpath->query('dfrn:poll/text()', $relocation)->item(0)->nodeValue; + $relocate = []; + $relocate['uid'] = $importer['importer_uid']; + $relocate['cid'] = $importer['id']; + $relocate['url'] = $xpath->query('dfrn:url/text()', $relocation)->item(0)->nodeValue; + $relocate['addr'] = $xpath->query('dfrn:addr/text()', $relocation)->item(0)->nodeValue; + $relocate['name'] = $xpath->query('dfrn:name/text()', $relocation)->item(0)->nodeValue; + $relocate['avatar'] = $xpath->query('dfrn:avatar/text()', $relocation)->item(0)->nodeValue; + $relocate['photo'] = $xpath->query('dfrn:photo/text()', $relocation)->item(0)->nodeValue; + $relocate['thumb'] = $xpath->query('dfrn:thumb/text()', $relocation)->item(0)->nodeValue; + $relocate['micro'] = $xpath->query('dfrn:micro/text()', $relocation)->item(0)->nodeValue; + $relocate['request'] = $xpath->query('dfrn:request/text()', $relocation)->item(0)->nodeValue; + $relocate['confirm'] = $xpath->query('dfrn:confirm/text()', $relocation)->item(0)->nodeValue; + $relocate['notify'] = $xpath->query('dfrn:notify/text()', $relocation)->item(0)->nodeValue; + $relocate['poll'] = $xpath->query('dfrn:poll/text()', $relocation)->item(0)->nodeValue; $relocate['sitepubkey'] = $xpath->query('dfrn:sitepubkey/text()', $relocation)->item(0)->nodeValue; if (($relocate['avatar'] == '') && ($relocate['photo'] != '')) { @@ -1476,21 +1452,21 @@ class DFRN // update contact $old = Contact::selectFirst(['photo', 'url'], ['id' => $importer['id'], 'uid' => $importer['importer_uid']]); if (!DBA::isResult($old)) { - Logger::warning('Existing contact had not been fetched', ['id' => $importer['id']]); + DI::logger()->warning('Existing contact had not been fetched', ['id' => $importer['id']]); return false; } // Update the contact table. We try to find every entry. $fields = [ - 'name' => $relocate['name'], - 'avatar' => $relocate['avatar'], - 'url' => $relocate['url'], - 'nurl' => Strings::normaliseLink($relocate['url']), - 'addr' => $relocate['addr'], - 'request' => $relocate['request'], - 'confirm' => $relocate['confirm'], - 'notify' => $relocate['notify'], - 'poll' => $relocate['poll'], + 'name' => $relocate['name'], + 'avatar' => $relocate['avatar'], + 'url' => $relocate['url'], + 'nurl' => Strings::normaliseLink($relocate['url']), + 'addr' => $relocate['addr'], + 'request' => $relocate['request'], + 'confirm' => $relocate['confirm'], + 'notify' => $relocate['notify'], + 'poll' => $relocate['poll'], 'site-pubkey' => $relocate['sitepubkey'], ]; $condition = ["(`id` = ?) OR (`nurl` = ?)", $importer['id'], Strings::normaliseLink($old['url'])]; @@ -1499,7 +1475,7 @@ class DFRN Contact::updateAvatar($importer['id'], $relocate['avatar'], true); - Logger::info('Contacts are updated.'); + DI::logger()->info('Contacts are updated.'); /// @TODO /// merge with current record, current contents have priority @@ -1531,10 +1507,10 @@ class DFRN } $fields = [ - 'title' => $item['title'] ?? '', - 'body' => $item['body'] ?? '', + 'title' => $item['title'] ?? '', + 'body' => $item['body'] ?? '', 'changed' => DateTimeFormat::utcNow(), - 'edited' => DateTimeFormat::utc($item['edited']), + 'edited' => DateTimeFormat::utc($item['edited']), ]; $condition = ["`uri` = ? AND `uid` IN (0, ?)", $item['uri'], $importer['importer_uid']]; @@ -1582,7 +1558,7 @@ class DFRN */ private static function processVerbs(int $entrytype, array $importer, array &$item) { - Logger::info('Process verb ' . $item['verb'] . ' and object-type ' . $item['object-type'] . ' for entrytype ' . $entrytype); + DI::logger()->info('Process verb ' . $item['verb'] . ' and object-type ' . $item['object-type'] . ' for entrytype ' . $entrytype); if (($entrytype == self::TOP_LEVEL) && !empty($importer['id'])) { // The filling of the "contact" variable is done for legacy reasons @@ -1594,22 +1570,22 @@ class DFRN // Big question: Do we need these functions? They were part of the "consume_feed" function. // This function once was responsible for DFRN and OStatus. if ($activity->match($item['verb'], Activity::FOLLOW)) { - Logger::info("New follower"); + DI::logger()->info("New follower"); Contact::addRelationship($importer, $contact, $item); return false; } if ($activity->match($item['verb'], Activity::UNFOLLOW)) { - Logger::info("Lost follower"); + DI::logger()->info("Lost follower"); Contact::removeFollower($contact); return false; } if ($activity->match($item['verb'], Activity::REQ_FRIEND)) { - Logger::info("New friend request"); + DI::logger()->info("New friend request"); Contact::addRelationship($importer, $contact, $item, true); return false; } if ($activity->match($item['verb'], Activity::UNFRIEND)) { - Logger::info("Lost sharer"); + DI::logger()->info("Lost sharer"); Contact::removeSharer($contact); return false; } @@ -1636,16 +1612,16 @@ class DFRN } $condition = ['uid' => $item['uid'], 'author-id' => $item['author-id'], 'gravity' => Item::GRAVITY_ACTIVITY, - 'verb' => $item['verb'], 'thr-parent' => $item['thr-parent']]; + 'verb' => $item['verb'], 'thr-parent' => $item['thr-parent']]; if (Post::exists($condition)) { return false; } // The owner of an activity must be the author - $item['owner-name'] = $item['author-name']; - $item['owner-link'] = $item['author-link']; + $item['owner-name'] = $item['author-name']; + $item['owner-link'] = $item['author-link']; $item['owner-avatar'] = $item['author-avatar']; - $item['owner-id'] = $item['author-id']; + $item['owner-id'] = $item['author-id']; } if (($item['verb'] == Activity::TAG) && ($item['object-type'] == Activity\ObjectType::TAGTERM)) { @@ -1655,7 +1631,7 @@ class DFRN if ($xt->type == Activity\ObjectType::NOTE) { $item_tag = Post::selectFirst(['id', 'uri-id'], ['uri' => $xt->id, 'uid' => $importer['importer_uid']]); if (!DBA::isResult($item_tag)) { - Logger::warning('Post had not been fetched', ['uri' => $xt->id, 'uid' => $importer['importer_uid']]); + DI::logger()->warning('Post had not been fetched', ['uri' => $xt->id, 'uid' => $importer['importer_uid']]); return false; } @@ -1679,19 +1655,24 @@ class DFRN */ private static function parseLinks($links, array &$item) { - $rel = ''; - $href = ''; - $type = null; + $rel = ''; + $href = ''; + $type = null; $length = null; - $title = null; + $title = null; foreach ($links as $link) { foreach ($link->attributes as $attributes) { switch ($attributes->name) { - case 'href' : $href = $attributes->textContent; break; - case 'rel' : $rel = $attributes->textContent; break; - case 'type' : $type = $attributes->textContent; break; - case 'length': $length = $attributes->textContent; break; - case 'title' : $title = $attributes->textContent; break; + case 'href': $href = $attributes->textContent; + break; + case 'rel': $rel = $attributes->textContent; + break; + case 'type': $type = $attributes->textContent; + break; + case 'length': $length = $attributes->textContent; + break; + case 'title': $title = $attributes->textContent; + break; } } if (($rel != '') && ($href != '')) { @@ -1702,7 +1683,7 @@ class DFRN case 'enclosure': Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::DOCUMENT, - 'url' => $href, 'mimetype' => $type, 'size' => $length, 'description' => $title]); + 'url' => $href, 'mimetype' => $type, 'size' => $length, 'description' => $title]); break; } } @@ -1720,23 +1701,23 @@ class DFRN { if (DBA::exists('contact', ["`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)", Strings::normaliseLink($item["author-link"]), 0, Contact::FRIEND, Contact::SHARING])) { - Logger::debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $item["author-link"]]); + DI::logger()->debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $item["author-link"]]); return true; } if ($importer['importer_uid'] != 0) { - Logger::debug('Message is directed to a user - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'importer' => $importer['importer_uid']]); + DI::logger()->debug('Message is directed to a user - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'importer' => $importer['importer_uid']]); return true; } if ($item['uri'] != $item['thr-parent']) { - Logger::debug('Message is no parent - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); + DI::logger()->debug('Message is no parent - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri']]); return true; } $tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name'); if (Relay::isSolicitedPost($tags, $item['body'], $item['author-id'], $item['uri'], Protocol::DFRN)) { - Logger::debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $item["author-link"]]); + DI::logger()->debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $item["author-link"]]); return true; } else { return false; @@ -1759,7 +1740,7 @@ class DFRN */ private static function processEntry(array $header, DOMXPath $xpath, DOMNode $entry, array $importer, string $xml, int $protocol) { - Logger::info("Processing entries"); + DI::logger()->info("Processing entries"); $item = $header; @@ -1770,12 +1751,13 @@ class DFRN $item['edited'] = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $entry); - $current = Post::selectFirst(['id', 'uid', 'edited', 'body'], + $current = Post::selectFirst( + ['id', 'uid', 'edited', 'body'], ['uri' => $item['uri'], 'uid' => $importer['importer_uid']] ); // Is there an existing item? if (DBA::isResult($current) && !self::isEditedTimestampNewer($current, $item)) { - Logger::info("Item " . $item['uri'] . " (" . $item['edited'] . ") already existed."); + DI::logger()->info("Item " . $item['uri'] . " (" . $item['edited'] . ") already existed."); return; } @@ -1784,18 +1766,18 @@ class DFRN $owner_unknown = (isset($owner['contact-unknown']) && $owner['contact-unknown']); - $item['owner-name'] = $owner['name']; - $item['owner-link'] = $owner['link']; + $item['owner-name'] = $owner['name']; + $item['owner-link'] = $owner['link']; $item['owner-avatar'] = $owner['avatar']; - $item['owner-id'] = Contact::getIdForURL($owner['link'], 0); + $item['owner-id'] = Contact::getIdForURL($owner['link'], 0); // fetch the author $author = self::fetchauthor($xpath, $entry, $importer, 'atom:author', true, $xml); - $item['author-name'] = $author['name']; - $item['author-link'] = $author['link']; + $item['author-name'] = $author['name']; + $item['author-link'] = $author['link']; $item['author-avatar'] = $author['avatar']; - $item['author-id'] = Contact::getIdForURL($author['link'], 0); + $item['author-id'] = Contact::getIdForURL($author['link'], 0); $item['title'] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry); @@ -1819,8 +1801,8 @@ class DFRN // We don't need the content element since "dfrn:env" is always present //$item['body'] = $xpath->query('atom:content/text()', $entry)->item(0)->nodeValue; $item['location'] = XML::getFirstNodeValue($xpath, 'dfrn:location/text()', $entry); - $item['coord'] = XML::getFirstNodeValue($xpath, 'georss:point', $entry); - $item['private'] = XML::getFirstNodeValue($xpath, 'dfrn:private/text()', $entry); + $item['coord'] = XML::getFirstNodeValue($xpath, 'georss:point', $entry); + $item['private'] = XML::getFirstNodeValue($xpath, 'dfrn:private/text()', $entry); $unlisted = XML::getFirstNodeValue($xpath, 'dfrn:unlisted/text()', $entry); if (!empty($unlisted) && ($item['private'] != Item::PRIVATE)) { @@ -1870,7 +1852,7 @@ class DFRN $item['object-type'] = XML::getFirstNodeValue($xpath, 'activity:object-type/text()', $entry); } - $object = $xpath->query('activity:object', $entry)->item(0); + $object = $xpath->query('activity:object', $entry)->item(0); $item['object'] = self::transformActivity($xpath, $object, 'object'); if (trim($item['object']) != '') { @@ -1880,13 +1862,13 @@ class DFRN } } - $target = $xpath->query('activity:target', $entry)->item(0); + $target = $xpath->query('activity:target', $entry)->item(0); $item['target'] = self::transformActivity($xpath, $target, 'target'); $categories = $xpath->query('atom:category', $entry); if ($categories) { foreach ($categories as $category) { - $term = ''; + $term = ''; $scheme = ''; foreach ($category->attributes as $attributes) { if ($attributes->name == 'term') { @@ -1943,7 +1925,7 @@ class DFRN // Check if the message is wanted if (!self::isSolicitedMessage($item, $importer)) { DBA::delete('item-uri', ['uri' => $item['uri']]); - return 403; + return; } // Get the type of the item (Top level post, reply or remote reply) @@ -1985,10 +1967,10 @@ class DFRN // Is it an event? if (($item['object-type'] == Activity\ObjectType::EVENT) && !$owner_unknown) { - Logger::info("Item " . $item['uri'] . " seems to contain an event."); + DI::logger()->info("Item " . $item['uri'] . " seems to contain an event."); $ev = Event::fromBBCode($item['body']); if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) { - Logger::info("Event in item " . $item['uri'] . " was found."); + DI::logger()->info("Event in item " . $item['uri'] . " was found."); $ev['cid'] = $importer['id']; $ev['uid'] = $importer['importer_uid']; $ev['uri'] = $item['uri']; @@ -2002,13 +1984,13 @@ class DFRN $ev['source'] = $item['source']; $condition = ['uri' => $item['uri'], 'uid' => $importer['importer_uid']]; - $event = DBA::selectFirst('event', ['id'], $condition); + $event = DBA::selectFirst('event', ['id'], $condition); if (DBA::isResult($event)) { $ev['id'] = $event['id']; } $event_id = Event::store($ev); - Logger::info('Event was stored', ['id' => $event_id]); + DI::logger()->info('Event was stored', ['id' => $event_id]); $item = Event::getItemArrayForImportedId($event_id, $item); } @@ -2016,13 +1998,13 @@ class DFRN } if (!self::processVerbs($entrytype, $importer, $item)) { - Logger::info("Exiting because 'processVerbs' told us so"); + DI::logger()->info("Exiting because 'processVerbs' told us so"); return; } // This check is done here to be able to receive connection requests in "processVerbs" if (($entrytype == self::TOP_LEVEL) && $owner_unknown) { - Logger::info("Item won't be stored because user " . $importer['importer_uid'] . " doesn't follow " . $item['owner-link'] . "."); + DI::logger()->info("Item won't be stored because user " . $importer['importer_uid'] . " doesn't follow " . $item['owner-link'] . "."); return; } @@ -2030,9 +2012,9 @@ class DFRN // Update content if 'updated' changes if (DBA::isResult($current)) { if (self::updateContent($current, $item, $importer, $entrytype)) { - Logger::info("Item " . $item['uri'] . " was updated."); + DI::logger()->info("Item " . $item['uri'] . " was updated."); } else { - Logger::info("Item " . $item['uri'] . " already existed."); + DI::logger()->info("Item " . $item['uri'] . " already existed."); } return; } @@ -2040,20 +2022,20 @@ class DFRN if (in_array($entrytype, [self::REPLY, self::REPLY_RC])) { if (($item['uid'] != 0) && !Post::exists(['uid' => $item['uid'], 'uri' => $item['thr-parent']])) { if (DI::pConfig()->get($item['uid'], 'system', 'accept_only_sharer') == Item::COMPLETION_NONE) { - Logger::info('Completion is set to "none", so we stop here.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); + DI::logger()->info('Completion is set to "none", so we stop here.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); return; } if (!Contact::isSharing($item['owner-id'], $item['uid']) && !Contact::isSharing($item['author-id'], $item['uid'])) { - Logger::info('Contact is not sharing with the user', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); + DI::logger()->info('Contact is not sharing with the user', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); return; } if (($item['gravity'] == Item::GRAVITY_ACTIVITY) && DI::pConfig()->get($item['uid'], 'system', 'accept_only_sharer') == Item::COMPLETION_COMMENT) { - Logger::info('Completion is set to "comment", but this is an activity. so we stop here.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); + DI::logger()->info('Completion is set to "comment", but this is an activity. so we stop here.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); return; } - Logger::debug('Post is accepted.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); + DI::logger()->debug('Post is accepted.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); } else { - Logger::debug('Thread parent exists.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); + DI::logger()->debug('Thread parent exists.', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); } // Will be overwritten for sharing accounts in Item::insert @@ -2063,23 +2045,23 @@ class DFRN $posted_id = Item::insert($item); if ($posted_id) { - Logger::info("Reply from contact " . $item['contact-id'] . " was stored with id " . $posted_id); + DI::logger()->info("Reply from contact " . $item['contact-id'] . " was stored with id " . $posted_id); if ($item['uid'] == 0) { Item::distribute($posted_id); } - return true; + return; } } else { // $entrytype == self::TOP_LEVEL if (($item['uid'] != 0) && !Contact::isSharing($item['owner-id'], $item['uid']) && !Contact::isSharing($item['author-id'], $item['uid'])) { - Logger::info('Contact is not sharing with the user', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); + DI::logger()->info('Contact is not sharing with the user', ['uid' => $item['uid'], 'owner-id' => $item['owner-id'], 'author-id' => $item['author-id'], 'gravity' => $item['gravity'], 'uri' => $item['uri']]); return; } // This is my contact on another system, but it's really me. // Turn this into a wall post. - $notify = Item::isRemoteSelf($importer, $item); + $notify = Item::isRemoteSelf($importer, $item); $item['wall'] = (bool)$notify; $posted_id = Item::insert($item, $notify); @@ -2088,7 +2070,7 @@ class DFRN $posted_id = $notify; } - Logger::info("Item was stored with id " . $posted_id); + DI::logger()->info("Item was stored with id " . $posted_id); if ($item['uid'] == 0) { Item::distribute($posted_id); @@ -2107,7 +2089,7 @@ class DFRN */ private static function processDeletion(DOMXPath $xpath, DOMNode $deletion, array $importer) { - Logger::info("Processing deletions"); + DI::logger()->info("Processing deletions"); $uri = null; foreach ($deletion->attributes as $attributes) { @@ -2117,24 +2099,24 @@ class DFRN } if (!$uri || !$importer['id']) { - return false; + return; } $condition = ['uri' => $uri, 'uid' => $importer['importer_uid']]; - $item = Post::selectFirst(['id', 'parent', 'contact-id', 'uri-id', 'deleted', 'gravity'], $condition); + $item = Post::selectFirst(['id', 'parent', 'contact-id', 'uri-id', 'deleted', 'gravity'], $condition); if (!DBA::isResult($item)) { - Logger::info('Item with URI ' . $uri . ' for user ' . $importer['importer_uid'] . ' was not found.'); + DI::logger()->info('Item with URI ' . $uri . ' for user ' . $importer['importer_uid'] . ' was not found.'); return; } if (DBA::exists('post-category', ['uri-id' => $item['uri-id'], 'uid' => $importer['importer_uid'], 'type' => Post\Category::FILE])) { - Logger::notice('Item is filed. It will not be deleted.', ['uri' => $uri, 'uri-id' => $item['uri_id'], 'uid' => $importer['importer_uid']]); + DI::logger()->notice('Item is filed. It will not be deleted.', ['uri' => $uri, 'uri-id' => $item['uri_id'], 'uid' => $importer['importer_uid']]); return; } // When it is a starting post it has to belong to the person that wants to delete it if (($item['gravity'] == Item::GRAVITY_PARENT) && ($item['contact-id'] != $importer['id'])) { - Logger::info('Item with URI ' . $uri . ' do not belong to contact ' . $importer['id'] . ' - ignoring deletion.'); + DI::logger()->info('Item with URI ' . $uri . ' do not belong to contact ' . $importer['id'] . ' - ignoring deletion.'); return; } @@ -2142,7 +2124,7 @@ class DFRN if (($item['gravity'] != Item::GRAVITY_PARENT) && ($item['contact-id'] != $importer['id'])) { $condition = ['id' => $item['parent'], 'contact-id' => $importer['id']]; if (!Post::exists($condition)) { - Logger::info('Item with URI ' . $uri . ' was not found or must not be deleted by contact ' . $importer['id'] . ' - ignoring deletion.'); + DI::logger()->info('Item with URI ' . $uri . ' was not found or must not be deleted by contact ' . $importer['id'] . ' - ignoring deletion.'); return; } } @@ -2151,7 +2133,7 @@ class DFRN return; } - Logger::info('deleting item '.$item['id'].' uri='.$uri); + DI::logger()->info('deleting item '.$item['id'].' uri='.$uri); Item::markForDeletion(['id' => $item['id']]); } @@ -2188,12 +2170,12 @@ class DFRN $xpath->registerNamespace('ostatus', ActivityNamespace::OSTATUS); $xpath->registerNamespace('statusnet', ActivityNamespace::STATUSNET); - $header = []; - $header['uid'] = $importer['importer_uid']; - $header['network'] = Protocol::DFRN; - $header['protocol'] = $protocol; - $header['wall'] = 0; - $header['origin'] = 0; + $header = []; + $header['uid'] = $importer['importer_uid']; + $header['network'] = Protocol::DFRN; + $header['protocol'] = $protocol; + $header['wall'] = 0; + $header['origin'] = 0; $header['contact-id'] = $importer['id']; $header = Diaspora::setDirection($header, $direction); @@ -2214,7 +2196,7 @@ class DFRN self::fetchauthor($xpath, $doc->firstChild, $importer, 'dfrn:owner', false, $xml); } - Logger::info("Import DFRN message for user " . $importer['importer_uid'] . " from contact " . $importer['id']); + DI::logger()->info("Import DFRN message for user " . $importer['importer_uid'] . " from contact " . $importer['id']); if (!empty($importer['gsid']) && ($protocol == Conversation::PARCEL_DIASPORA_DFRN)) { GServer::setProtocol($importer['gsid'], Post\DeliveryData::DFRN); @@ -2287,7 +2269,7 @@ class DFRN self::processDeletion($xpath, $deletion, $importer); } if (count($deletions) > 0) { - Logger::info(count($deletions) . ' deletions had been processed'); + DI::logger()->info(count($deletions) . ' deletions had been processed'); return 200; } } @@ -2297,7 +2279,7 @@ class DFRN self::processEntry($header, $xpath, $entry, $importer, $xml, $protocol); } - Logger::info("Import done for user " . $importer['importer_uid'] . " from contact " . $importer['id']); + DI::logger()->info("Import done for user " . $importer['importer_uid'] . " from contact " . $importer['id']); return 200; } @@ -2339,7 +2321,7 @@ class DFRN } $existing_edited = DateTimeFormat::utc($existing['edited']); - $update_edited = DateTimeFormat::utc($update['edited']); + $update_edited = DateTimeFormat::utc($update['edited']); return (strcmp($existing_edited, $update_edited) < 0); } diff --git a/src/Protocol/Delivery.php b/src/Protocol/Delivery.php index 9f8174e2d4..d99b67c9a8 100644 --- a/src/Protocol/Delivery.php +++ b/src/Protocol/Delivery.php @@ -1,32 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; use Friendica\Contact\FriendSuggest\Collection\FriendSuggests; use Friendica\Contact\FriendSuggest\Exception\FriendSuggestNotFoundException; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Worker; -use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; @@ -57,11 +41,13 @@ class Delivery */ public static function deliver(string $cmd, int $post_uriid, int $contact_id, int $sender_uid = 0): bool { - Logger::info('Invoked', ['cmd' => $cmd, 'target' => $post_uriid, 'sender_uid' => $sender_uid, 'contact' => $contact_id]); + DI::logger()->info('Invoked', ['cmd' => $cmd, 'target' => $post_uriid, 'sender_uid' => $sender_uid, 'contact' => $contact_id]); $top_level = false; $followup = false; $public_message = false; + $parent = false; + $thr_parent = false; $items = []; if ($cmd == self::MAIL) { @@ -84,7 +70,7 @@ class Delivery } else { $item = Post::selectFirst(['id', 'parent'], ['uri-id' => $post_uriid, 'uid' => $sender_uid]); if (!DBA::isResult($item) || empty($item['parent'])) { - Logger::warning('Post not found', ['uri-id' => $post_uriid, 'uid' => $sender_uid]); + DI::logger()->warning('Post not found', ['uri-id' => $post_uriid, 'uid' => $sender_uid]); return true; } $target_id = intval($item['id']); @@ -110,12 +96,12 @@ class Delivery DBA::close($itemdata); if (empty($target_item)) { - Logger::warning("No target item data. Quitting here.", ['id' => $target_id]); + DI::logger()->warning("No target item data. Quitting here.", ['id' => $target_id]); return true; } if (empty($parent)) { - Logger::warning('Parent ' . $parent_id . ' for item ' . $target_id . "wasn't found. Quitting here."); + DI::logger()->warning('Parent ' . $parent_id . ' for item ' . $target_id . "wasn't found. Quitting here."); self::setFailedQueue($cmd, $target_item); return true; } @@ -125,7 +111,7 @@ class Delivery } elseif (!empty($target_item['uid'])) { $uid = $target_item['uid']; } else { - Logger::info('Only public users for item ' . $target_id); + DI::logger()->info('Only public users for item ' . $target_id); self::setFailedQueue($cmd, $target_item); return true; } @@ -139,7 +125,7 @@ class Delivery } if (!empty($contact_id) && Contact::isArchived($contact_id)) { - Logger::info('Contact is archived', ['id' => $contact_id, 'cmd' => $cmd, 'item' => $target_item['id']]); + DI::logger()->info('Contact is archived', ['id' => $contact_id, 'cmd' => $cmd, 'item' => $target_item['id']]); self::setFailedQueue($cmd, $target_item); return true; } @@ -174,7 +160,7 @@ class Delivery */ if (!$top_level && ($parent['wall'] == 0) && stristr($target_item['uri'], $localhost)) { - Logger::info('Followup ' . $target_item["guid"]); + DI::logger()->info('Followup ' . $target_item["guid"]); // local followup to remote post $followup = true; } @@ -189,7 +175,7 @@ class Delivery } if (empty($items)) { - Logger::warning('No delivery data', ['command' => $cmd, 'uri-id' => $post_uriid, 'cid' => $contact_id]); + DI::logger()->warning('No delivery data', ['command' => $cmd, 'uri-id' => $post_uriid, 'cid' => $contact_id]); } $owner = User::getOwnerDataById($uid); @@ -199,7 +185,9 @@ class Delivery } // We don't deliver our items to blocked, archived or pending contacts, and not to ourselves either - $contact = DBA::selectFirst('contact', [], + $contact = DBA::selectFirst( + 'contact', + [], ['id' => $contact_id, 'archive' => false, 'blocked' => false, 'pending' => false, 'self' => false] ); if (!DBA::isResult($contact)) { @@ -219,11 +207,11 @@ class Delivery // This is done since the uri wouldn't match (Diaspora doesn't transmit it) // Also transmit relayed posts from Diaspora contacts via Diaspora. if (($contact['network'] != Protocol::DIASPORA) && in_array(Protocol::DIASPORA, [$parent['network'] ?? '', $thr_parent['network'] ?? '', $target_item['network'] ?? ''])) { - Logger::info('Enforcing the Diaspora protocol', ['id' => $contact['id'], 'network' => $contact['network'], 'parent' => $parent['network'], 'thread-parent' => $thr_parent['network'], 'post' => $target_item['network']]); + DI::logger()->info('Enforcing the Diaspora protocol', ['id' => $contact['id'], 'network' => $contact['network'], 'parent' => $parent['network'], 'thread-parent' => $thr_parent['network'], 'post' => $target_item['network']]); $contact['network'] = Protocol::DIASPORA; } - Logger::notice('Delivering', ['cmd' => $cmd, 'uri-id' => $post_uriid, 'followup' => $followup, 'network' => $contact['network']]); + DI::logger()->notice('Delivering', ['cmd' => $cmd, 'uri-id' => $post_uriid, 'followup' => $followup, 'network' => $contact['network']]); switch ($contact['network']) { case Protocol::DFRN: @@ -286,11 +274,11 @@ class Delivery // Transmit Diaspora reshares via Diaspora if the Friendica contact support Diaspora if (Diaspora::getReshareDetails($target_item) && Diaspora::isSupportedByContactUrl($contact['addr'])) { - Logger::info('Reshare will be transmitted via Diaspora', ['url' => $contact['url'], 'guid' => $target_item_id]); + DI::logger()->info('Reshare will be transmitted via Diaspora', ['url' => $contact['url'], 'guid' => $target_item_id]); return self::deliverDiaspora($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); } - Logger::info('Deliver ' . ($target_item_id ?? 'relocation') . ' via DFRN to ' . ($contact['addr'] ?? '' ?: $contact['url'])); + DI::logger()->info('Deliver ' . ($target_item_id ?? 'relocation') . ' via DFRN to ' . ($contact['addr'] ?? '' ?: $contact['url'])); if ($cmd == self::MAIL) { $item = $target_item; @@ -322,7 +310,7 @@ class Delivery $atom = DFRN::entries($msgitems, $owner); } - Logger::debug('Notifier entry: ' . $contact['url'] . ' ' . ($target_item_id ?? 'relocation') . ' entry: ' . $atom); + DI::logger()->debug('Notifier entry', ['url' => $contact['url'], 'target_item_id' => ($target_item_id ?? 'relocation'), 'entry' => $atom]); $protocol = Post\DeliveryData::DFRN; @@ -336,18 +324,15 @@ class Delivery // We never spool failed relay deliveries if ($public_dfrn) { - Logger::info('Relay delivery to ' . $contact['url'] . ' with guid ' . $target_item['guid'] . ' returns ' . $deliver_status); + DI::logger()->info('Relay delivery to ' . $contact['url'] . ' with guid ' . $target_item['guid'] . ' returns ' . $deliver_status); + + $success = ($deliver_status >= 200) && ($deliver_status <= 299); if ($cmd == Delivery::POST) { - if (($deliver_status >= 200) && ($deliver_status <= 299)) { - Post\DeliveryData::incrementQueueDone($target_item['uri-id'], $protocol); - - GServer::setProtocol($contact['gsid'] ?? 0, $protocol); - $success = true; - } else { - Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); - $success = false; - } + Post\DeliveryData::incrementQueueDone($target_item['uri-id'], $protocol); + GServer::setProtocol($contact['gsid'] ?? 0, $protocol); + } else { + Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); } return $success; } @@ -361,7 +346,7 @@ class Delivery $deliver_status = DFRN::transmit($owner, $contact, $atom); } - Logger::info('DFRN Delivery', ['cmd' => $cmd, 'url' => $contact['url'], 'guid' => $target_item_id, 'return' => $deliver_status]); + DI::logger()->info('DFRN Delivery', ['cmd' => $cmd, 'url' => $contact['url'], 'guid' => $target_item_id, 'return' => $deliver_status]); if (($deliver_status >= 200) && ($deliver_status <= 299)) { // We successfully delivered a message, the contact is alive @@ -377,7 +362,7 @@ class Delivery // The message could not be delivered. We mark the contact as "dead" Contact::markForArchival($contact); - Logger::info('Delivery failed: defer message', ['id' => $target_item_id]); + DI::logger()->info('Delivery failed: defer message', ['id' => $target_item_id]); if (!Worker::defer() && $cmd == Delivery::POST) { Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); } @@ -413,7 +398,7 @@ class Delivery $loc = $contact['addr']; } - Logger::notice('Deliver via Diaspora', ['target' => $target_item['id'], 'guid' => $target_item['guid'], 'to' => $loc]); + DI::logger()->notice('Deliver via Diaspora', ['target' => $target_item['id'], 'guid' => $target_item['guid'], 'to' => $loc]); if (!DI::config()->get('system', 'diaspora_enabled')) { return true; @@ -436,22 +421,22 @@ class Delivery $deliver_status = Diaspora::sendAccountMigration($owner, $contact, $owner['uid']); } elseif ($target_item['deleted'] && (($target_item['uri'] === $target_item['parent-uri']) || $followup)) { // top-level retraction - Logger::notice('diaspora retract: ' . $loc); + DI::logger()->notice('diaspora retract: ' . $loc); $deliver_status = Diaspora::sendRetraction($target_item, $owner, $contact, $public_message); } elseif ($followup) { // send comments and likes to owner to relay - Logger::notice('diaspora followup: ' . $loc); + DI::logger()->notice('diaspora followup: ' . $loc); $deliver_status = Diaspora::sendFollowup($target_item, $owner, $contact, $public_message); } elseif ($target_item['uri'] !== $target_item['parent-uri']) { // we are the relay - send comments, likes and relayable_retractions to our conversants - Logger::notice('diaspora relay: ' . $loc); + DI::logger()->notice('diaspora relay: ' . $loc); $deliver_status = Diaspora::sendRelay($target_item, $owner, $contact, $public_message); } elseif ($top_level && !$walltowall) { // currently no workable solution for sending walltowall - Logger::notice('diaspora status: ' . $loc); + DI::logger()->notice('diaspora status: ' . $loc); $deliver_status = Diaspora::sendStatus($target_item, $owner, $contact, $public_message); } else { - Logger::warning('Unknown mode', ['command' => $cmd, 'target' => $loc]); + DI::logger()->warning('Unknown mode', ['command' => $cmd, 'target' => $loc]); return true; } @@ -475,7 +460,7 @@ class Delivery } if (empty($contact['contact-type']) || ($contact['contact-type'] != Contact::TYPE_RELAY)) { - Logger::info('Delivery failed: defer message', ['id' => ($target_item['guid'] ?? '') ?: $target_item['id']]); + DI::logger()->info('Delivery failed: defer message', ['id' => ($target_item['guid'] ?? '') ?: $target_item['id']]); // defer message for redelivery if (!Worker::defer() && $cmd == Delivery::POST) { Post\DeliveryData::incrementQueueFailed($target_item['uri-id']); @@ -524,10 +509,10 @@ class Delivery $data = json_decode($thr_parent['object'], true); if (!empty($data['reply_to'])) { $addr = $data['reply_to'][0]['mailbox'] . '@' . $data['reply_to'][0]['host']; - Logger::info('Use "reply-to" address of the thread parent', ['addr' => $addr]); + DI::logger()->info('Use "reply-to" address of the thread parent', ['addr' => $addr]); } elseif (!empty($data['from'])) { $addr = $data['from'][0]['mailbox'] . '@' . $data['from'][0]['host']; - Logger::info('Use "from" address of the thread parent', ['addr' => $addr]); + DI::logger()->info('Use "from" address of the thread parent', ['addr' => $addr]); } } @@ -536,7 +521,7 @@ class Delivery return true; } - Logger::info('About to deliver via mail', ['guid' => $target_item['guid'], 'to' => $addr]); + DI::logger()->info('About to deliver via mail', ['guid' => $target_item['guid'], 'to' => $addr]); $reply_to = ''; $mailacct = DBA::selectFirst('mailacct', ['reply_to'], ['uid' => $owner['uid']]); @@ -550,10 +535,10 @@ class Delivery if (($contact['rel'] == Contact::FRIEND) && !$contact['blocked']) { if ($reply_to) { - $headers = 'From: ' . Email::encodeHeader($local_user['username'],'UTF-8') . ' <' . $reply_to . '>' . "\n"; + $headers = 'From: ' . Email::encodeHeader($local_user['username'], 'UTF-8') . ' <' . $reply_to . '>' . "\n"; $headers .= 'Sender: ' . $local_user['email'] . "\n"; } else { - $headers = 'From: ' . Email::encodeHeader($local_user['username'],'UTF-8') . ' <' . $local_user['email'] . '>' . "\n"; + $headers = 'From: ' . Email::encodeHeader($local_user['username'], 'UTF-8') . ' <' . $local_user['email'] . '>' . "\n"; } } else { $sender = DI::config()->get('config', 'sender_email', 'noreply@' . DI::baseUrl()->getHost()); @@ -599,10 +584,10 @@ class Delivery if ($success) { // Success Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Post\DeliveryData::MAIL); - Logger::info('Delivered via mail', ['guid' => $target_item['guid'], 'to' => $addr, 'subject' => $subject]); + DI::logger()->info('Delivered via mail', ['guid' => $target_item['guid'], 'to' => $addr, 'subject' => $subject]); } else { // Failed - Logger::warning('Delivery of mail has FAILED', ['to' => $addr, 'subject' => $subject, 'guid' => $target_item['guid']]); + DI::logger()->warning('Delivery of mail has FAILED', ['to' => $addr, 'subject' => $subject, 'guid' => $target_item['guid']]); } return $success; } diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 59918287ab..0166e65f9d 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; @@ -25,7 +11,6 @@ use Friendica\Content\Feature; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\Markdown; use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Core\Worker; @@ -41,13 +26,16 @@ use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Network\HTTPException; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Network\HTTPException\NotFoundException; use Friendica\Network\Probe; -use Friendica\Protocol\Delivery; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Map; use Friendica\Util\Network; +use Friendica\Util\Proxy; use Friendica\Util\Strings; use Friendica\Util\XML; use GuzzleHttp\Psr7\Uri; @@ -79,7 +67,7 @@ class Diaspora public static function participantsForThread(array $item, array $contacts): array { if (!in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]) || in_array($item['verb'], [Activity::FOLLOW, Activity::TAG])) { - Logger::info('Item is private or a participation request. It will not be relayed', ['guid' => $item['guid'], 'private' => $item['private'], 'verb' => $item['verb']]); + DI::logger()->info('Item is private or a participation request. It will not be relayed', ['guid' => $item['guid'], 'private' => $item['private'], 'verb' => $item['verb']]); return $contacts; } @@ -109,7 +97,7 @@ class Diaspora } if (!$exists) { - Logger::info('Add participant to receiver list', ['parent' => $item['parent-guid'], 'item' => $item['guid'], 'participant' => $contact['url']]); + DI::logger()->info('Add participant to receiver list', ['parent' => $item['parent-guid'], 'item' => $item['guid'], 'participant' => $contact['url']]); $contacts[] = $contact; } } @@ -132,14 +120,14 @@ class Diaspora $basedom = XML::parseString($envelope, true); if (!is_object($basedom)) { - Logger::notice('Envelope is no XML file'); + DI::logger()->notice('Envelope is no XML file'); return false; } $children = $basedom->children(ActivityNamespace::SALMON_ME); if (sizeof($children) == 0) { - Logger::notice('XML has no children'); + DI::logger()->notice('XML has no children'); return false; } @@ -152,19 +140,19 @@ class Diaspora $alg = $children->alg; - $sig = Strings::base64UrlDecode($children->sig); + $sig = Strings::base64UrlDecode($children->sig); $key_id = $children->sig->attributes()->key_id[0]; if ($key_id != '') { $handle = Strings::base64UrlDecode($key_id); } $b64url_data = Strings::base64UrlEncode($data); - $msg = str_replace(["\n", "\r", " ", "\t"], ['', '', '', ''], $b64url_data); + $msg = str_replace(["\n", "\r", " ", "\t"], ['', '', '', ''], $b64url_data); $signable_data = $msg . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); if ($handle == '') { - Logger::notice('No author could be decoded. Discarding. Message: ' . $envelope); + DI::logger()->notice('No author could be decoded. Discarding. Message: ' . $envelope); return false; } @@ -174,13 +162,13 @@ class Diaspora throw new \InvalidArgumentException(); } } catch (\InvalidArgumentException $e) { - Logger::notice("Couldn't get a key for handle " . $handle . ". Discarding."); + DI::logger()->notice("Couldn't get a key for handle " . $handle . ". Discarding."); return false; } $verify = Crypto::rsaVerify($signable_data, $sig, $key); if (!$verify) { - Logger::notice('Message from ' . $handle . ' did not verify. Discarding.'); + DI::logger()->notice('Message from ' . $handle . ' did not verify. Discarding.'); return false; } @@ -237,33 +225,33 @@ class Diaspora if (is_object($data)) { try { if (!isset($data->aes_key) || !isset($data->encrypted_magic_envelope)) { - Logger::info('Missing keys "aes_key" and/or "encrypted_magic_envelope"', ['data' => $data]); + DI::logger()->info('Missing keys "aes_key" and/or "encrypted_magic_envelope"', ['data' => $data]); throw new \RuntimeException('Missing keys "aes_key" and/or "encrypted_magic_envelope"'); } $encrypted_aes_key_bundle = base64_decode($data->aes_key); - $ciphertext = base64_decode($data->encrypted_magic_envelope); + $ciphertext = base64_decode($data->encrypted_magic_envelope); $outer_key_bundle = ''; @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); $j_outer_key_bundle = json_decode($outer_key_bundle); if (!is_object($j_outer_key_bundle)) { - Logger::info('Unable to decode outer key bundle', ['outer_key_bundle' => $outer_key_bundle]); + DI::logger()->info('Unable to decode outer key bundle', ['outer_key_bundle' => $outer_key_bundle]); throw new \RuntimeException('Unable to decode outer key bundle'); } if (!isset($j_outer_key_bundle->iv) || !isset($j_outer_key_bundle->key)) { - Logger::info('Missing keys "iv" and/or "key" from outer Salmon', ['j_outer_key_bundle' => $j_outer_key_bundle]); + DI::logger()->info('Missing keys "iv" and/or "key" from outer Salmon', ['j_outer_key_bundle' => $j_outer_key_bundle]); throw new \RuntimeException('Missing keys "iv" and/or "key" from outer Salmon'); } - $outer_iv = base64_decode($j_outer_key_bundle->iv); + $outer_iv = base64_decode($j_outer_key_bundle->iv); $outer_key = base64_decode($j_outer_key_bundle->key); $xml = self::aesDecrypt($outer_key, $outer_iv, $ciphertext); } catch (\Throwable $e) { - Logger::notice('Outer Salmon did not verify. Discarding.'); + DI::logger()->notice('Outer Salmon did not verify. Discarding.'); if ($no_exit) { return false; } else { @@ -277,7 +265,7 @@ class Diaspora $basedom = XML::parseString($xml, true); if (!is_object($basedom)) { - Logger::notice('Received data does not seem to be an XML. Discarding. ' . $xml); + DI::logger()->notice('Received data does not seem to be an XML. Discarding. ' . $xml); if ($no_exit) { return false; } else { @@ -291,19 +279,19 @@ class Diaspora $data = str_replace([" ", "\t", "\r", "\n"], ['', '', '', ''], $base->data); // Build the signed data - $type = $base->data[0]->attributes()->type[0]; - $encoding = $base->encoding; - $alg = $base->alg; + $type = $base->data[0]->attributes()->type[0]; + $encoding = $base->encoding; + $alg = $base->alg; $signed_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); // This is the signature $signature = Strings::base64UrlDecode($base->sig); // Get the senders' public key - $key_id = $base->sig[0]->attributes()->key_id[0]; + $key_id = $base->sig[0]->attributes()->key_id[0]; $author_addr = base64_decode($key_id); if ($author_addr == '') { - Logger::notice('No author could be decoded. Discarding. Message: ' . $xml); + DI::logger()->notice('No author could be decoded. Discarding. Message: ' . $xml); if ($no_exit) { return false; } else { @@ -313,12 +301,12 @@ class Diaspora try { $author = WebFingerUri::fromString($author_addr); - $key = self::key($author); + $key = self::key($author); if ($key == '') { throw new \InvalidArgumentException(); } } catch (\InvalidArgumentException $e) { - Logger::notice("Couldn't get a key for handle " . $author_addr . ". Discarding."); + DI::logger()->notice("Couldn't get a key for handle " . $author_addr . ". Discarding."); if ($no_exit) { return false; } else { @@ -328,7 +316,7 @@ class Diaspora $verify = Crypto::rsaVerify($signed_data, $signature, $key); if (!$verify) { - Logger::notice('Message did not verify. Discarding.'); + DI::logger()->notice('Message did not verify. Discarding.'); if ($no_exit) { return false; } else { @@ -349,7 +337,7 @@ class Diaspora * @param string $xml urldecoded Diaspora salmon * @param string $privKey The private key of the importer * - * @return array + * @return array|false array with decoded data or false on error * 'message' -> decoded Diaspora XML message * 'author' -> author diaspora handle * 'key' -> author public key (converted to pkcs#8) @@ -358,54 +346,54 @@ class Diaspora */ public static function decode(string $xml, string $privKey = '') { - $public = false; + $public = false; $basedom = XML::parseString($xml); if (!is_object($basedom)) { - Logger::notice('XML is not parseable.'); + DI::logger()->notice('XML is not parseable.'); return false; } $children = $basedom->children('https://joindiaspora.com/protocol'); $inner_aes_key = null; - $inner_iv = null; + $inner_iv = null; if ($children->header) { $public = true; - $idom = $children->header; + $idom = $children->header; } else { // This happens with posts from a relais if (empty($privKey)) { - Logger::info('This is no private post in the old format'); + DI::logger()->info('This is no private post in the old format'); return false; } $encrypted_header = json_decode(base64_decode($children->encrypted_header)); $encrypted_aes_key_bundle = base64_decode($encrypted_header->aes_key); - $ciphertext = base64_decode($encrypted_header->ciphertext); + $ciphertext = base64_decode($encrypted_header->ciphertext); $outer_key_bundle = ''; openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); $j_outer_key_bundle = json_decode($outer_key_bundle); - $outer_iv = base64_decode($j_outer_key_bundle->iv); + $outer_iv = base64_decode($j_outer_key_bundle->iv); $outer_key = base64_decode($j_outer_key_bundle->key); $decrypted = self::aesDecrypt($outer_key, $outer_iv, $ciphertext); - Logger::info('decrypted', ['data' => $decrypted]); + DI::logger()->info('decrypted', ['data' => $decrypted]); $idom = XML::parseString($decrypted); - $inner_iv = base64_decode($idom->iv); + $inner_iv = base64_decode($idom->iv); $inner_aes_key = base64_decode($idom->aes_key); } try { $author = WebFingerUri::fromString($idom->author_id); } catch (\Throwable $e) { - Logger::notice('Could not retrieve author URI.', ['idom' => $idom]); + DI::logger()->notice('Could not retrieve author URI.', ['idom' => $idom]); throw new \Friendica\Network\HTTPException\BadRequestException(); } @@ -423,7 +411,7 @@ class Diaspora } if (!$base) { - Logger::notice('unable to locate salmon data in xml'); + DI::logger()->notice('unable to locate salmon data in xml'); throw new HTTPException\BadRequestException(); } @@ -439,10 +427,10 @@ class Diaspora // stash away some other stuff for later - $type = $base->data[0]->attributes()->type[0]; - $keyhash = $base->sig[0]->attributes()->keyhash[0]; + $type = $base->data[0]->attributes()->type[0]; + $keyhash = $base->sig[0]->attributes()->keyhash[0]; $encoding = $base->encoding; - $alg = $base->alg; + $alg = $base->alg; $signed_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); @@ -460,21 +448,21 @@ class Diaspora // Once we have the author URI, go to the web and try to find their public key // (first this will look it up locally if it is in the diaspora-contact cache) // This will also convert diaspora public key from pkcs#1 to pkcs#8 - Logger::info('Fetching key for ' . $author); + DI::logger()->info('Fetching key for ' . $author); $key = self::key($author); if (!$key) { - Logger::notice('Could not retrieve author key.'); + DI::logger()->notice('Could not retrieve author key.'); throw new HTTPException\BadRequestException(); } $verify = Crypto::rsaVerify($signed_data, $signature, $key); if (!$verify) { - Logger::notice('Message did not verify. Discarding.'); + DI::logger()->notice('Message did not verify. Discarding.'); throw new HTTPException\BadRequestException(); } - Logger::info('Message verified.'); + DI::logger()->info('Message verified.'); return [ 'message' => $inner_decrypted, @@ -497,17 +485,17 @@ class Diaspora public static function dispatchPublic(array $msg, int $direction) { if (!DI::config()->get('system', 'diaspora_enabled')) { - Logger::notice('Diaspora is disabled'); + DI::logger()->notice('Diaspora is disabled'); return false; } if (!($fields = self::validPosting($msg))) { - Logger::notice('Invalid posting', ['msg' => $msg]); + DI::logger()->notice('Invalid posting', ['msg' => $msg]); return false; } $importer = [ - 'uid' => 0, + 'uid' => 0, 'page-flags' => User::PAGE_FLAGS_FREELOVE ]; $success = self::dispatch($importer, $msg, $fields, $direction); @@ -537,7 +525,7 @@ class Diaspora if (is_null($fields)) { $private = true; if (!($fields = self::validPosting($msg))) { - Logger::notice('Invalid posting', ['msg' => $msg]); + DI::logger()->notice('Invalid posting', ['msg' => $msg]); return false; } } else { @@ -546,12 +534,12 @@ class Diaspora $type = $fields->getName(); - Logger::info('Received message', ['type' => $type, 'sender' => $sender->getAddr(), 'user' => $importer['uid']]); + DI::logger()->info('Received message', ['type' => $type, 'sender' => $sender->getAddr(), 'user' => $importer['uid']]); switch ($type) { case 'account_migration': if (!$private) { - Logger::notice('Message with type ' . $type . ' is not private, quitting.'); + DI::logger()->notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveAccountMigration($importer, $fields); @@ -564,14 +552,14 @@ class Diaspora case 'contact': if (!$private) { - Logger::notice('Message with type ' . $type . ' is not private, quitting.'); + DI::logger()->notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveContactRequest($importer, $fields); case 'conversation': if (!$private) { - Logger::notice('Message with type ' . $type . ' is not private, quitting.'); + DI::logger()->notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveConversation($importer, $msg, $fields); @@ -581,14 +569,14 @@ class Diaspora case 'message': if (!$private) { - Logger::notice('Message with type ' . $type . ' is not private, quitting.'); + DI::logger()->notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveMessage($importer, $fields); case 'participation': if (!$private) { - Logger::notice('Message with type ' . $type . ' is not private, quitting.'); + DI::logger()->notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveParticipation($importer, $fields, $direction); @@ -601,7 +589,7 @@ class Diaspora case 'profile': if (!$private) { - Logger::notice('Message with type ' . $type . ' is not private, quitting.'); + DI::logger()->notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveProfile($importer, $fields); @@ -616,7 +604,7 @@ class Diaspora return self::receiveStatusMessage($importer, $fields, $msg['message'], $direction); default: - Logger::notice('Unknown message type ' . $type); + DI::logger()->notice('Unknown message type ' . $type); return false; } } @@ -635,33 +623,33 @@ class Diaspora */ private static function validPosting(array $msg) { - $data = XML::parseString($msg['message']); + $element = XML::parseString($msg['message']); - if (!is_object($data)) { - Logger::info('No valid XML', ['message' => $msg['message']]); + if (!is_object($element)) { + DI::logger()->info('No valid XML', ['message' => $msg['message']]); return false; } + $oldXML = false; + // Is this the new or the old version? - if ($data->getName() == 'XML') { + if ($element->getName() === 'XML') { $oldXML = true; - foreach ($data->post->children() as $child) { + foreach ($element->post->children() as $child) { $element = $child; } - } else { - $oldXML = false; - $element = $data; } - $type = $element->getName(); + $type = $element->getName(); $orig_type = $type; - Logger::debug('Got message', ['type' => $type, 'message' => $msg['message']]); + DI::logger()->debug('Got message', ['type' => $type, 'message' => $msg['message']]); // All retractions are handled identically from now on. // In the new version there will only be "retraction". - if (in_array($type, ['signed_retraction', 'relayable_retraction'])) + if (in_array($type, ['signed_retraction', 'relayable_retraction'])) { $type = 'retraction'; + } if ($type == 'request') { $type = 'contact'; @@ -669,8 +657,8 @@ class Diaspora $fields = new SimpleXMLElement('<' . $type . '/>'); - $signed_data = ''; - $author_signature = null; + $signed_data = ''; + $author_signature = null; $parent_author_signature = null; foreach ($element->children() as $fieldname => $entry) { @@ -733,7 +721,7 @@ class Diaspora // This is something that shouldn't happen at all. if (in_array($type, ['status_message', 'reshare', 'profile'])) { if ($msg['author'] != $fields->author) { - Logger::notice('Message handle is not the same as envelope sender. Quitting this message.', ['author1' => $msg['author'], 'author2' => $fields->author]); + DI::logger()->notice('Message handle is not the same as envelope sender. Quitting this message.', ['author1' => $msg['author'], 'author2' => $fields->author]); return false; } } @@ -744,25 +732,25 @@ class Diaspora } if (!isset($author_signature) && ($msg['author'] == $fields->author)) { - Logger::debug('No author signature, but the sender matches the author', ['type' => $type, 'msg-author' => $msg['author'], 'message' => $msg['message']]); + DI::logger()->debug('No author signature, but the sender matches the author', ['type' => $type, 'msg-author' => $msg['author'], 'message' => $msg['message']]); return $fields; } // No author_signature? This is a must, so we quit. if (!isset($author_signature)) { - Logger::info('No author signature', ['type' => $type, 'msg-author' => $msg['author'], 'fields-author' => $fields->author, 'message' => $msg['message']]); + DI::logger()->info('No author signature', ['type' => $type, 'msg-author' => $msg['author'], 'fields-author' => $fields->author, 'message' => $msg['message']]); return false; } if (isset($parent_author_signature)) { $key = self::key(WebFingerUri::fromString($msg['author'])); if (empty($key)) { - Logger::info('No key found for parent', ['author' => $msg['author']]); + DI::logger()->info('No key found for parent', ['author' => $msg['author']]); return false; } if (!Crypto::rsaVerify($signed_data, $parent_author_signature, $key, 'sha256')) { - Logger::info('No valid parent author signature', ['author' => $msg['author'], 'type' => $type, 'signed data' => $signed_data, 'message' => $msg['message'], 'signature' => $parent_author_signature]); + DI::logger()->info('No valid parent author signature', ['author' => $msg['author'], 'type' => $type, 'signed data' => $signed_data, 'message' => $msg['message'], 'signature' => $parent_author_signature]); return false; } } @@ -773,12 +761,12 @@ class Diaspora throw new \InvalidArgumentException(); } } catch (\Throwable $e) { - Logger::info('No key found', ['author' => $fields->author]); + DI::logger()->info('No key found', ['author' => $fields->author]); return false; } if (!Crypto::rsaVerify($signed_data, $author_signature, $key, 'sha256')) { - Logger::info('No valid author signature for author', ['author' => $fields->author, 'type' => $type, 'signed data' => $signed_data, 'message' => $msg['message'], 'signature' => $author_signature]); + DI::logger()->info('No valid author signature for author', ['author' => $fields->author, 'type' => $type, 'signed data' => $signed_data, 'message' => $msg['message'], 'signature' => $author_signature]); return false; } else { return $fields; @@ -791,15 +779,15 @@ class Diaspora * @param WebFingerUri $uri The handle * * @return string The public key - * @throws InternalServerErrorException + * @throws NotFoundException * @throws \ImagickException */ private static function key(WebFingerUri $uri): string { - Logger::info('Fetching diaspora key', ['handle' => $uri->getAddr()]); + DI::logger()->info('Fetching diaspora key', ['handle' => $uri->getAddr()]); try { return DI::dsprContact()->getByAddr($uri)->pubKey; - } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + } catch (NotFoundException | \InvalidArgumentException $e) { return ''; } } @@ -826,7 +814,7 @@ class Diaspora * @param string $url profile url or WebFinger address * @param boolean|null $update true = always update, false = never update, null = update when not found or outdated * @return boolean - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException * @throws \ImagickException */ public static function isSupportedByContactUrl(string $url, ?bool $update = null): bool @@ -867,7 +855,7 @@ class Diaspora // ); // // $contact["rel"] = Contact::FRIEND; - // Logger::notice("defining user ".$contact["nick"]." as friend"); + // DI::logger()->notice("defining user ".$contact["nick"]." as friend"); //} // Contact server is blocked @@ -905,7 +893,7 @@ class Diaspora { $contact = self::contactByHandle($importer['uid'], $contact_uri); if (!$contact) { - Logger::notice('A Contact for handle ' . $contact_uri . ' and user ' . $importer['uid'] . ' was not found'); + DI::logger()->notice('A Contact for handle ' . $contact_uri . ' and user ' . $importer['uid'] . ' was not found'); // If a contact isn't found, we accept it anyway if it is a comment if ($is_comment && ($importer['uid'] != 0)) { return self::contactByHandle(0, $contact_uri); @@ -917,7 +905,7 @@ class Diaspora } if (!self::postAllow($importer, $contact, $is_comment)) { - Logger::notice('The handle: ' . $contact_uri . ' is not allowed to post to user ' . $importer['uid']); + DI::logger()->notice('The handle: ' . $contact_uri . ' is not allowed to post to user ' . $importer['uid']); return false; } return $contact; @@ -936,7 +924,7 @@ class Diaspora { $item = Post::selectFirst(['id'], ['uid' => $uid, 'guid' => $guid]); if (DBA::isResult($item)) { - Logger::notice('Message already exists.', ['uid' => $uid, 'guid' => $guid, 'id' => $item['id']]); + DI::logger()->notice('Message already exists.', ['uid' => $uid, 'guid' => $guid, 'id' => $item['id']]); return $item['id']; } @@ -994,7 +982,7 @@ class Diaspora } else { // No local match, restoring absolute remote URL from author scheme and host $author_url = parse_url($author_link); - $return = '[url=' . $author_url['scheme'] . '://' . $author_url['host'] . '/people/' . $match[1] . ']' . $match[2] . '[/url]'; + $return = '[url=' . $author_url['scheme'] . '://' . $author_url['host'] . '/people/' . $match[1] . ']' . $match[2] . '[/url]'; } return $return; @@ -1042,7 +1030,7 @@ class Diaspora $server = $serverparts['scheme'] . '://' . $serverparts['host']; - Logger::info('Trying to fetch item ' . $guid . ' from ' . $server); + DI::logger()->info('Trying to fetch item ' . $guid . ' from ' . $server); $msg = self::message($guid, $server); @@ -1050,7 +1038,7 @@ class Diaspora return false; } - Logger::info('Successfully fetched item ' . $guid . ' from ' . $server); + DI::logger()->info('Successfully fetched item ' . $guid . ' from ' . $server); // Now call the dispatcher return self::dispatchPublic($msg, $force ? self::FORCED_FETCH : self::FETCHED); @@ -1063,7 +1051,7 @@ class Diaspora * @param string $server The url of the server * @param int $level Endless loop prevention * - * @return array + * @return array|false The message as array or false on error * 'message' => The message XML * 'author' => The author handle * 'key' => The public key of the author @@ -1078,16 +1066,16 @@ class Diaspora // This will work for new Diaspora servers and Friendica servers from 3.5 $source_url = $server . '/fetch/post/' . urlencode($guid); - Logger::info('Fetch post from ' . $source_url); + DI::logger()->info('Fetch post from ' . $source_url); - $envelope = DI::httpClient()->fetch($source_url, HttpClientAccept::MAGIC); + $envelope = DI::httpClient()->fetch($source_url, HttpClientAccept::MAGIC, 0, '', HttpClientRequest::DIASPORA); if ($envelope) { - Logger::info('Envelope was fetched.'); + DI::logger()->info('Envelope was fetched.'); $x = self::verifyMagicEnvelope($envelope); if (!$x) { - Logger::info('Envelope could not be verified.'); + DI::logger()->info('Envelope could not be verified.'); } else { - Logger::info('Envelope was verified.'); + DI::logger()->info('Envelope was verified.'); } } else { $x = false; @@ -1105,11 +1093,11 @@ class Diaspora if ($source_xml->post->reshare) { // Reshare of a reshare - old Diaspora version - Logger::info('Message is a reshare'); + DI::logger()->info('Message is a reshare'); return self::message($source_xml->post->reshare->root_guid, $server, ++$level); } elseif ($source_xml->getName() == 'reshare') { // Reshare of a reshare - new Diaspora version - Logger::info('Message is a new reshare'); + DI::logger()->info('Message is a new reshare'); return self::message($source_xml->root_guid, $server, ++$level); } @@ -1126,7 +1114,7 @@ class Diaspora $author = WebFingerUri::fromString($author_handle); } catch (\InvalidArgumentException $e) { // If this isn't a "status_message" then quit - Logger::info("Message doesn't seem to be a status message"); + DI::logger()->info("Message doesn't seem to be a status message"); return false; } @@ -1151,7 +1139,7 @@ class Diaspora { // Check for Diaspora (and Friendica) typical paths if (!preg_match('=(https?://.+)/(?:posts|display|objects)/([a-zA-Z0-9-_@.:%]+[a-zA-Z0-9])=i', $url, $matches)) { - Logger::notice('Invalid url', ['url' => $url]); + DI::logger()->notice('Invalid url', ['url' => $url]); return false; } @@ -1159,20 +1147,20 @@ class Diaspora $item = Post::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]); if (DBA::isResult($item)) { - Logger::info('Found', ['id' => $item['id']]); + DI::logger()->info('Found', ['id' => $item['id']]); return $item['id']; } - Logger::info('Fetch GUID from origin', ['guid' => $guid, 'server' => $matches[1]]); + DI::logger()->info('Fetch GUID from origin', ['guid' => $guid, 'server' => $matches[1]]); $ret = self::storeByGuid($guid, $matches[1], true); - Logger::info('Result', ['ret' => $ret]); + DI::logger()->info('Result', ['ret' => $ret]); $item = Post::selectFirst(['id'], ['guid' => $guid, 'uid' => $uid]); if (DBA::isResult($item)) { - Logger::info('Found', ['id' => $item['id']]); + DI::logger()->info('Found', ['id' => $item['id']]); return $item['id']; } else { - Logger::notice('Not found', ['guid' => $guid, 'uid' => $uid]); + DI::logger()->notice('Not found', ['guid' => $guid, 'uid' => $uid]); return false; } } @@ -1198,7 +1186,7 @@ class Diaspora ]; $condition = ['uid' => $uid, 'guid' => $guid]; - $item = Post::selectFirst($fields, $condition); + $item = Post::selectFirst($fields, $condition); if (!DBA::isResult($item)) { try { @@ -1210,20 +1198,20 @@ class Diaspora } if ($result) { - Logger::info('Fetched missing item ' . $guid . ' - result: ' . $result); + DI::logger()->info('Fetched missing item ' . $guid . ' - result: ' . $result); $item = Post::selectFirst($fields, $condition); } } catch (HTTPException\NotFoundException $e) { - Logger::notice('Unable to retrieve author details', ['author' => $author->getAddr()]); + DI::logger()->notice('Unable to retrieve author details', ['author' => $author->getAddr()]); } } if (!DBA::isResult($item)) { - Logger::notice('Parent item not found: parent: ' . $guid . ' - user: ' . $uid); + DI::logger()->notice('Parent item not found: parent: ' . $guid . ' - user: ' . $uid); return false; } else { - Logger::info('Parent item found: parent: ' . $guid . ' - user: ' . $uid); + DI::logger()->info('Parent item found: parent: ' . $guid . ' - user: ' . $uid); return $item; } } @@ -1243,17 +1231,17 @@ class Diaspora private static function authorContactByUrl(array $def_contact, string $contact_url, int $uid): array { $condition = ['nurl' => Strings::normaliseLink($contact_url), 'uid' => $uid]; - $contact = DBA::selectFirst('contact', ['id', 'network'], $condition); + $contact = DBA::selectFirst('contact', ['id', 'network'], $condition); if (DBA::isResult($contact)) { - $cid = $contact['id']; + $cid = $contact['id']; $network = $contact['network']; } else { - $cid = $def_contact['id']; + $cid = $def_contact['id']; $network = Protocol::DIASPORA; } return [ - 'cid' => $cid, + 'cid' => $cid, 'network' => $network ]; } @@ -1285,28 +1273,28 @@ class Diaspora { $contact = Contact::getByURL($addr); if (empty($contact)) { - Logger::info('No contact data for address', ['addr' => $addr]); + DI::logger()->info('No contact data for address', ['addr' => $addr]); return ''; } if (empty($contact['baseurl'])) { $contact['baseurl'] = 'https://' . substr($addr, strpos($addr, '@') + 1); - Logger::info('Create baseurl from address', ['baseurl' => $contact['baseurl'], 'url' => $contact['url']]); + DI::logger()->info('Create baseurl from address', ['baseurl' => $contact['baseurl'], 'url' => $contact['url']]); } $platform = ''; - $gserver = DBA::selectFirst('gserver', ['platform'], ['nurl' => Strings::normaliseLink($contact['baseurl'])]); + $gserver = DBA::selectFirst('gserver', ['platform'], ['nurl' => Strings::normaliseLink($contact['baseurl'])]); if (!empty($gserver['platform'])) { $platform = strtolower($gserver['platform']); - Logger::info('Detected platform', ['platform' => $platform, 'url' => $contact['url']]); + DI::logger()->info('Detected platform', ['platform' => $platform, 'url' => $contact['url']]); } if (!in_array($platform, ['diaspora', 'friendica', 'hubzilla', 'socialhome'])) { if (self::isHubzilla($contact['url'])) { - Logger::info('Detected unknown platform as Hubzilla', ['platform' => $platform, 'url' => $contact['url']]); + DI::logger()->info('Detected unknown platform as Hubzilla', ['platform' => $platform, 'url' => $contact['url']]); $platform = 'hubzilla'; } elseif ($contact['network'] == Protocol::DFRN) { - Logger::info('Detected unknown platform as Friendica', ['platform' => $platform, 'url' => $contact['url']]); + DI::logger()->info('Detected unknown platform as Friendica', ['platform' => $platform, 'url' => $contact['url']]); $platform = 'friendica'; } } @@ -1324,7 +1312,7 @@ class Diaspora } if ($platform != 'diaspora') { - Logger::info('Unknown platform', ['platform' => $platform, 'url' => $contact['url']]); + DI::logger()->info('Unknown platform', ['platform' => $platform, 'url' => $contact['url']]); return ''; } @@ -1351,7 +1339,7 @@ class Diaspora $old_author = WebFingerUri::fromString(XML::unescape($data->author)); $new_author = WebFingerUri::fromString(XML::unescape($data->profile->author)); } catch (\Throwable $e) { - Logger::notice('Cannot find handles for sender and user', ['data' => $data]); + DI::logger()->notice('Cannot find handles for sender and user', ['data' => $data]); return false; } @@ -1359,17 +1347,17 @@ class Diaspora $contact = self::contactByHandle($importer['uid'], $old_author); if (!$contact) { - Logger::notice('Cannot find contact for sender: ' . $old_author . ' and user ' . $importer['uid']); + DI::logger()->notice('Cannot find contact for sender: ' . $old_author . ' and user ' . $importer['uid']); return false; } - Logger::info('Got migration for ' . $old_author . ', to ' . $new_author . ' with user ' . $importer['uid']); + DI::logger()->info('Got migration for ' . $old_author . ', to ' . $new_author . ' with user ' . $importer['uid']); // Check signature $signed_text = 'AccountMigration:' . $old_author . ':' . $new_author; - $key = self::key($old_author); + $key = self::key($old_author); if (!Crypto::rsaVerify($signed_text, $signature, $key, 'sha256')) { - Logger::notice('No valid signature for migration.'); + DI::logger()->notice('No valid signature for migration.'); return false; } @@ -1379,7 +1367,7 @@ class Diaspora // change the technical stuff in contact $data = Probe::uri($new_author); if ($data['network'] == Protocol::PHANTOM) { - Logger::notice("Account for " . $new_author . " couldn't be probed."); + DI::logger()->notice("Account for " . $new_author . " couldn't be probed."); return false; } @@ -1397,7 +1385,7 @@ class Diaspora Contact::update($fields, ['addr' => $old_author->getAddr()]); - Logger::info('Contacts are updated.'); + DI::logger()->info('Contacts are updated.'); return true; } @@ -1420,7 +1408,7 @@ class Diaspora } DBA::close($contacts); - Logger::info('Removed contacts for ' . $author_handle); + DI::logger()->info('Removed contacts for ' . $author_handle); return true; } @@ -1472,7 +1460,7 @@ class Diaspora */ foreach ($matches as $match) { - if (empty($match)) { + if ($match === '') { continue; } @@ -1499,10 +1487,10 @@ class Diaspora */ private static function receiveComment(array $importer, WebFingerUri $sender, SimpleXMLElement $data, string $xml, int $direction): bool { - $author = WebFingerUri::fromString(XML::unescape($data->author)); - $guid = XML::unescape($data->guid); + $author = WebFingerUri::fromString(XML::unescape($data->author)); + $guid = XML::unescape($data->guid); $parent_guid = XML::unescape($data->parent_guid); - $text = XML::unescape($data->text); + $text = XML::unescape($data->text); if (isset($data->created_at)) { $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); @@ -1512,7 +1500,7 @@ class Diaspora if (isset($data->thread_parent_guid)) { $thread_parent_guid = XML::unescape($data->thread_parent_guid); - $thr_parent = self::getUriFromGuid($thread_parent_guid); + $thr_parent = self::getUriFromGuid($thread_parent_guid); } else { $thr_parent = ''; } @@ -1539,7 +1527,7 @@ class Diaspora try { $author_url = (string)DI::dsprContact()->getByAddr($author)->url; } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { - Logger::notice('Unable to find author details', ['author' => $author->getAddr()]); + DI::logger()->notice('Unable to find author details', ['author' => $author->getAddr()]); return false; } @@ -1548,24 +1536,24 @@ class Diaspora $datarray = []; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $author_contact['cid']; - $datarray['network'] = $author_contact['network']; + $datarray['network'] = $author_contact['network']; $datarray['author-link'] = $author_url; - $datarray['author-id'] = Contact::getIdForURL($author_url); + $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['owner-link'] = $contact['url']; - $datarray['owner-id'] = Contact::getIdForURL($contact['url']); + $datarray['owner-id'] = Contact::getIdForURL($contact['url']); // Will be overwritten for sharing accounts in Item::insert $datarray = self::setDirection($datarray, $direction); - $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($guid, $author); + $datarray['guid'] = $guid; + $datarray['uri'] = self::getUriFromGuid($guid, $author); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); - $datarray['verb'] = Activity::POST; + $datarray['verb'] = Activity::POST; $datarray['gravity'] = Item::GRAVITY_COMMENT; $datarray['private'] = $toplevel_parent_item['private']; @@ -1577,17 +1565,17 @@ class Diaspora $datarray['thr-parent'] = $thr_parent ?: $toplevel_parent_item['uri']; $datarray['object-type'] = Activity\ObjectType::COMMENT; - $datarray['post-type'] = Item::PT_NOTE; + $datarray['post-type'] = Item::PT_NOTE; $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['source'] = $xml; + $datarray['source'] = $xml; $datarray = self::setDirection($datarray, $direction); $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $created_at; $datarray['plink'] = self::plink($author, $guid, $toplevel_parent_item['guid']); - $body = Markdown::toBBCode($text); + $body = Markdown::toBBCode($text); $datarray['body'] = self::replacePeopleGuid($body, $author_url); @@ -1602,8 +1590,8 @@ class Diaspora $datarray['diaspora_signed_text'] = json_encode($data); } - if (Item::isTooOld($datarray)) { - Logger::info('Comment is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); + if (DI::contentItem()->isTooOld($datarray['created'], $datarray['uid'])) { + DI::logger()->info('Comment is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); return false; } @@ -1614,7 +1602,7 @@ class Diaspora } if ($message_id) { - Logger::info('Stored comment ' . $datarray['guid'] . ' with message id ' . $message_id); + DI::logger()->info('Stored comment ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id, json_encode($data)); } @@ -1640,8 +1628,8 @@ class Diaspora private static function receiveConversationMessage(array $importer, array $contact, SimpleXMLElement $data, array $msg, $mesg, array $conversation): bool { $author_handle = XML::unescape($data->author); - $guid = XML::unescape($data->guid); - $subject = XML::unescape($data->subject); + $guid = XML::unescape($data->guid); + $subject = XML::unescape($data->subject); // "diaspora_handle" is the element name from the old version // "author" is the element name from the new version @@ -1659,13 +1647,13 @@ class Diaspora return false; } - $msg_guid = XML::unescape($mesg->guid); + $msg_guid = XML::unescape($mesg->guid); $msg_conversation_guid = XML::unescape($mesg->conversation_guid); - $msg_text = XML::unescape($mesg->text); - $msg_created_at = DateTimeFormat::utc(XML::unescape($mesg->created_at)); + $msg_text = XML::unescape($mesg->text); + $msg_created_at = DateTimeFormat::utc(XML::unescape($mesg->created_at)); if ($msg_conversation_guid != $guid) { - Logger::notice('Message conversation guid does not belong to the current conversation.', ['guid' => $guid]); + DI::logger()->notice('Message conversation guid does not belong to the current conversation.', ['guid' => $guid]); return false; } @@ -1700,15 +1688,15 @@ class Diaspora private static function receiveConversation(array $importer, array $msg, SimpleXMLElement $data) { $author_handle = XML::unescape($data->author); - $guid = XML::unescape($data->guid); - $subject = XML::unescape($data->subject); - $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); - $participants = XML::unescape($data->participants); + $guid = XML::unescape($data->guid); + $subject = XML::unescape($data->subject); + $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); + $participants = XML::unescape($data->participants); $messages = $data->message; if (!count($messages)) { - Logger::notice('Empty conversation'); + DI::logger()->notice('Empty conversation'); return false; } @@ -1738,7 +1726,7 @@ class Diaspora } } if (!$conversation) { - Logger::warning('Unable to create conversation.'); + DI::logger()->warning('Unable to create conversation.'); return false; } @@ -1763,11 +1751,11 @@ class Diaspora */ private static function receiveLike(array $importer, WebFingerUri $sender, SimpleXMLElement $data, int $direction): bool { - $author = WebFingerUri::fromString(XML::unescape($data->author)); - $guid = XML::unescape($data->guid); + $author = WebFingerUri::fromString(XML::unescape($data->author)); + $guid = XML::unescape($data->guid); $parent_guid = XML::unescape($data->parent_guid); $parent_type = XML::unescape($data->parent_type); - $positive = XML::unescape($data->positive); + $positive = XML::unescape($data->positive); // likes on comments aren't supported by Diaspora - only on posts // But maybe this will be supported in the future, so we will accept it. @@ -1797,7 +1785,7 @@ class Diaspora try { $author_url = (string)DI::dsprContact()->getByAddr($author)->url; } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { - Logger::notice('Unable to find author details', ['author' => $author->getAddr()]); + DI::logger()->notice('Unable to find author details', ['author' => $author->getAddr()]); return false; } @@ -1816,19 +1804,19 @@ class Diaspora $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $author_contact['cid']; - $datarray['network'] = $author_contact['network']; + $datarray['network'] = $author_contact['network']; $datarray = self::setDirection($datarray, $direction); $datarray['owner-link'] = $datarray['author-link'] = $author_url; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($guid, $author); + $datarray['uri'] = self::getUriFromGuid($guid, $author); - $datarray['verb'] = $verb; + $datarray['verb'] = $verb; $datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['private'] = $toplevel_parent_item['private']; @@ -1849,7 +1837,7 @@ class Diaspora // like on comments have the comment as parent. So we need to fetch the toplevel parent if ($toplevel_parent_item['gravity'] != Item::GRAVITY_PARENT) { $toplevel = Post::selectFirst(['origin'], ['id' => $toplevel_parent_item['parent']]); - $origin = $toplevel['origin']; + $origin = $toplevel['origin']; } else { $origin = $toplevel_parent_item['origin']; } @@ -1860,8 +1848,8 @@ class Diaspora $datarray['diaspora_signed_text'] = json_encode($data); } - if (Item::isTooOld($datarray)) { - Logger::info('Like is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); + if (DI::contentItem()->isTooOld($datarray['created'], $datarray['uid'])) { + DI::logger()->info('Like is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); return false; } @@ -1872,7 +1860,7 @@ class Diaspora } if ($message_id) { - Logger::info('Stored like ' . $datarray['guid'] . ' with message id ' . $message_id); + DI::logger()->info('Stored like ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id, json_encode($data)); } @@ -1892,11 +1880,11 @@ class Diaspora */ private static function receiveMessage(array $importer, SimpleXMLElement $data): bool { - $author_uri = WebFingerUri::fromString(XML::unescape($data->author)); - $guid = XML::unescape($data->guid); + $author_uri = WebFingerUri::fromString(XML::unescape($data->author)); + $guid = XML::unescape($data->guid); $conversation_guid = XML::unescape($data->conversation_guid); - $text = XML::unescape($data->text); - $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); + $text = XML::unescape($data->text); + $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); $contact = self::allowedContactByHandle($importer, $author_uri, true); if (!$contact) { @@ -1907,17 +1895,17 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $condition = ['uid' => $importer['uid'], 'guid' => $conversation_guid]; + $condition = ['uid' => $importer['uid'], 'guid' => $conversation_guid]; $conversation = DBA::selectFirst('conv', [], $condition); if (!DBA::isResult($conversation)) { - Logger::notice('Conversation not available.'); + DI::logger()->notice('Conversation not available.'); return false; } try { $author = DI::dsprContact()->getByAddr($author_uri); } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { - Logger::notice('Unable to find author details', ['author' => $author_uri->getAddr()]); + DI::logger()->notice('Unable to find author details', ['author' => $author_uri->getAddr()]); return false; } @@ -1955,8 +1943,8 @@ class Diaspora */ private static function receiveParticipation(array $importer, SimpleXMLElement $data, int $direction): bool { - $author = WebFingerUri::fromString(strtolower(XML::unescape($data->author))); - $guid = XML::unescape($data->guid); + $author = WebFingerUri::fromString(strtolower(XML::unescape($data->author))); + $guid = XML::unescape($data->guid); $parent_guid = XML::unescape($data->parent_guid); $contact = self::allowedContactByHandle($importer, $author, true); @@ -1978,18 +1966,18 @@ class Diaspora } if (!$toplevel_parent_item['origin']) { - Logger::info('Not our origin. Participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); + DI::logger()->info('Not our origin. Participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); } if (!in_array($toplevel_parent_item['private'], [Item::PUBLIC, Item::UNLISTED])) { - Logger::info('Item is not public, participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); + DI::logger()->info('Item is not public, participation is ignored', ['parent_guid' => $parent_guid, 'guid' => $guid, 'author' => $author]); return false; } try { $author_url = (string)DI::dsprContact()->getByAddr($author)->url; } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { - Logger::notice('unable to find author details', ['author' => $author->getAddr()]); + DI::logger()->notice('unable to find author details', ['author' => $author->getAddr()]); return false; } @@ -2000,20 +1988,20 @@ class Diaspora $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $author_contact['cid']; - $datarray['network'] = $author_contact['network']; + $datarray['network'] = $author_contact['network']; $datarray = self::setDirection($datarray, $direction); $datarray['owner-link'] = $datarray['author-link'] = $author_url; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($guid, $author); + $datarray['uri'] = self::getUriFromGuid($guid, $author); - $datarray['verb'] = Activity::FOLLOW; - $datarray['gravity'] = Item::GRAVITY_ACTIVITY; + $datarray['verb'] = Activity::FOLLOW; + $datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['thr-parent'] = $toplevel_parent_item['uri']; $datarray['object-type'] = Activity\ObjectType::NOTE; @@ -2023,14 +2011,14 @@ class Diaspora // Diaspora doesn't provide a date for a participation $datarray['changed'] = $datarray['created'] = $datarray['edited'] = DateTimeFormat::utcNow(); - if (Item::isTooOld($datarray)) { - Logger::info('Participation is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); + if (DI::contentItem()->isTooOld($datarray['created'], $datarray['uid'])) { + DI::logger()->info('Participation is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); return false; } $message_id = Item::insert($datarray); - Logger::info('Participation stored', ['id' => $message_id, 'guid' => $guid, 'parent_guid' => $parent_guid, 'author' => $author]); + DI::logger()->info('Participation stored', ['id' => $message_id, 'guid' => $guid, 'parent_guid' => $parent_guid, 'author' => $author]); // Send all existing comments and likes to the requesting server $comments = Post::select( @@ -2039,21 +2027,21 @@ class Diaspora ); while ($comment = Post::fetch($comments)) { if (($comment['gravity'] == Item::GRAVITY_ACTIVITY) && !in_array($comment['verb'], [Activity::LIKE, Activity::DISLIKE])) { - Logger::info('Unsupported activities are not relayed', ['item' => $comment['id'], 'verb' => $comment['verb']]); + DI::logger()->info('Unsupported activities are not relayed', ['item' => $comment['id'], 'verb' => $comment['verb']]); continue; } if ($comment['author-network'] == Protocol::ACTIVITYPUB) { - Logger::info('Comments from ActivityPub authors are not relayed', ['item' => $comment['id']]); + DI::logger()->info('Comments from ActivityPub authors are not relayed', ['item' => $comment['id']]); continue; } if ($comment['parent-author-network'] == Protocol::ACTIVITYPUB) { - Logger::info('Comments to comments from ActivityPub authors are not relayed', ['item' => $comment['id']]); + DI::logger()->info('Comments to comments from ActivityPub authors are not relayed', ['item' => $comment['id']]); continue; } - Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $author_contact['cid']]); + DI::logger()->info('Deliver participation', ['item' => $comment['id'], 'contact' => $author_contact['cid']]); if (Worker::add(Worker::PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['uri-id'], $author_contact['cid'], $datarray['uid'])) { Post\DeliveryData::incrementQueueCount($comment['uri-id'], 1); } @@ -2111,14 +2099,14 @@ class Diaspora return false; } - $name = XML::unescape($data->first_name) . ((strlen($data->last_name)) ? ' ' . XML::unescape($data->last_name) : ''); - $image_url = XML::unescape($data->image_url); - $birthday = XML::unescape($data->birthday); - $about = Markdown::toBBCode(XML::unescape($data->bio)); - $location = Markdown::toBBCode(XML::unescape($data->location)); + $name = XML::unescape($data->first_name) . ((strlen($data->last_name)) ? ' ' . XML::unescape($data->last_name) : ''); + $image_url = XML::unescape($data->image_url); + $birthday = XML::unescape($data->birthday); + $about = Markdown::toBBCode(XML::unescape($data->bio)); + $location = Markdown::toBBCode(XML::unescape($data->location)); $searchable = (XML::unescape($data->searchable) == 'true'); - $nsfw = (XML::unescape($data->nsfw) == 'true'); - $tags = XML::unescape($data->tag_string); + $nsfw = (XML::unescape($data->nsfw) == 'true'); + $tags = XML::unescape($data->tag_string); $tags = explode('#', $tags); @@ -2159,9 +2147,9 @@ class Diaspora } $fields = [ - 'name' => $name, 'location' => $location, - 'name-date' => DateTimeFormat::utcNow(), 'about' => $about, - 'addr' => $author->getAddr(), 'nick' => $author->getUser(), 'keywords' => $keywords, + 'name' => $name, 'location' => $location, + 'name-date' => DateTimeFormat::utcNow(), 'about' => $about, + 'addr' => $author->getAddr(), 'nick' => $author->getUser(), 'keywords' => $keywords, 'unsearchable' => !$searchable, 'sensitive' => $nsfw ]; @@ -2171,7 +2159,7 @@ class Diaspora Contact::update($fields, ['id' => $contact['id']]); - Logger::info('Profile of contact ' . $contact['id'] . ' stored for user ' . $importer['uid']); + DI::logger()->info('Profile of contact ' . $contact['id'] . ' stored for user ' . $importer['uid']); return true; } @@ -2206,7 +2194,7 @@ class Diaspora private static function receiveContactRequest(array $importer, SimpleXMLElement $data): bool { $author_handle = XML::unescape($data->author); - $recipient = XML::unescape($data->recipient); + $recipient = XML::unescape($data->recipient); if (!$author_handle || !$recipient) { return false; @@ -2234,7 +2222,7 @@ class Diaspora // That makes us friends. if ($contact) { if ($following) { - Logger::info('Author ' . $author . ' (Contact ' . $contact['id'] . ') wants to follow us.'); + DI::logger()->info('Author ' . $author . ' (Contact ' . $contact['id'] . ') wants to follow us.'); self::receiveRequestMakeFriend($importer, $contact); // refetch the contact array @@ -2245,36 +2233,36 @@ class Diaspora if (in_array($contact['rel'], [Contact::FRIEND])) { $user = DBA::selectFirst('user', [], ['uid' => $importer['uid']]); if (DBA::isResult($user)) { - Logger::info('Sending share message to author ' . $author . ' - Contact: ' . $contact['id'] . ' - User: ' . $importer['uid']); + DI::logger()->info('Sending share message to author ' . $author . ' - Contact: ' . $contact['id'] . ' - User: ' . $importer['uid']); self::sendShare($user, $contact); } } return true; } else { - Logger::info("Author " . $author . " doesn't want to follow us anymore."); + DI::logger()->info("Author " . $author . " doesn't want to follow us anymore."); Contact::removeFollower($contact); return true; } } if (!$following && $sharing && in_array($importer['page-flags'], [User::PAGE_FLAGS_SOAPBOX, User::PAGE_FLAGS_NORMAL])) { - Logger::info("Author " . $author . " wants to share with us - but doesn't want to listen. Request is ignored."); + DI::logger()->info("Author " . $author . " wants to share with us - but doesn't want to listen. Request is ignored."); return false; } elseif (!$following && !$sharing) { - Logger::info("Author " . $author . " doesn't want anything - and we don't know the author. Request is ignored."); + DI::logger()->info("Author " . $author . " doesn't want anything - and we don't know the author. Request is ignored."); return false; } elseif (!$following && $sharing) { - Logger::info("Author " . $author . " wants to share with us."); + DI::logger()->info("Author " . $author . " wants to share with us."); } elseif ($following && $sharing) { - Logger::info("Author " . $author . " wants to have a bidirectional connection."); + DI::logger()->info("Author " . $author . " wants to have a bidirectional connection."); } elseif ($following && !$sharing) { - Logger::info("Author " . $author . " wants to listen to us."); + DI::logger()->info("Author " . $author . " wants to listen to us."); } try { $author_url = (string)DI::dsprContact()->getByAddr($author)->url; } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { - Logger::notice('Cannot resolve diaspora handle for recipient', ['author' => $author->getAddr(), 'recipient' => $recipient]); + DI::logger()->notice('Cannot resolve diaspora handle for recipient', ['author' => $author->getAddr(), 'recipient' => $recipient]); return false; } @@ -2294,7 +2282,7 @@ class Diaspora if ($result === true) { $contact_record = self::contactByHandle($importer['uid'], $author); if (!$contact_record) { - Logger::info('unable to locate newly created contact record.'); + DI::logger()->info('unable to locate newly created contact record.'); return false; } @@ -2324,8 +2312,8 @@ class Diaspora */ private static function receiveReshare(array $importer, SimpleXMLElement $data, string $xml, int $direction): bool { - $author = WebFingerUri::fromString(XML::unescape($data->author)); - $guid = XML::unescape($data->guid); + $author = WebFingerUri::fromString(XML::unescape($data->author)); + $guid = XML::unescape($data->guid); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); try { $root_author = WebFingerUri::fromString(XML::unescape($data->root_author)); @@ -2359,25 +2347,25 @@ class Diaspora $datarray = []; - $datarray['uid'] = $importer['uid']; + $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $contact['id']; - $datarray['network'] = Protocol::DIASPORA; + $datarray['network'] = Protocol::DIASPORA; $datarray['author-link'] = $contact['url']; - $datarray['author-id'] = Contact::getIdForURL($contact['url'], 0); + $datarray['author-id'] = Contact::getIdForURL($contact['url'], 0); $datarray['owner-link'] = $datarray['author-link']; - $datarray['owner-id'] = $datarray['author-id']; + $datarray['owner-id'] = $datarray['author-id']; - $datarray['guid'] = $guid; - $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author); + $datarray['guid'] = $guid; + $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); - $datarray['verb'] = Activity::POST; + $datarray['verb'] = Activity::POST; $datarray['gravity'] = Item::GRAVITY_PARENT; $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['source'] = $xml; + $datarray['source'] = $xml; $datarray = self::setDirection($datarray, $direction); @@ -2393,8 +2381,8 @@ class Diaspora self::fetchGuid($datarray); - if (Item::isTooOld($datarray)) { - Logger::info('Reshare is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); + if (DI::contentItem()->isTooOld($datarray['created'], $datarray['uid'])) { + DI::logger()->info('Reshare is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); return false; } @@ -2403,7 +2391,7 @@ class Diaspora self::sendParticipation($contact, $datarray); if ($message_id) { - Logger::info('Stored reshare ' . $datarray['guid'] . ' with message id ' . $message_id); + DI::logger()->info('Stored reshare ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id); } @@ -2418,14 +2406,14 @@ class Diaspora $shared_item = Post::selectFirst(['uri-id'], ['guid' => $guid, 'uid' => [$uid, 0], 'private' => [Item::PUBLIC, Item::UNLISTED]]); if (!DBA::isResult($shared_item) && !empty($host) && Diaspora::storeByGuid($guid, $host, true)) { - Logger::debug('Fetched post', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); + DI::logger()->debug('Fetched post', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); $shared_item = Post::selectFirst(['uri-id'], ['guid' => $guid, 'uid' => [$uid, 0], 'private' => [Item::PUBLIC, Item::UNLISTED]]); } elseif (DBA::isResult($shared_item)) { - Logger::debug('Found existing post', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); + DI::logger()->debug('Found existing post', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); } if (!DBA::isResult($shared_item)) { - Logger::notice('Post does not exist.', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); + DI::logger()->notice('Post does not exist.', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); return 0; } @@ -2451,7 +2439,7 @@ class Diaspora try { $author = DI::dsprContact()->getByAddr($author_uri); } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { - Logger::notice('Unable to find details for author', ['author' => $author_uri->getAddr()]); + DI::logger()->notice('Unable to find details for author', ['author' => $author_uri->getAddr()]); return false; } @@ -2469,13 +2457,13 @@ class Diaspora $r = Post::select($fields, $condition); if (!DBA::isResult($r)) { - Logger::notice('Target guid ' . $target_guid . ' was not found on this system for user ' . $importer['uid'] . '.'); + DI::logger()->notice('Target guid ' . $target_guid . ' was not found on this system for user ' . $importer['uid'] . '.'); return false; } while ($item = Post::fetch($r)) { if (DBA::exists('post-category', ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], 'type' => Post\Category::FILE])) { - Logger::info("Target guid " . $target_guid . " for user " . $item['uid'] . " is filed. So it won't be deleted."); + DI::logger()->info("Target guid " . $target_guid . " for user " . $item['uid'] . " is filed. So it won't be deleted."); continue; } @@ -2484,13 +2472,13 @@ class Diaspora // Only delete it if the parent author really fits if (!Strings::compareLink($parent['author-link'], $contact_url) && !Strings::compareLink($item['author-link'], $contact_url)) { - Logger::info("Thread author " . $parent['author-link'] . " and item author " . $item['author-link'] . " don't fit to expected contact " . $contact_url); + DI::logger()->info("Thread author " . $parent['author-link'] . " and item author " . $item['author-link'] . " don't fit to expected contact " . $contact_url); continue; } Item::markForDeletion(['id' => $item['id']]); - Logger::info('Deleted target ' . $target_guid . ' (' . $item['id'] . ') from user ' . $item['uid'] . ' parent: ' . $item['parent']); + DI::logger()->info('Deleted target ' . $target_guid . ' (' . $item['id'] . ') from user ' . $item['uid'] . ' parent: ' . $item['parent']); } DBA::close($r); @@ -2513,7 +2501,7 @@ class Diaspora $contact = self::contactByHandle($importer['uid'], $sender); if (!$contact && (in_array($target_type, ['Contact', 'Person']))) { - Logger::notice('Cannot find contact for sender: ' . $sender . ' and user ' . $importer['uid']); + DI::logger()->notice('Cannot find contact for sender: ' . $sender . ' and user ' . $importer['uid']); return false; } @@ -2521,7 +2509,7 @@ class Diaspora $contact = []; } - Logger::info('Got retraction for ' . $target_type . ', sender ' . $sender . ' and user ' . $importer['uid']); + DI::logger()->info('Got retraction for ' . $target_type . ', sender ' . $sender . ' and user ' . $importer['uid']); switch ($target_type) { case 'Comment': @@ -2537,7 +2525,7 @@ class Diaspora break; default: - Logger::notice('Unknown target type ' . $target_type); + DI::logger()->notice('Unknown target type ' . $target_type); return false; } return true; @@ -2557,18 +2545,18 @@ class Diaspora { $contact = Contact::getByURL($author); if (DBA::exists('contact', ['`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)', $contact['nurl'], 0, Contact::FRIEND, Contact::SHARING])) { - Logger::debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); + DI::logger()->debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); return true; } if ($direction == self::FORCED_FETCH) { - Logger::debug('Post is a forced fetch - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); + DI::logger()->debug('Post is a forced fetch - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); return true; } $tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name'); if (Relay::isSolicitedPost($tags, $body, $contact['id'], $item['uri'], Protocol::DIASPORA)) { - Logger::debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); + DI::logger()->debug('Post is accepted because of the relay settings', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); return true; } else { return false; @@ -2586,7 +2574,7 @@ class Diaspora private static function storePhotoAsMedia(int $uriid, $photo) { // @TODO Need to find object type, roland@f.haeder.net - Logger::debug('photo=' . get_class($photo)); + DI::logger()->debug('photo=' . get_class($photo)); $data = [ 'uri-id' => $uriid, 'type' => Post\Media::IMAGE, @@ -2636,11 +2624,11 @@ class Diaspora */ private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, string $xml, int $direction) { - $author = WebFingerUri::fromString(XML::unescape($data->author)); - $guid = XML::unescape($data->guid); - $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); - $public = XML::unescape($data->public); - $text = XML::unescape($data->text); + $author = WebFingerUri::fromString(XML::unescape($data->author)); + $guid = XML::unescape($data->guid); + $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); + $public = XML::unescape($data->public); + $text = XML::unescape($data->text); $provider_display_name = XML::unescape($data->provider_display_name); $contact = self::allowedContactByHandle($importer, $author); @@ -2700,7 +2688,7 @@ class Diaspora } $datarray['object-type'] = Activity\ObjectType::IMAGE; - $datarray['post-type'] = Item::PT_IMAGE; + $datarray['post-type'] = Item::PT_IMAGE; } elseif ($data->poll) { $datarray['post-type'] = Item::PT_POLL; } @@ -2738,8 +2726,8 @@ class Diaspora self::fetchGuid($datarray); - if (Item::isTooOld($datarray)) { - Logger::info('Status is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); + if (DI::contentItem()->isTooOld($datarray['created'], $datarray['uid'])) { + DI::logger()->info('Status is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); return false; } @@ -2748,7 +2736,7 @@ class Diaspora self::sendParticipation($contact, $datarray); if ($message_id) { - Logger::info('Stored item ' . $datarray['guid'] . ' with message id ' . $message_id); + DI::logger()->info('Stored item ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id); } @@ -2802,18 +2790,18 @@ class Diaspora */ public static function encodePrivateData(string $msg, array $user, array $contact, string $prvkey, string $pubkey): string { - Logger::debug('Message: ' . $msg); + DI::logger()->debug('Diaspora message', ['msg' => $msg]); // without a public key nothing will work if (!$pubkey) { - Logger::notice('pubkey missing: contact id: ' . $contact['id']); - return false; + DI::logger()->notice('pubkey missing: contact id: ' . $contact['id']); + return ''; } - $aes_key = random_bytes(32); + $aes_key = random_bytes(32); $b_aes_key = base64_encode($aes_key); - $iv = random_bytes(16); - $b_iv = base64_encode($iv); + $iv = random_bytes(16); + $b_iv = base64_encode($iv); $ciphertext = self::aesEncrypt($aes_key, $iv, $msg); @@ -2821,12 +2809,12 @@ class Diaspora $encrypted_key_bundle = ''; if (!@openssl_public_encrypt($json, $encrypted_key_bundle, $pubkey)) { - return false; + return ''; } $json_object = json_encode( [ - 'aes_key' => base64_encode($encrypted_key_bundle), + 'aes_key' => base64_encode($encrypted_key_bundle), 'encrypted_magic_envelope' => base64_encode($ciphertext) ] ); @@ -2846,12 +2834,12 @@ class Diaspora public static function buildMagicEnvelope(string $msg, array $user): string { $b64url_data = Strings::base64UrlEncode($msg); - $data = str_replace(["\n", "\r", ' ', "\t"], ['', '', '', ''], $b64url_data); + $data = str_replace(["\n", "\r", ' ', "\t"], ['', '', '', ''], $b64url_data); - $key_id = Strings::base64UrlEncode(self::myHandle($user)); - $type = 'application/xml'; - $encoding = 'base64url'; - $alg = 'RSA-SHA256'; + $key_id = Strings::base64UrlEncode(self::myHandle($user)); + $type = 'application/xml'; + $encoding = 'base64url'; + $alg = 'RSA-SHA256'; $signable_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); // Fallback if the private key wasn't transmitted in the expected field @@ -2860,7 +2848,7 @@ class Diaspora } $signature = Crypto::rsaSign($signable_data, $user['uprvkey']); - $sig = Strings::base64UrlEncode($signature); + $sig = Strings::base64UrlEncode($signature); $xmldata = [ 'me:env' => [ @@ -2926,17 +2914,16 @@ class Diaspora /** * Transmit a message to a target server * - * @param array $owner the array of the item owner * @param array $contact Target of the communication * @param string $envelope The message that is to be transmitted * @param bool $public_batch Is it a public post? * @param string $guid message guid * * @return int Result of the transmission - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws InternalServerErrorException * @throws \ImagickException */ - private static function transmit(array $owner, array $contact, string $envelope, bool $public_batch, string $guid = ''): int + private static function transmit(array $contact, string $envelope, bool $public_batch, string $guid = ''): int { $enabled = intval(DI::config()->get('system', 'diaspora_enabled')); if (!$enabled) { @@ -2948,7 +2935,7 @@ class Diaspora // We always try to use the data from the diaspora-contact table. // This is important for transmitting data to Friendica servers. try { - $target = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr'])); + $target = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr'])); $dest_url = $public_batch ? $target->batch : $target->notify; } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { } @@ -2958,29 +2945,35 @@ class Diaspora } if (!$dest_url) { - Logger::notice('No URL for contact: ' . $contact['id'] . ' batch mode =' . $public_batch); + DI::logger()->notice('No URL for contact: ' . $contact['id'] . ' batch mode =' . $public_batch); return 0; } - Logger::info('transmit: ' . $logid . '-' . $guid . ' ' . $dest_url); + DI::logger()->info('transmit: ' . $logid . '-' . $guid . ' ' . $dest_url); if (!intval(DI::config()->get('system', 'diaspora_test'))) { $content_type = (($public_batch) ? 'application/magic-envelope+xml' : 'application/json'); - $postResult = DI::httpClient()->post($dest_url . '/', $envelope, ['Content-Type' => $content_type]); + try { + $postResult = DI::httpClient()->post($dest_url . '/', $envelope, ['Content-Type' => $content_type], 0, HttpClientRequest::DIASPORA); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return 0; + } $return_code = $postResult->getReturnCode(); } else { - Logger::notice('test_mode'); + DI::logger()->notice('test_mode'); return 200; } if (!empty($contact['gsid']) && (empty($return_code) || $postResult->isTimeout())) { GServer::setFailureById($contact['gsid']); } elseif (!empty($contact['gsid']) && ($return_code >= 200) && ($return_code <= 299)) { + Item::incrementOutbound(Protocol::DIASPORA); GServer::setReachableById($contact['gsid'], Protocol::DIASPORA); } - Logger::info('transmit: ' . $logid . '-' . $guid . ' to ' . $dest_url . ' returns: ' . $return_code); + DI::logger()->info('transmit: ' . $logid . '-' . $guid . ' to ' . $dest_url . ' returns: ' . $return_code); return $return_code ? $return_code : -1; } @@ -3036,14 +3029,14 @@ class Diaspora // The "addr" field should always be filled. // If this isn't the case, it will raise a notice some lines later. // And in the log we will see where it came from, and we can handle it there. - Logger::notice('Empty addr', ['contact' => $contact ?? []]); + DI::logger()->notice('Empty addr', ['contact' => $contact]); } $envelope = self::buildMessage($msg, $owner, $contact, $owner['uprvkey'], $pubkey ?? '', $public_batch); - $return_code = self::transmit($owner, $contact, $envelope, $public_batch, $guid); + $return_code = self::transmit($contact, $envelope, $public_batch, $guid); - Logger::info('Transmitted message', ['owner' => $owner['uid'], 'target' => $contact['addr'], 'type' => $type, 'guid' => $guid, 'result' => $return_code]); + DI::logger()->info('Transmitted message', ['owner' => $owner['uid'], 'target' => $contact['addr'], 'type' => $type, 'guid' => $guid, 'result' => $return_code]); return $return_code; } @@ -3076,9 +3069,9 @@ class Diaspora // If the item belongs to a user, we take this user id. if ($item['uid'] == 0) { // @todo Possibly use an administrator account? - $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false, 'account-type' => User::ACCOUNT_TYPE_PERSON]; + $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false, 'account-type' => User::ACCOUNT_TYPE_PERSON]; $first_user = DBA::selectFirst('user', ['uid'], $condition, ['order' => ['uid']]); - $owner = User::getOwnerDataById($first_user['uid']); + $owner = User::getOwnerDataById($first_user['uid']); } else { $owner = User::getOwnerDataById($item['uid']); } @@ -3086,13 +3079,13 @@ class Diaspora $author_handle = self::myHandle($owner); $message = [ - 'author' => $author_handle, - 'guid' => System::createUUID(), + 'author' => $author_handle, + 'guid' => System::createUUID(), 'parent_type' => 'Post', 'parent_guid' => $item['guid'] ]; - Logger::info('Send participation for ' . $item['guid'] . ' by ' . $author_handle); + DI::logger()->info('Send participation for ' . $item['guid'] . ' by ' . $author_handle); // It doesn't matter what we store, we only want to avoid sending repeated notifications for the same item DI::cache()->set($cachekey, $item['guid'], Duration::QUARTER_HOUR); @@ -3114,18 +3107,18 @@ class Diaspora public static function sendAccountMigration(array $owner, array $contact, int $uid): int { $old_handle = DI::pConfig()->get($uid, 'system', 'previous_addr'); - $profile = self::createProfileData($uid); + $profile = self::createProfileData($uid); $signed_text = 'AccountMigration:' . $old_handle . ':' . $profile['author']; - $signature = base64_encode(Crypto::rsaSign($signed_text, $owner['uprvkey'], 'sha256')); + $signature = base64_encode(Crypto::rsaSign($signed_text, $owner['uprvkey'], 'sha256')); $message = [ - 'author' => $old_handle, - 'profile' => $profile, + 'author' => $old_handle, + 'profile' => $profile, 'signature' => $signature ]; - Logger::info('Send account migration', ['msg' => $message]); + DI::logger()->info('Send account migration', ['msg' => $message]); return self::buildAndTransmit($owner, $contact, 'account_migration', $message); } @@ -3165,13 +3158,13 @@ class Diaspora */ $message = [ - 'author' => self::myHandle($owner), + 'author' => self::myHandle($owner), 'recipient' => $contact['addr'], 'following' => 'true', - 'sharing' => 'true' + 'sharing' => 'true' ]; - Logger::info('Send share', ['msg' => $message]); + DI::logger()->info('Send share', ['msg' => $message]); return self::buildAndTransmit($owner, $contact, 'contact', $message); } @@ -3194,7 +3187,7 @@ class Diaspora 'sharing' => 'false' ]; - Logger::info('Send unshare', ['msg' => $message]); + DI::logger()->info('Send unshare', ['msg' => $message]); return self::buildAndTransmit($owner, $contact, 'contact', $message); } @@ -3229,7 +3222,7 @@ class Diaspora /** * Create an event array * - * @param integer $event_id The id of the event + * @param string $event_id The id of the event * * @return array with event data * @throws \Friendica\Network\HTTPException\InternalServerErrorException @@ -3259,7 +3252,9 @@ class Diaspora /// @todo - establish "all day" events in Friendica $eventdata['all_day'] = 'false'; - $eventdata['timezone'] = 'UTC'; + // @todo Should be user timezone - but only if the event is supposed to be displayed + // in that specific timezone and not the user's timezone. + // $eventdata['timezone'] = 'UTC'; if ($event['start']) { $eventdata['start'] = DateTimeFormat::utc($event['start'], $mask); @@ -3275,9 +3270,9 @@ class Diaspora } if ($event['location']) { $event['location'] = preg_replace("/\[map\](.*?)\[\/map\]/ism", '$1', $event['location']); - $coord = Map::getCoordinates($event['location']); + $coord = Map::getCoordinates($event['location']); - $location = []; + $location = []; $location['address'] = html_entity_decode(BBCode::toMarkdown($event['location'])); if (!empty($coord['lat']) && !empty($coord['lon'])) { $location['lat'] = $coord['lat']; @@ -3315,9 +3310,9 @@ class Diaspora $myaddr = self::myHandle($owner); - $public = ($item['private'] == Item::PRIVATE ? 'false' : 'true'); + $public = ($item['private'] == Item::PRIVATE ? 'false' : 'true'); $created = DateTimeFormat::utc($item['received'], DateTimeFormat::ATOM); - $edited = DateTimeFormat::utc($item['edited'] ?? $item['created'], DateTimeFormat::ATOM); + $edited = DateTimeFormat::utc($item['edited'] ?? $item['created'], DateTimeFormat::ATOM); // Detect a share element and do a reshare if (($item['private'] != Item::PRIVATE) && ($ret = self::getReshareDetails($item))) { @@ -3353,42 +3348,43 @@ class Diaspora } } + $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT]); + if (!empty($attachments)) { + $body .= "\n[hr]\n"; + foreach ($attachments as $attachment) { + $body .= "[url=" . $attachment['url'] . "]" . $attachment['description'] . "[/url]\n"; + } + } + // convert to markdown - $body = html_entity_decode(BBCode::toMarkdown($body)); + $body = BBCode::toMarkdown($body); // Adding the title if (strlen($title)) { $body = '### ' . html_entity_decode($title) . "\n\n" . $body; } - $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT]); - if (!empty($attachments)) { - $body .= "\n[hr]\n"; - foreach ($attachments as $attachment) { - $body .= "[" . $attachment['description'] . "](" . $attachment['url'] . ")\n"; - } - } - $location = []; - if ($item['location'] != '') + if ($item['location'] != '') { $location['address'] = $item['location']; + } if ($item['coord'] != '') { - $coord = explode(' ', $item['coord']); + $coord = explode(' ', $item['coord']); $location['lat'] = $coord[0]; $location['lng'] = $coord[1]; } $message = [ - 'author' => $myaddr, - 'guid' => $item['guid'], - 'created_at' => $created, - 'edited_at' => $edited, - 'public' => $public, - 'text' => $body, + 'author' => $myaddr, + 'guid' => $item['guid'], + 'created_at' => $created, + 'edited_at' => $edited, + 'public' => $public, + 'text' => $body, 'provider_display_name' => $item['app'], - 'location' => $location + 'location' => $location ]; if ($native_photos) { @@ -3403,8 +3399,7 @@ class Diaspora if ($item['event-id'] > 0) { $event = self::buildEvent($item['event-id']); if (count($event)) { - // Deactivated, since Diaspora seems to have problems with the processing. - // $message['event'] = $event; + $message['event'] = $event; if ( !empty($event['location']['address']) && @@ -3520,7 +3515,7 @@ class Diaspora } $target_type = ($parent['uri'] === $parent['thr-parent'] ? 'Post' : 'Comment'); - $positive = null; + $positive = null; if ($item['verb'] === Activity::LIKE) { $positive = 'true'; } elseif ($item['verb'] === Activity::DISLIKE) { @@ -3564,15 +3559,15 @@ class Diaspora $attend_answer = 'tentative'; break; default: - Logger::warning('Unknown verb ' . $item['verb'] . ' in item ' . $item['guid']); + DI::logger()->warning('Unknown verb ' . $item['verb'] . ' in item ' . $item['guid']); return false; } return [ - 'author' => self::myHandle($owner), - 'guid' => $item['guid'], - 'parent_guid' => $parent['guid'], - 'status' => $attend_answer, + 'author' => self::myHandle($owner), + 'guid' => $item['guid'], + 'parent_guid' => $parent['guid'], + 'status' => $attend_answer, 'author_signature' => '' ]; } @@ -3597,7 +3592,7 @@ class Diaspora $toplevel_item = Post::selectFirst(['guid', 'author-id', 'author-link', 'gravity'], ['id' => $item['parent'], 'parent' => $item['parent']]); if (!DBA::isResult($toplevel_item)) { - Logger::error('Missing parent conversation item', ['parent' => $item['parent']]); + DI::logger()->error('Missing parent conversation item', ['parent' => $item['parent']]); return false; } @@ -3616,23 +3611,23 @@ class Diaspora if ( $item['author-id'] != $thread_parent_item['author-id'] && ($thread_parent_item['gravity'] != Item::GRAVITY_PARENT) - && (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) + && (empty($item['uid']) || !Feature::isEnabled($item['uid'], Feature::EXPLICIT_MENTIONS)) && !DI::config()->get('system', 'disable_implicit_mentions') ) { $body = self::prependParentAuthorMention($body, $thread_parent_item['author-link']); } - $text = html_entity_decode(BBCode::toMarkdown($body)); + $text = html_entity_decode(BBCode::toMarkdown($body)); $created = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM); - $edited = DateTimeFormat::utc($item['edited'], DateTimeFormat::ATOM); + $edited = DateTimeFormat::utc($item['edited'], DateTimeFormat::ATOM); $comment = [ - 'author' => self::myHandle($owner), - 'guid' => $item['guid'], - 'created_at' => $created, - 'edited_at' => $edited, - 'parent_guid' => $toplevel_item['guid'], - 'text' => $text, + 'author' => self::myHandle($owner), + 'guid' => $item['guid'], + 'created_at' => $created, + 'edited_at' => $edited, + 'parent_guid' => $toplevel_item['guid'], + 'text' => $text, 'author_signature' => '', ]; @@ -3660,15 +3655,17 @@ class Diaspora */ public static function sendFollowup(array $item, array $owner, array $contact, bool $public_batch = false): int { + $type = ''; + if (in_array($item['verb'], [Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE])) { $message = self::constructAttend($item, $owner); - $type = 'event_participation'; + $type = 'event_participation'; } elseif (in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { $message = self::constructLike($item, $owner); - $type = 'like'; + $type = 'like'; } elseif (!in_array($item['verb'], [Activity::FOLLOW, Activity::TAG])) { $message = self::constructComment($item, $owner); - $type = 'comment'; + $type = 'comment'; } if (empty($message)) { @@ -3701,7 +3698,7 @@ class Diaspora $type = 'comment'; } - Logger::info('Got relayable data ' . $type . ' for item ' . $item['guid'] . ' (' . $item['id'] . ')'); + DI::logger()->info('Got relayable data ' . $type . ' for item ' . $item['guid'] . ' (' . $item['id'] . ')'); $msg = json_decode($item['signed_text'] ?? '', true); @@ -3720,12 +3717,12 @@ class Diaspora $message[$field] = $data; } } else { - Logger::info('Signature text for item ' . $item['guid'] . ' (' . $item['id'] . ') could not be extracted: ' . $item['signed_text']); + DI::logger()->info('Signature text for item ' . $item['guid'] . ' (' . $item['id'] . ') could not be extracted: ' . $item['signed_text']); } $message['parent_author_signature'] = self::signature($owner, $message); - Logger::info('Relayed data', ['msg' => $message]); + DI::logger()->info('Relayed data', ['msg' => $message]); return self::buildAndTransmit($owner, $contact, $type, $message, $public_batch, $item['guid']); } @@ -3757,12 +3754,12 @@ class Diaspora } $message = [ - 'author' => $itemaddr, + 'author' => $itemaddr, 'target_guid' => $item['guid'], 'target_type' => $target_type ]; - Logger::info('Got message', ['msg' => $message]); + DI::logger()->info('Got message', ['msg' => $message]); return self::buildAndTransmit($owner, $contact, $msg_type, $message, $public_batch, $item['guid']); } @@ -3784,32 +3781,32 @@ class Diaspora $cnv = DBA::selectFirst('conv', [], ['id' => $item['convid'], 'uid' => $item['uid']]); if (!DBA::isResult($cnv)) { - Logger::notice('Conversation not found.'); + DI::logger()->notice('Conversation not found.'); return -1; } - $body = BBCode::toMarkdown($item['body']); + $body = BBCode::toMarkdown($item['body']); $created = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM); $msg = [ - 'author' => $myaddr, - 'guid' => $item['guid'], + 'author' => $myaddr, + 'guid' => $item['guid'], 'conversation_guid' => $cnv['guid'], - 'text' => $body, - 'created_at' => $created, + 'text' => $body, + 'created_at' => $created, ]; if ($item['reply']) { $message = $msg; - $type = 'message'; + $type = 'message'; } else { $message = [ - 'author' => $cnv['creator'], - 'guid' => $cnv['guid'], - 'subject' => $cnv['subject'], - 'created_at' => DateTimeFormat::utc($cnv['created'], DateTimeFormat::ATOM), + 'author' => $cnv['creator'], + 'guid' => $cnv['guid'], + 'subject' => $cnv['subject'], + 'created_at' => DateTimeFormat::utc($cnv['created'], DateTimeFormat::ATOM), 'participants' => $cnv['recips'], - 'message' => $msg + 'message' => $msg ]; $type = 'conversation'; @@ -3840,14 +3837,14 @@ class Diaspora // Take the first word as first name $first = ((strpos($name, ' ') ? trim(substr($name, 0, strpos($name, ' '))) : $name)); - $last = (($first === $name) ? '' : trim(substr($name, strlen($first)))); + $last = (($first === $name) ? '' : trim(substr($name, strlen($first)))); if ((strlen($first) < 32) && (strlen($last) < 32)) { return ['first' => $first, 'last' => $last]; } // Take the last word as last name $first = ((strrpos($name, ' ') ? trim(substr($name, 0, strrpos($name, ' '))) : $name)); - $last = (($first === $name) ? '' : trim(substr($name, strlen($first)))); + $last = (($first === $name) ? '' : trim(substr($name, strlen($first)))); if ((strlen($first) < 32) && (strlen($last) < 32)) { return ['first' => $first, 'last' => $last]; @@ -3856,12 +3853,12 @@ class Diaspora // Take the first 32 characters if there is no space in the first 32 characters if ((strpos($name, ' ') > 32) || (strpos($name, ' ') === false)) { $first = substr($name, 0, 32); - $last = substr($name, 32); + $last = substr($name, 32); return ['first' => $first, 'last' => $last]; } $first = trim(substr($name, 0, strrpos(substr($name, 0, 33), ' '))); - $last = (($first === $name) ? '' : trim(substr($name, strlen($first)))); + $last = (($first === $name) ? '' : trim(substr($name, strlen($first)))); // Check if the last name is longer than 32 characters if (strlen($last) > 32) { @@ -3885,7 +3882,7 @@ class Diaspora */ private static function createProfileData(int $uid): array { - $profile = DBA::selectFirst('owner-view', ['uid', 'addr', 'name', 'location', 'net-publish', 'dob', 'about', 'pub_keywords', 'updated'], ['uid' => $uid]); + $profile = User::getOwnerDataById($uid); if (!DBA::isResult($profile)) { return []; @@ -3899,9 +3896,9 @@ class Diaspora 'full_name' => $profile['name'], 'first_name' => $split_name['first'], 'last_name' => $split_name['last'], - 'image_url' => DI::baseUrl() . '/photo/custom/300/' . $profile['uid'] . '.jpg', - 'image_url_medium' => DI::baseUrl() . '/photo/custom/100/' . $profile['uid'] . '.jpg', - 'image_url_small' => DI::baseUrl() . '/photo/custom/50/' . $profile['uid'] . '.jpg', + 'image_url' => User::getAvatarUrl($profile, Proxy::SIZE_SMALL), + 'image_url_medium' => User::getAvatarUrl($profile, Proxy::SIZE_THUMB), + 'image_url_small' => User::getAvatarUrl($profile, Proxy::SIZE_MICRO), 'bio' => null, 'birthday' => null, 'gender' => null, @@ -3916,7 +3913,7 @@ class Diaspora $data['birthday'] = ''; if ($profile['dob'] && ($profile['dob'] > '0000-00-00')) { - [$year, $month, $day] = sscanf($profile['dob'], '%4d-%2d-%2d'); + list($year, $month, $day) = sscanf($profile['dob'], '%4d-%2d-%2d'); if ($year < 1004) { $year = 1004; } @@ -3925,12 +3922,12 @@ class Diaspora $data['bio'] = BBCode::toMarkdown($profile['about'] ?? ''); - $data['location'] = $profile['location']; + $data['location'] = $profile['location']; $data['tag_string'] = ''; if ($profile['pub_keywords']) { - $kw = str_replace(',', ' ', $profile['pub_keywords']); - $kw = str_replace(' ', ' ', $kw); + $kw = str_replace(',', ' ', $profile['pub_keywords']); + $kw = str_replace(' ', ' ', $kw); $arr = explode(' ', $kw); if (count($arr)) { for ($x = 0; $x < 5; $x++) { @@ -3958,23 +3955,23 @@ class Diaspora public static function sendProfile(int $uid, array $recipients = []) { if (!$uid) { - Logger::warning('Parameter "uid" is empty'); + DI::logger()->warning('Parameter "uid" is empty'); return; } $owner = User::getOwnerDataById($uid); if (empty($owner)) { - Logger::warning('Cannot fetch User record', ['uid' => $uid]); + DI::logger()->warning('Cannot fetch User record', ['uid' => $uid]); return; } if (empty($recipients)) { - Logger::debug('No recipients provided, fetching for user', ['uid' => $uid]); + DI::logger()->debug('No recipients provided, fetching for user', ['uid' => $uid]); $recipients = DBA::selectToArray('contact', [], ['network' => Protocol::DIASPORA, 'uid' => $uid, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]); } if (empty($recipients)) { - Logger::warning('Cannot fetch recipients', ['uid' => $uid]); + DI::logger()->warning('Cannot fetch recipients', ['uid' => $uid]); return; } @@ -3983,7 +3980,7 @@ class Diaspora // @todo Split this into single worker jobs foreach ($recipients as $recipient) { if ((empty($recipient['gsid']) || GServer::isReachableById($recipient['gsid'])) && !Contact\User::isBlocked($recipient['id'], $uid)) { - Logger::info('Send updated profile data for user ' . $uid . ' to contact ' . $recipient['id']); + DI::logger()->info('Send updated profile data for user ' . $uid . ' to contact ' . $recipient['id']); self::buildAndTransmit($owner, $recipient, 'profile', $message); } } @@ -4002,12 +3999,13 @@ class Diaspora { $owner = User::getOwnerDataById($uid); if (empty($owner)) { - Logger::info('No owner post, so not storing signature', ['uid' => $uid]); + DI::logger()->info('No owner post, so not storing signature', ['uid' => $uid]); return false; } if (!in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { - Logger::warning('Item is neither a like nor a dislike', ['uid' => $uid, 'item[verb]' => $item['verb']]);; + DI::logger()->warning('Item is neither a like nor a dislike', ['uid' => $uid, 'item[verb]' => $item['verb']]); + ; return false; } @@ -4037,7 +4035,7 @@ class Diaspora } else { $contact = Contact::getById($item['author-id'], ['url']); if (empty($contact['url'])) { - Logger::warning('Author Contact not found', ['author-id' => $item['author-id']]); + DI::logger()->warning('Author Contact not found', ['author-id' => $item['author-id']]); return false; } $url = $contact['url']; @@ -4045,13 +4043,13 @@ class Diaspora $uid = User::getIdForURL($url); if (empty($uid)) { - Logger::info('No owner post, so not storing signature', ['url' => $contact['url'] ?? 'No contact loaded']); + DI::logger()->info('No owner post, so not storing signature', ['url' => $contact['url'] ?? 'No contact loaded']); return false; } $owner = User::getOwnerDataById($uid); if (empty($owner)) { - Logger::info('No owner post, so not storing signature'); + DI::logger()->info('No owner post, so not storing signature'); return false; } @@ -4061,7 +4059,7 @@ class Diaspora } if (!self::parentSupportDiaspora($item['thr-parent-id'], $uid)) { - Logger::info('One of the parents does not support Diaspora. A signature will not be created.', ['uri-id' => $item['uri-id'], 'guid' => $item['guid']]); + DI::logger()->info('One of the parents does not support Diaspora. A signature will not be created.', ['uri-id' => $item['uri-id'], 'guid' => $item['guid']]); return false; } @@ -4088,17 +4086,17 @@ class Diaspora { $parent_post = Post::selectFirst(['gravity', 'signed_text', 'author-link', 'thr-parent-id', 'protocol'], ['uri-id' => $parent_id, 'uid' => [0, $uid]]); if (empty($parent_post['thr-parent-id'])) { - Logger::warning('Parent post does not exist.', ['parent-id' => $parent_id]); + DI::logger()->warning('Parent post does not exist.', ['parent-id' => $parent_id]); return false; } if (!self::isSupportedByContactUrl($parent_post['author-link'])) { - Logger::info('Parent author is no Diaspora contact.', ['parent-id' => $parent_id]); + DI::logger()->info('Parent author is no Diaspora contact.', ['parent-id' => $parent_id]); return false; } if (($parent_post['protocol'] != Conversation::PARCEL_DIASPORA) && ($parent_post['gravity'] == Item::GRAVITY_COMMENT) && empty($parent_post['signed_text'])) { - Logger::info('Parent comment has got no Diaspora signature.', ['parent-id' => $parent_id]); + DI::logger()->info('Parent comment has got no Diaspora signature.', ['parent-id' => $parent_id]); return false; } @@ -4124,8 +4122,8 @@ class Diaspora 'quote-uri-id' => $UriId, 'allow_cid' => $owner['allow_cid'] ?? '', 'allow_gid' => $owner['allow_gid'] ?? '', - 'deny_cid' => $owner['deny_cid'] ?? '', - 'deny_gid' => $owner['deny_gid'] ?? '', + 'deny_cid' => $owner['deny_cid'] ?? '', + 'deny_gid' => $owner['deny_gid'] ?? '', ]; if (!empty($item['allow_cid'] . $item['allow_gid'] . $item['deny_cid'] . $item['deny_gid'])) { diff --git a/src/Protocol/Diaspora/Entity/DiasporaContact.php b/src/Protocol/Diaspora/Entity/DiasporaContact.php index a8b19d292d..2811c7ee5d 100644 --- a/src/Protocol/Diaspora/Entity/DiasporaContact.php +++ b/src/Protocol/Diaspora/Entity/DiasporaContact.php @@ -1,54 +1,40 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\Diaspora\Entity; use Psr\Http\Message\UriInterface; /** - * @property-read $uriId - * @property-read $url - * @property-read $guid - * @property-read $addr - * @property-read $alias - * @property-read $nick - * @property-read $name - * @property-read $givenName - * @property-read $familyName - * @property-read $photo - * @property-read $photoMedium - * @property-read $photoSmall - * @property-read $batch - * @property-read $notify - * @property-read $poll - * @property-read $subscribe - * @property-read $searchable - * @property-read $pubKey - * @property-read $baseurl - * @property-read $gsid - * @property-read $created - * @property-read $updated - * @property-read $interacting_count - * @property-read $interacted_count - * @property-read $post_count + * @property-read int $uriId + * @property-read UriInterface $url + * @property-read string $guid + * @property-read string $addr + * @property-read UriInterface $alias + * @property-read string $nick + * @property-read string $name + * @property-read string $givenName + * @property-read string $familyName + * @property-read UriInterface $photo + * @property-read UriInterface $photoMedium + * @property-read UriInterface $photoSmall + * @property-read UriInterface $batch + * @property-read UriInterface $notify + * @property-read UriInterface $poll + * @property-read string $subscribe + * @property-read bool $searchable + * @property-read string $pubKey + * @property-read UriInterface $baseurl + * @property-read int $gsid + * @property-read \DateTime $created + * @property-read \DateTime $updated + * @property-read int $interacting_count + * @property-read int $interacted_count + * @property-read int $post_count */ class DiasporaContact extends \Friendica\BaseEntity { diff --git a/src/Protocol/Diaspora/Factory/DiasporaContact.php b/src/Protocol/Diaspora/Factory/DiasporaContact.php index 80a26b2986..29d7ac3fd4 100644 --- a/src/Protocol/Diaspora/Factory/DiasporaContact.php +++ b/src/Protocol/Diaspora/Factory/DiasporaContact.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\Diaspora\Factory; diff --git a/src/Protocol/Diaspora/Repository/DiasporaContact.php b/src/Protocol/Diaspora/Repository/DiasporaContact.php index 3d2fa6641b..5cf05b8aa0 100644 --- a/src/Protocol/Diaspora/Repository/DiasporaContact.php +++ b/src/Protocol/Diaspora/Repository/DiasporaContact.php @@ -1,26 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\Diaspora\Repository; +use DateTime; +use DateTimeZone; +use Exception; use Friendica\BaseRepository; use Friendica\Database\Database; use Friendica\Database\Definition\DbaDefinition; @@ -28,11 +17,12 @@ use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\ItemURI; -use Friendica\Network\HTTPException; -use Friendica\Protocol\Diaspora\Entity; -use Friendica\Protocol\Diaspora\Factory; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Protocol\Diaspora\Entity\DiasporaContact as DiasporaContactEntity; +use Friendica\Protocol\Diaspora\Factory\DiasporaContact as DiasporaContactFactory; use Friendica\Protocol\WebFingerUri; use Friendica\Util\DateTimeFormat; +use InvalidArgumentException; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; @@ -44,12 +34,12 @@ class DiasporaContact extends BaseRepository protected static $table_name = 'diaspora-contact-view'; - /** @var Factory\DiasporaContact */ + /** @var DiasporaContactFactory */ protected $factory; /** @var DbaDefinition */ private $definition; - public function __construct(DbaDefinition $definition, Database $database, LoggerInterface $logger, Factory\DiasporaContact $factory) + public function __construct(DbaDefinition $definition, Database $database, LoggerInterface $logger, DiasporaContactFactory $factory) { parent::__construct($database, $logger, $factory); @@ -57,67 +47,58 @@ class DiasporaContact extends BaseRepository } /** - * @param array $condition - * @param array $params - * @return Entity\DiasporaContact - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function selectOne(array $condition, array $params = []): Entity\DiasporaContact + public function selectOne(array $condition, array $params = []): DiasporaContactEntity { - return parent::_selectOne($condition, $params); + $fields = $this->_selectFirstRowAsArray( $condition, $params); + + return $this->factory->createFromTableRow($fields); } /** - * @param int $uriId - * @return Entity\DiasporaContact - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function selectOneByUriId(int $uriId): Entity\DiasporaContact + public function selectOneByUriId(int $uriId): DiasporaContactEntity { return $this->selectOne(['uri-id' => $uriId]); } /** - * @param UriInterface $uri - * @return Entity\DiasporaContact - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function selectOneByUri(UriInterface $uri): Entity\DiasporaContact + public function selectOneByUri(UriInterface $uri): DiasporaContactEntity { try { return $this->selectOne(['url' => (string) $uri]); - } catch (HTTPException\NotFoundException $e) { + } catch (NotFoundException $e) { } try { return $this->selectOne(['addr' => (string) $uri]); - } catch (HTTPException\NotFoundException $e) { + } catch (NotFoundException $e) { } return $this->selectOne(['alias' => (string) $uri]); } /** - * @param WebFingerUri $uri - * @return Entity\DiasporaContact - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function selectOneByAddr(WebFingerUri $uri): Entity\DiasporaContact + public function selectOneByAddr(WebFingerUri $uri): DiasporaContactEntity { return $this->selectOne(['addr' => $uri->getAddr()]); } /** - * @param int $uriId - * @return bool - * @throws \Exception + * @throws Exception */ public function existsByUriId(int $uriId): bool { return $this->db->exists(self::$table_name, ['uri-id' => $uriId]); } - public function save(Entity\DiasporaContact $DiasporaContact): Entity\DiasporaContact + public function save(DiasporaContactEntity $DiasporaContact): DiasporaContactEntity { $uriId = $DiasporaContact->uriId ?? ItemURI::insert(['uri' => $DiasporaContact->url, 'guid' => $DiasporaContact->guid]); @@ -159,10 +140,9 @@ class DiasporaContact extends BaseRepository * * @param WebFingerUri $uri Profile address * @param boolean $update true = always update, false = never update, null = update when not found or outdated - * @return Entity\DiasporaContact - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function getByAddr(WebFingerUri $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact + public function getByAddr(WebFingerUri $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): DiasporaContactEntity { if ($update !== self::ALWAYS_UPDATE) { try { @@ -170,7 +150,7 @@ class DiasporaContact extends BaseRepository if ($update === self::NEVER_UPDATE) { return $dcontact; } - } catch (HTTPException\NotFoundException $e) { + } catch (NotFoundException $e) { if ($update === self::NEVER_UPDATE) { throw $e; } @@ -183,7 +163,7 @@ class DiasporaContact extends BaseRepository $contact = Contact::getByURL($uri, $update, ['uri-id']); if (empty($contact['uri-id'])) { - throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found'); + throw new NotFoundException('Diaspora profile with URI ' . $uri . ' not found'); } return self::selectOneByUriId($contact['uri-id']); @@ -194,10 +174,9 @@ class DiasporaContact extends BaseRepository * * @param UriInterface $uri Profile URL * @param boolean $update true = always update, false = never update, null = update when not found or outdated - * @return Entity\DiasporaContact - * @throws HTTPException\NotFoundException + * @throws NotFoundException */ - public function getByUrl(UriInterface $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact + public function getByUrl(UriInterface $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): DiasporaContactEntity { if ($update !== self::ALWAYS_UPDATE) { try { @@ -205,7 +184,7 @@ class DiasporaContact extends BaseRepository if ($update === self::NEVER_UPDATE) { return $dcontact; } - } catch (HTTPException\NotFoundException $e) { + } catch (NotFoundException $e) { if ($update === self::NEVER_UPDATE) { throw $e; } @@ -218,7 +197,7 @@ class DiasporaContact extends BaseRepository $contact = Contact::getByURL($uri, $update, ['uri-id']); if (empty($contact['uri-id'])) { - throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found'); + throw new NotFoundException('Diaspora profile with URI ' . $uri . ' not found'); } return self::selectOneByUriId($contact['uri-id']); @@ -228,27 +207,27 @@ class DiasporaContact extends BaseRepository * Update or create a diaspora-contact entry via a probe array * * @param array $data Probe array - * @return Entity\DiasporaContact - * @throws \Exception + * @throws Exception */ - public function updateFromProbeArray(array $data): Entity\DiasporaContact + public function updateFromProbeArray(array $data): DiasporaContactEntity { if (empty($data['url'])) { - throw new \InvalidArgumentException('Missing url key in Diaspora probe data array'); + throw new InvalidArgumentException('Missing url key in Diaspora probe data array'); } if (empty($data['guid'])) { - throw new \InvalidArgumentException('Missing guid key in Diaspora probe data array'); + throw new InvalidArgumentException('Missing guid key in Diaspora probe data array'); } if (empty($data['pubkey'])) { - throw new \InvalidArgumentException('Missing pubkey key in Diaspora probe data array'); + throw new InvalidArgumentException('Missing pubkey key in Diaspora probe data array'); } $uriId = ItemURI::insert(['uri' => $data['url'], 'guid' => $data['guid']]); $contact = Contact::getByUriId($uriId, ['id', 'created']); $apcontact = APContact::getByURL($data['url'], false); + if (!empty($apcontact)) { $interacting_count = $apcontact['followers_count']; $interacted_count = $apcontact['following_count']; @@ -264,10 +243,10 @@ class DiasporaContact extends BaseRepository $DiasporaContact = $this->factory->createfromProbeData( $data, $uriId, - new \DateTime($contact['created'] ?? 'now', new \DateTimeZone('UTC')), + new DateTime($contact['created'] ?? 'now', new DateTimeZone('UTC')), $interacting_count ?? 0, - $interacted_count ?? 0, - $post_count ?? 0 + $interacted_count ?? 0, + $post_count ?? 0 ); $DiasporaContact = $this->save($DiasporaContact); @@ -280,10 +259,10 @@ class DiasporaContact extends BaseRepository /** * get a url (scheme://domain.tld/u/user) from a given contact guid * - * @param mixed $guid Hexadecimal string guid + * @param string $guid Hexadecimal string guid * * @return string the contact url or null - * @throws \Exception + * @throws Exception */ public function getUrlByGuid(string $guid): ?string { diff --git a/src/Protocol/Email.php b/src/Protocol/Email.php index 35f5fdfdd4..340e3421f5 100644 --- a/src/Protocol/Email.php +++ b/src/Protocol/Email.php @@ -1,33 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; +use Friendica\Core\Protocol; +use Friendica\DI; use Friendica\Model\Item; use Friendica\Util\Strings; -use \IMAP\Connection; +use IMAP\Connection; /** * Email class @@ -38,7 +25,7 @@ class Email * @param string $mailbox The mailbox name * @param string $username The username * @param string $password The password - * @return Connection|resource|bool + * @return Connection|resource|false * @throws \Exception */ public static function connect(string $mailbox, string $username, string $password) @@ -51,21 +38,23 @@ class Email $errors = imap_errors(); if (!empty($errors)) { - Logger::notice('IMAP Errors occurred', ['errors' => $errors]); + DI::logger()->notice('IMAP Errors occurred', ['errors' => $errors]); } $alerts = imap_alerts(); if (!empty($alerts)) { - Logger::notice('IMAP Alerts occurred: ', ['alerts' => $alerts]); + DI::logger()->notice('IMAP Alerts occurred: ', ['alerts' => $alerts]); } + if (empty($errors) && empty($alerts)) { + Item::incrementInbound(Protocol::MAIL); + } return $mbox; } /** * @param Connection|resource $mbox mailbox * @param string $email_addr email - * @return array * @throws \Exception */ public static function poll($mbox, string $email_addr): array @@ -78,21 +67,24 @@ class Email if (!$search1) { $search1 = []; } else { - Logger::debug("Found mails from ".$email_addr); + DI::logger()->debug("Found mails from ".$email_addr); + Item::incrementInbound(Protocol::MAIL); } $search2 = @imap_search($mbox, 'UNDELETED TO "' . $email_addr . '"', SE_UID); if (!$search2) { $search2 = []; } else { - Logger::debug("Found mails to ".$email_addr); + DI::logger()->debug("Found mails to ".$email_addr); + Item::incrementInbound(Protocol::MAIL); } $search3 = @imap_search($mbox, 'UNDELETED CC "' . $email_addr . '"', SE_UID); if (!$search3) { $search3 = []; } else { - Logger::debug("Found mails cc ".$email_addr); + DI::logger()->debug("Found mails cc ".$email_addr); + Item::incrementInbound(Protocol::MAIL); } $res = array_unique(array_merge($search1, $search2, $search3)); @@ -135,15 +127,16 @@ class Email */ public static function getMessage($mbox, int $uid, string $reply, array $item): array { - $ret = $item; - + $ret = $item; $struc = (($mbox && $uid) ? @imap_fetchstructure($mbox, $uid, FT_UID) : null); if (!$struc) { - Logger::notice("IMAP structure couldn't be fetched", ['uid' => $uid]); + DI::logger()->notice("IMAP structure couldn't be fetched", ['uid' => $uid]); return $ret; } + Item::incrementInbound(Protocol::MAIL); + if (empty($struc->parts)) { $html = trim(self::messageGetPart($mbox, $uid, $struc, 0, 'html')); @@ -161,7 +154,7 @@ class Email $message = ['text' => $text, 'html' => '', 'item' => $ret]; Hook::callAll('email_getmessage', $message); - $ret = $message['item']; + $ret = $message['item']; $ret['body'] = $message['text']; } } else { @@ -193,7 +186,7 @@ class Email } $ret['body'] = self::removeGPG($ret['body']); - $msg = self::removeSig($ret['body']); + $msg = self::removeSig($ret['body']); $ret['body'] = $msg['body']; $ret['body'] = self::convertQuote($ret['body'], $reply); @@ -228,8 +221,8 @@ class Email // DECODE DATA $data = ($partno) - ? @imap_fetchbody($mbox, $uid, $partno, FT_UID|FT_PEEK) - : @imap_body($mbox, $uid, FT_UID|FT_PEEK); + ? @imap_fetchbody($mbox, $uid, $partno, FT_UID | FT_PEEK) + : @imap_body($mbox, $uid, FT_UID | FT_PEEK); // Any part may be encoded, even plain text messages, so check everything. if ($p->encoding == 4) { @@ -259,7 +252,7 @@ class Email if ((isset($params['filename']) && $params['filename']) || (isset($params['name']) && $params['name'])) { // filename may be given as 'Filename' or 'Name' or both - $filename = ($params['filename'])? $params['filename'] : $params['name']; + $filename = $params['filename'] ?? $params['name']; // filename may be encoded, so see imap_mime_header_decode() $attachments[$filename] = $data; // this is a problem if two files have same name } @@ -268,7 +261,7 @@ class Email if ($p->type == 0 && $data) { // Messages may be split in different parts because of inline attachments, // so append parts together with blank row. - if (strtolower($p->subtype)==$subtype) { + if (strtolower($p->subtype) == $subtype) { $data = iconv($params['charset'], 'UTF-8//IGNORE', $data); return (trim($data) ."\n\n"); } else { @@ -292,7 +285,7 @@ class Email if (isset($p->parts) && $p->parts) { $x = ""; foreach ($p->parts as $partno0 => $p2) { - $x .= self::messageGetPart($mbox, $uid, $p2, $partno . '.' . ($partno0+1), $subtype); // 1.2, 1.2.1, etc. + $x .= self::messageGetPart($mbox, $uid, $p2, $partno . '.' . ($partno0 + 1), $subtype); // 1.2, 1.2.1, etc. } return $x; } @@ -308,10 +301,10 @@ class Email */ public static function encodeHeader(string $in_str, string $charset): string { - $out_str = $in_str; + $out_str = $in_str; $need_to_convert = false; - for ($x = 0; $x < strlen($in_str); $x ++) { + for ($x = 0; $x < strlen($in_str); $x++) { if ((ord($in_str[$x]) == 0) || ((ord($in_str[$x]) > 128))) { $need_to_convert = true; } @@ -323,8 +316,8 @@ class Email if ($out_str && $charset) { // define start delimiter, end delimiter and spacer - $end = "?="; - $start = "=?" . $charset . "?B?"; + $end = "?="; + $start = "=?" . $charset . "?B?"; $spacer = $end . "\r\n " . $start; // determine length of encoded text within chunks @@ -351,7 +344,7 @@ class Email // remove trailing spacer and // add start and end delimiters - $spacer = preg_quote($spacer, '/'); + $spacer = preg_quote($spacer, '/'); $out_str = preg_replace("/" . $spacer . "$/", "", $out_str); $out_str = $start . $out_str . $end; } @@ -381,7 +374,7 @@ class Email $part = uniqid('', true); - $html = Item::prepareBody($item); + $html = Item::prepareBody($item); $headers .= "Mime-Version: 1.0\n"; $headers .= 'Content-Type: multipart/alternative; boundary="=_'.$part.'"'."\n\n"; @@ -402,8 +395,12 @@ class Email //$message = '' . $html . ''; //$message = html2plain($html); - Logger::notice('notifier: email delivery to ' . $addr); - return mail($addr, $subject, $body, $headers); + DI::logger()->notice('notifier: email delivery to ' . $addr); + $success = mail($addr, $subject, $body, $headers); + if ($success) { + Item::incrementOutbound(Protocol::MAIL); + } + return $success; } /** @@ -574,13 +571,13 @@ class Email */ private static function removeSig(string $message): array { - $sigpos = strrpos($message, "\n-- \n"); + $sigpos = strrpos($message, "\n-- \n"); $quotepos = strrpos($message, "[/quote]"); if ($sigpos == 0) { // Especially for web.de who are using that as a separator - $message = str_replace("\n___________________________________________________________\n", "\n-- \n", $message); - $sigpos = strrpos($message, "\n-- \n"); + $message = str_replace("\n___________________________________________________________\n", "\n-- \n", $message); + $sigpos = strrpos($message, "\n-- \n"); $quotepos = strrpos($message, "[/quote]"); } @@ -595,10 +592,10 @@ class Email if (!empty($result[1]) && !empty($result[2])) { $cleaned = trim($result[1])."\n"; - $sig = trim($result[2]); + $sig = trim($result[2]); } else { $cleaned = $message; - $sig = ''; + $sig = ''; } return ['body' => $cleaned, 'sig' => $sig]; @@ -614,13 +611,13 @@ class Email { $arrbody = explode("\n", trim($message)); - $lines = []; + $lines = []; $lineno = 0; foreach ($arrbody as $i => $line) { $currquotelevel = 0; - $currline = $line; - while ((strlen($currline)>0) && ((substr($currline, 0, 1) == '>') + $currline = $line; + while ((strlen($currline) > 0) && ((substr($currline, 0, 1) == '>') || (substr($currline, 0, 1) == ' '))) { if (substr($currline, 0, 1) == '>') { $currquotelevel++; @@ -630,8 +627,8 @@ class Email } $quotelevel = 0; - $nextline = trim($arrbody[$i + 1] ?? ''); - while ((strlen($nextline)>0) && ((substr($nextline, 0, 1) == '>') + $nextline = trim($arrbody[$i + 1] ?? ''); + while ((strlen($nextline) > 0) && ((substr($nextline, 0, 1) == '>') || (substr($nextline, 0, 1) == ' '))) { if (substr($nextline, 0, 1) == '>') { $quotelevel++; @@ -645,7 +642,7 @@ class Email $lines[$lineno] .= ' '; } - while ((strlen($line)>0) && ((substr($line, 0, 1) == '>') + while ((strlen($line) > 0) && ((substr($line, 0, 1) == '>') || (substr($line, 0, 1) == ' '))) { $line = ltrim(substr($line, 1)); @@ -666,34 +663,35 @@ class Email private static function convertQuote(string $body, string $reply): string { // Convert Quotes - $arrbody = explode("\n", trim($body)); + $arrbody = explode("\n", trim($body)); $arrlevel = []; for ($i = 0; $i < count($arrbody); $i++) { $quotelevel = 0; - $quoteline = $arrbody[$i]; + $quoteline = $arrbody[$i]; - while ((strlen($quoteline)>0) and ((substr($quoteline, 0, 1) == '>') + while ((strlen($quoteline) > 0) and ((substr($quoteline, 0, 1) == '>') || (substr($quoteline, 0, 1) == ' '))) { - if (substr($quoteline, 0, 1) == '>') + if (substr($quoteline, 0, 1) == '>') { $quotelevel++; + } $quoteline = ltrim(substr($quoteline, 1)); } $arrlevel[$i] = $quotelevel; - $arrbody[$i] = $quoteline; + $arrbody[$i] = $quoteline; } - $quotelevel = 0; + $quotelevel = 0; $arrbodyquoted = []; for ($i = 0; $i < count($arrbody); $i++) { $previousquote = $quotelevel; - $quotelevel = $arrlevel[$i]; + $quotelevel = $arrlevel[$i]; while ($previousquote < $quotelevel) { - $quote = "[quote]"; + $quote = "[quote]"; $arrbody[$i] = $quote.$arrbody[$i]; $previousquote++; } @@ -729,8 +727,8 @@ class Email do { $oldmessage = $message; - $message = preg_replace('=\[/quote\][\s](.*?)\[quote\]=i', '$1', $message); - $message = str_replace('[/quote][quote]', '', $message); + $message = preg_replace('=\[/quote\][\s](.*?)\[quote\]=i', '$1', $message); + $message = str_replace('[/quote][quote]', '', $message); } while ($message != $oldmessage); $quotes = []; @@ -741,12 +739,12 @@ class Email while (($pos = strpos($message, '[quote', $start)) > 0) { $quotes[$pos] = -1; - $start = $pos + 7; + $start = $pos + 7; $startquotes++; } $endquotes = 0; - $start = 0; + $start = 0; while (($pos = strpos($message, '[/quote]', $start)) > 0) { $start = $pos + 7; @@ -762,7 +760,7 @@ class Email while (($pos = strpos($message, '[/quote]', $start)) > 0) { $quotes[$pos] = 1; - $start = $pos + 7; + $start = $pos + 7; } if (strtolower(substr($message, -8)) != '[/quote]') { @@ -776,12 +774,13 @@ class Email foreach ($quotes as $index => $quote) { $quotelevel += $quote; - if (($quotelevel == 0) and ($quotestart == 0)) + if (($quotelevel == 0) and ($quotestart == 0)) { $quotestart = $index; + } } if ($quotestart != 0) { - $message = trim(substr($message, 0, $quotestart))."\n[spoiler]".substr($message, $quotestart+7, -8) . '[/spoiler]'; + $message = trim(substr($message, 0, $quotestart))."\n[spoiler]".substr($message, $quotestart + 7, -8) . '[/spoiler]'; } return $message; diff --git a/src/Protocol/Feed.php b/src/Protocol/Feed.php index fdfe9be7d5..48780e6181 100644 --- a/src/Protocol/Feed.php +++ b/src/Protocol/Feed.php @@ -1,36 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; use DOMDocument; use DOMElement; +use DOMNode; +use DOMNodeList; use DOMXPath; use Friendica\App; use Friendica\Contact\LocalRelationship\Entity\LocalRelationship; use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; -use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Worker; use Friendica\Database\DBA; @@ -43,6 +29,7 @@ use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Network\HTTPException; use Friendica\Util\DateTimeFormat; +use Friendica\Util\Images; use Friendica\Util\Network; use Friendica\Util\ParseUrl; use Friendica\Util\Proxy; @@ -70,24 +57,24 @@ class Feed $dryRun = empty($importer) && empty($contact); if ($dryRun) { - Logger::info("Test Atom/RSS feed"); + DI::logger()->info("Test Atom/RSS feed"); } else { - Logger::info('Import Atom/RSS feed "' . $contact['name'] . '" (Contact ' . $contact['id'] . ') for user ' . $importer['uid']); + DI::logger()->info('Import Atom/RSS feed "' . $contact['name'] . '" (Contact ' . $contact['id'] . ') for user ' . $importer['uid']); } $xml = trim($xml); if (empty($xml)) { - Logger::info('XML is empty.'); + DI::logger()->info('XML is empty.'); 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(); @@ -103,14 +90,14 @@ class Feed $xpath->registerNamespace('media', 'http://search.yahoo.com/mrss/'); $xpath->registerNamespace('poco', ActivityNamespace::POCO); - $author = []; - $atomns = 'atom'; - $entries = null; + $author = []; + $atomns = 'atom'; + $entries = null; $protocol = Conversation::PARCEL_UNKNOWN; // Is it RDF? if ($xpath->query('/rdf:RDF/rss:channel')->length > 0) { - $protocol = Conversation::PARCEL_RDF; + $protocol = Conversation::PARCEL_RDF; $author['author-link'] = XML::getFirstNodeValue($xpath, '/rdf:RDF/rss:channel/rss:link/text()'); $author['author-name'] = XML::getFirstNodeValue($xpath, '/rdf:RDF/rss:channel/rss:title/text()'); @@ -121,9 +108,9 @@ class Feed } if ($xpath->query('/opml')->length > 0) { - $protocol = Conversation::PARCEL_OPML; + $protocol = Conversation::PARCEL_OPML; $author['author-name'] = XML::getFirstNodeValue($xpath, '/opml/head/title/text()'); - $entries = $xpath->query('/opml/body/outline'); + $entries = $xpath->query('/opml/body/outline'); } // Is it Atom? @@ -131,7 +118,7 @@ class Feed $protocol = Conversation::PARCEL_ATOM; } elseif ($xpath->query('/atom03:feed')->length > 0) { $protocol = Conversation::PARCEL_ATOM03; - $atomns = 'atom03'; + $atomns = 'atom03'; } if (in_array($protocol, [Conversation::PARCEL_ATOM, Conversation::PARCEL_ATOM03])) { @@ -218,7 +205,7 @@ class Feed // Is it RSS? if ($xpath->query('/rss/channel')->length > 0) { - $protocol = Conversation::PARCEL_RSS; + $protocol = Conversation::PARCEL_RSS; $author['author-link'] = XML::getFirstNodeValue($xpath, '/rss/channel/link/text()'); $author['author-name'] = XML::getFirstNodeValue($xpath, '/rss/channel/title/text()'); @@ -266,8 +253,8 @@ class Feed $author['author-avatar'] = $contact['thumb']; - $author['owner-link'] = $contact['url']; - $author['owner-name'] = $contact['name']; + $author['owner-link'] = $contact['url']; + $author['owner-name'] = $contact['name']; $author['owner-avatar'] = $contact['thumb']; } @@ -284,24 +271,95 @@ class Feed 'contact-id' => $contact['id'] ?? 0, ]; - $datarray['protocol'] = $protocol; + $datarray['protocol'] = $protocol; $datarray['direction'] = Conversation::PULL; if (!is_object($entries)) { - Logger::info("There are no entries in this feed."); + DI::logger()->info("There are no entries in this feed."); return []; } - $items = []; + $items = []; $creation_dates = []; // Limit the number of items that are about to be fetched $total_items = ($entries->length - 1); - $max_items = DI::config()->get('system', 'max_feed_items'); + $max_items = DI::config()->get('system', 'max_feed_items'); if (($max_items > 0) && ($total_items > $max_items)) { $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 @@ -326,7 +384,7 @@ class Feed if ($entry->nodeName == 'outline') { $isrss = false; $plink = ''; - $uri = ''; + $uri = ''; foreach ($entry->attributes as $attribute) { switch ($attribute->nodeName) { case 'title': @@ -351,7 +409,7 @@ class Feed } } $item['plink'] = $plink ?: $uri; - $item['uri'] = $uri ?: $plink; + $item['uri'] = $uri ?: $plink; if (!$isrss || empty($item['uri'])) { continue; } @@ -373,42 +431,35 @@ class Feed } $guid = XML::getFirstNodeValue($xpath, 'guid/text()', $entry); + $host = self::getHostname($item, $guid, $basepath); if (!empty($guid)) { - $item['uri'] = $guid; + if (empty($item['uri'])) { + $item['uri'] = $guid; + } // Don't use the GUID value directly but instead use it as a basis for the GUID - $item['guid'] = Item::guidFromUri($guid, parse_url($guid, PHP_URL_HOST) ?? parse_url($item['plink'], PHP_URL_HOST)); + $item['guid'] = Item::guidFromUri($guid, $host); } if (empty($item['uri'])) { $item['uri'] = $item['plink']; } + if (!parse_url($item['uri'], PHP_URL_HOST)) { + $item['uri'] = 'feed::' . $host . ':' . $item['uri']; + } + $orig_plink = $item['plink']; - try { - $item['plink'] = DI::httpClient()->finalUrl($item['plink']); - } catch (TransferException $exception) { - Logger::notice('Item URL couldn\'t get expanded', ['url' => $item['plink'], 'exception' => $exception]); + if (!$dryRun) { + try { + $item['plink'] = DI::httpClient()->finalUrl($item['plink']); + } catch (TransferException $exception) { + DI::logger()->notice('Item URL couldn\'t get expanded', ['url' => $item['plink'], 'exception' => $exception]); + } } - 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'] = 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); @@ -447,7 +498,7 @@ class Feed if (DBA::isResult($previous)) { // Use the creation date when the post had been stored. It can happen this date changes in the feed. $creation_dates[] = $previous['created']; - Logger::info('Item with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . ' already existed under id ' . $previous['id']); + DI::logger()->info('Item with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . ' already existed under id ' . $previous['id']); continue; } $creation_dates[] = DateTimeFormat::utc($item['created']); @@ -482,9 +533,9 @@ class Feed $enclosures = $xpath->query("enclosure|$atomns:link[@rel='enclosure']", $entry); if (!empty($enclosures)) { foreach ($enclosures as $enclosure) { - $href = ''; + $href = ''; $length = null; - $type = null; + $type = null; foreach ($enclosure->attributes as $attribute) { if (in_array($attribute->name, ['url', 'href'])) { @@ -511,7 +562,7 @@ class Feed } } - $taglist = []; + $taglist = []; $categories = $xpath->query('category', $entry); foreach ($categories as $category) { $taglist[] = $category->nodeValue; @@ -532,11 +583,7 @@ class Feed } if (empty($body)) { - $body = $summary; - $summary = ''; - } - - if ($body == $summary) { + $body = $summary; $summary = ''; } @@ -545,35 +592,38 @@ class Feed if (self::titleIsBody($item['title'], $body)) { $item['title'] = ''; } - $item['body'] = HTML::toBBCode($body, $basepath); - // Remove tracking pixels - $item['body'] = preg_replace("/\[img=1x1\]([^\[\]]*)\[\/img\]/Usi", '', $item['body']); + $item['body'] = self::formatBody($body, $basepath); + $summary = self::formatBody($summary, $basepath); if (($item['body'] == '') && ($item['title'] != '')) { - $item['body'] = $item['title']; + $item['body'] = $item['title']; $item['title'] = ''; } if ($dryRun) { $item['attachments'] = $attachments; - $items[] = $item; + $items[] = $item; break; } elseif (!Item::isValid($item)) { - Logger::info('Feed item is invalid', ['created' => $item['created'], 'uid' => $item['uid'], 'uri' => $item['uri']]); + DI::logger()->info('Feed item is invalid', ['created' => $item['created'], 'uid' => $item['uid'], 'uri' => $item['uri']]); continue; - } elseif (Item::isTooOld($item)) { - Logger::info('Feed is too old', ['created' => $item['created'], 'uid' => $item['uid'], 'uri' => $item['uri']]); + } elseif (DI::contentItem()->isTooOld($item['created'], $item['uid'])) { + DI::logger()->info('Feed is too old', ['created' => $item['created'], 'uid' => $item['uid'], 'uri' => $item['uri']]); continue; } - $fetch_further_information = $contact['fetch_further_information'] ?? LocalRelationship::FFI_NONE; + if (!empty($item['plink'])) { + $fetch_further_information = $contact['fetch_further_information'] ?? LocalRelationship::FFI_NONE; + } else { + $fetch_further_information = LocalRelationship::FFI_NONE; + } $preview = ''; if (in_array($fetch_further_information, [LocalRelationship::FFI_INFORMATION, LocalRelationship::FFI_BOTH])) { // Handle enclosures and treat them as preview picture foreach ($attachments as $attachment) { - if ($attachment['mimetype'] == 'image/jpeg') { + if (Images::isSupportedMimeType($attachment['mimetype'])) { $preview = $attachment['url']; } } @@ -582,24 +632,18 @@ class Feed $item['body'] = str_replace($item['plink'], '', $item['body']); $item['body'] = trim(preg_replace('/\[url\=\](\w+.*?)\[\/url\]/i', '', $item['body'])); - // Replace the content when the title is longer than the body - $replace = (strlen($item['title']) > strlen($item['body'])); + $summary = str_replace($item['plink'], '', $summary); + $summary = trim(preg_replace('/\[url\=\](\w+.*?)\[\/url\]/i', '', $summary)); - // Replace it, when there is an image in the body - if (strstr($item['body'], '[/img]')) { - $replace = true; + if (!empty($summary) && self::replaceBodyWithTitle($summary, $item['title'])) { + $summary = ''; } - // Replace it, when there is a link in the body - if (strstr($item['body'], '[/url]')) { - $replace = true; - } - - $saved_body = $item['body']; + $saved_body = $item['body']; $saved_title = $item['title']; - if ($replace) { - $item['body'] = trim($item['title']); + if (self::replaceBodyWithTitle($item['body'], $item['title'])) { + $item['body'] = $summary ?: $item['title']; } $data = ParseUrl::getSiteinfoCached($item['plink']); @@ -625,7 +669,7 @@ class Feed // Take the data that was provided by the feed if the query is empty if (($data['type'] == 'link') && empty($data['title']) && empty($data['text'])) { $data['title'] = $saved_title; - $item['body'] = $saved_body; + $item['body'] = $saved_body; } $data_text = strip_tags(trim($data['text'] ?? '')); @@ -636,11 +680,11 @@ class Feed } // We always strip the title since it will be added in the page information - $item['title'] = ''; - $item['body'] = $item['body'] . "\n" . PageInfo::getFooterFromData($data, false); - $taglist = $fetch_further_information == LocalRelationship::FFI_BOTH ? PageInfo::getTagsFromUrl($item['plink'], $preview, $contact['ffi_keyword_denylist'] ?? '') : []; + $item['title'] = ''; + $item['body'] = $item['body'] . "\n" . PageInfo::getFooterFromData($data, false); + $taglist = $fetch_further_information == LocalRelationship::FFI_BOTH ? PageInfo::getTagsFromUrl($item['plink'], $preview, $contact['ffi_keyword_denylist'] ?? '') : []; $item['object-type'] = Activity\ObjectType::BOOKMARK; - $attachments = []; + $attachments = []; foreach (['audio', 'video'] as $elementname) { if (!empty($data[$elementname])) { @@ -666,10 +710,6 @@ class Feed } } } else { - if (!empty($summary)) { - $item['body'] = '[abstract]' . HTML::toBBCode($summary, $basepath) . "[/abstract]\n" . $item['body']; - } - if ($fetch_further_information == LocalRelationship::FFI_KEYWORD) { if (empty($taglist)) { $taglist = PageInfo::getTagsFromUrl($item['plink'], $preview, $contact['ffi_keyword_denylist'] ?? ''); @@ -689,9 +729,9 @@ class Feed $item['post-type'] = Item::PT_NOTE; } - Logger::info('Stored feed', ['item' => $item]); + DI::logger()->info('Stored feed', ['item' => $item]); - $notify = Item::isRemoteSelf($contact, $item); + $notify = Item::isRemoteSelf($contact, $item); $item['wall'] = (bool)$notify; // Distributed items should have a well-formatted URI. @@ -713,57 +753,50 @@ class Feed Post\Delayed::publish($item, $notify, $taglist, $attachments); } else { $postings[] = [ - 'item' => $item, 'notify' => $notify, + 'item' => $item, 'notify' => $notify, 'taglist' => $taglist, 'attachments' => $attachments ]; } } else { - Logger::info('Post already created or exists in the delayed posts queue', ['uid' => $item['uid'], 'uri' => $item['uri']]); + DI::logger()->info('Post already created or exists in the delayed posts queue', ['uid' => $item['uid'], 'uri' => $item['uri']]); } } - 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); - 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($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; } + /** + * Return the hostname out of a variety of provided URL + * + * @param array $item + * @param string|null $guid + * @param string|null $basepath + * @return string + */ + private static function getHostname(array $item, string $guid = null, string $basepath = null): string + { + $host = parse_url($item['plink'], PHP_URL_HOST); + if (!empty($host)) { + return $host; + } + + $host = parse_url($item['uri'], PHP_URL_HOST); + if (!empty($host)) { + return $host; + } + + $host = parse_url($guid, PHP_URL_HOST); + if (!empty($host)) { + return $host; + } + + $host = parse_url($item['author-link'], PHP_URL_HOST); + if (!empty($host)) { + return $host; + } + + return parse_url($basepath, PHP_URL_HOST); + } /** * Automatically adjust the poll frequency according to the post frequency * @@ -774,21 +807,21 @@ class Feed private static function adjustPollFrequency(array $contact, array $creation_dates) { if ($contact['network'] != Protocol::FEED) { - Logger::info('Contact is no feed, skip.', ['id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url'], 'network' => $contact['network']]); + DI::logger()->info('Contact is no feed, skip.', ['id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url'], 'network' => $contact['network']]); return; } if (!empty($creation_dates)) { // Count the post frequency and the earliest and latest post date - $frequency = []; - $oldest = time(); - $newest = 0; - $oldest_date = $newest_date = ''; + $frequency = []; + $oldest = time(); + $newest = 0; + $newest_date = ''; foreach ($creation_dates as $date) { $timestamp = strtotime($date); - $day = intdiv($timestamp, 86400); - $hour = $timestamp % 86400; + $day = intdiv($timestamp, 86400); + $hour = $timestamp % 86400; // Only have a look at values from the last seven days if (((time() / 86400) - $day) < 7) { @@ -806,32 +839,31 @@ class Feed } if ($oldest > $day) { $oldest = $day; - $oldest_date = $date; } if ($newest < $day) { - $newest = $day; + $newest = $day; $newest_date = $date; } } if (count($creation_dates) == 1) { - Logger::info('Feed had posted a single time, switching to daily polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->info('Feed had posted a single time, switching to daily polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); $priority = 8; // Poll once a day } if (empty($priority) && (((time() / 86400) - $newest) > 730)) { - Logger::info('Feed had not posted for two years, switching to monthly polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->info('Feed had not posted for two years, switching to monthly polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); $priority = 10; // Poll every month } if (empty($priority) && (((time() / 86400) - $newest) > 365)) { - Logger::info('Feed had not posted for a year, switching to weekly polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->info('Feed had not posted for a year, switching to weekly polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); $priority = 9; // Poll every week } if (empty($priority) && empty($frequency)) { - Logger::info('Feed had not posted for at least a week, switching to daily polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->info('Feed had not posted for at least a week, switching to daily polling', ['newest' => $newest_date, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); $priority = 8; // Poll once a day } @@ -848,7 +880,7 @@ class Feed // Assume at least four hours between oldest and newest post per day - should be okay for news outlets $duration = max($entry['high'] - $entry['low'], 14400); - $ppd = (86400 / $duration) * $entry['count']; + $ppd = (86400 / $duration) * $entry['count']; if ($ppd > $max) { $max = $ppd; } @@ -868,15 +900,15 @@ class Feed } else { $priority = 7; // Poll twice a day } - Logger::info('Calculated priority by the posts per day', ['priority' => $priority, 'max' => round($max, 2), 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->info('Calculated priority by the posts per day', ['priority' => $priority, 'max' => round($max, 2), 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); } } else { - Logger::info('No posts, switching to daily polling', ['id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->info('No posts, switching to daily polling', ['id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); $priority = 8; // Poll once a day } if ($contact['rating'] != $priority) { - Logger::notice('Adjusting priority', ['old' => $contact['rating'], 'new' => $priority, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); + DI::logger()->notice('Adjusting priority', ['old' => $contact['rating'], 'new' => $priority, 'id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url']]); Contact::update(['rating' => $priority], ['id' => $contact['id']]); } } @@ -903,11 +935,6 @@ class Feed $rating = 9; } - // Friendica and OStatus are checked once a day - if (in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS])) { - $rating = 8; - } - // Check archived contacts or contacts with unsupported protocols once a month if ($contact['archive'] || in_array($contact['network'], [Protocol::ZOT, Protocol::PHANTOM])) { $rating = 10; @@ -971,7 +998,7 @@ class Feed $pos = strrpos($title, '...'); if ($pos > 0) { $title = substr($title, 0, $pos); - $body = substr($body, 0, $pos); + $body = substr($body, 0, $pos); } } return ($title == $body); @@ -1002,57 +1029,55 @@ class Feed { $stamp = microtime(true); - $cachekey = 'feed:feed:' . $owner['nickname'] . ':' . $filter . ':' . $last_update; - // Display events in the user's timezone if (strlen($owner['timezone'])) { - DI::app()->setTimeZone($owner['timezone']); + DI::appHelper()->setTimeZone($owner['timezone']); } $previous_created = $last_update; - // Don't cache when the last item was posted less than 15 minutes ago (Cache duration) - if ((time() - strtotime($owner['last-item'])) < 15 * 60) { - $result = DI::cache()->get($cachekey); - if (!$nocache && !is_null($result)) { - Logger::info('Cached feed duration', ['seconds' => number_format(microtime(true) - $stamp, 3), 'nick' => $owner['nickname'], 'filter' => $filter, 'created' => $previous_created]); - return $result['feed']; - } - } - $check_date = empty($last_update) ? '' : DateTimeFormat::utc($last_update); - $authorid = Contact::getIdForURL($owner['url']); + $authorid = Contact::getIdForURL($owner['url']); $condition = [ - "`uid` = ? AND `received` > ? AND NOT `deleted` AND `gravity` IN (?, ?) - AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?, ?, ?)", + "`uid` = ? AND `received` > ? AND NOT `deleted` + AND ((`gravity` IN (?, ?) AND `wall`) OR (`gravity` = ? AND `verb` = ?)) + AND `origin` AND `private` != ? AND `visible` AND `parent-network` IN (?, ?, ?) + AND `author-id` = ?", $owner['uid'], $check_date, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT, - Item::PRIVATE, Protocol::ACTIVITYPUB, - Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA + Item::GRAVITY_ACTIVITY, Activity::ANNOUNCE, + Item::PRIVATE, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, + $authorid ]; if ($filter === 'comments') { - $condition[0] .= " AND `gravity` = ? "; - $condition[] = Item::GRAVITY_COMMENT; - } - - if ($owner['account-type'] != User::ACCOUNT_TYPE_COMMUNITY) { - $condition[0] .= " AND `contact-id` = ? AND `author-id` = ?"; - $condition[] = $owner['id']; - $condition[] = $authorid; + $condition = DBA::mergeConditions($condition, ['gravity' => Item::GRAVITY_COMMENT]); + } elseif ($filter === 'posts') { + $condition = DBA::mergeConditions($condition, ['gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_ACTIVITY]]); } $params = ['order' => ['received' => true], 'limit' => $max_items]; - if ($filter === 'posts') { - $ret = Post::selectThread(Item::DELIVER_FIELDLIST, $condition, $params); - } else { - $ret = Post::select(Item::DELIVER_FIELDLIST, $condition, $params); - } + $ret = Post::selectOrigin(Item::DELIVER_FIELDLIST, $condition, $params); $items = Post::toArray($ret); + $reshares = []; + foreach ($items as $index => $item) { + if ($item['verb'] == Activity::ANNOUNCE) { + $reshares[$item['thr-parent-id']] = $index; + } + } + + if (!empty($reshares)) { + $posts = Post::selectToArray(Item::DELIVER_FIELDLIST, ['uri-id' => array_keys($reshares), 'uid' => $owner['uid']]); + foreach ($posts as $post) { + $items[$reshares[$post['uri-id']]] = $post; + } + } + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->formatOutput = true; $root = self::addHeader($doc, $owner, $filter); @@ -1068,10 +1093,7 @@ class Feed $feeddata = trim($doc->saveXML()); - $msg = ['feed' => $feeddata, 'last_update' => $last_update]; - DI::cache()->set($cachekey, $msg, Duration::QUARTER_HOUR); - - Logger::info('Feed duration', ['seconds' => number_format(microtime(true) - $stamp, 3), 'nick' => $owner['nickname'], 'filter' => $filter, 'created' => $previous_created]); + DI::logger()->info('Feed duration', ['seconds' => number_format(microtime(true) - $stamp, 3), 'nick' => $owner['nickname'], 'filter' => $filter, 'created' => $previous_created]); return $feeddata; } @@ -1091,7 +1113,7 @@ class Feed $root = $doc->createElementNS(ActivityNamespace::ATOM1, 'feed'); $doc->appendChild($root); - $title = ''; + $title = ''; $selfUri = '/feed/' . $owner['nick'] . '/'; switch ($filter) { case 'activity': @@ -1121,8 +1143,6 @@ class Feed $attributes = ['href' => $owner['url'], 'rel' => 'alternate', 'type' => 'text/html']; XML::addElement($doc, $root, 'link', '', $attributes); - OStatus::addHubLink($doc, $root, $owner['nick']); - $attributes = ['href' => DI::baseUrl() . $selfUri, 'rel' => 'self', 'type' => 'application/atom+xml']; XML::addElement($doc, $root, 'link', '', $attributes); @@ -1153,7 +1173,6 @@ class Feed * @param DOMDocument $doc XML document * @param array $item Data of the item that is to be posted * @param array $owner Contact data of the poster - * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? * @return DOMElement Entry element * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException @@ -1161,10 +1180,10 @@ class Feed private static function noteEntry(DOMDocument $doc, array $item, array $owner): DOMElement { if (($item['gravity'] != Item::GRAVITY_PARENT) && (Strings::normaliseLink($item['author-link']) != Strings::normaliseLink($owner['url']))) { - Logger::info('Feed entry author does not match feed owner', ['owner' => $owner['url'], 'author' => $item['author-link']]); + DI::logger()->info('Feed entry author does not match feed owner', ['owner' => $owner['url'], 'author' => $item['author-link']]); } - $entry = OStatus::entryHeader($doc, $owner, $item, false); + $entry = self::entryHeader($doc, $owner, $item, false); self::entryContent($doc, $entry, $item, self::getTitle($item), '', true); @@ -1179,17 +1198,16 @@ class Feed * @param DOMDocument $doc XML document * @param \DOMElement $entry Entry element where the content is added * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster * @param string $title Title for the post * @param string $verb The activity verb * @param bool $complete Add the "status_net" element? - * @return void + * * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function entryContent(DOMDocument $doc, DOMElement $entry, array $item, $title, string $verb = '', bool $complete = true) + private static function entryContent(DOMDocument $doc, DOMElement $entry, array $item, $title, string $verb = '', bool $complete = true): void { if ($verb == '') { - $verb = OStatus::constructVerb($item); + $verb = self::constructVerb($item); } XML::addElement($doc, $entry, 'id', $item['uri']); @@ -1208,7 +1226,7 @@ class Feed 'link', '', [ - 'rel' => 'alternate', 'type' => 'text/html', + 'rel' => 'alternate', 'type' => 'text/html', 'href' => DI::baseUrl() . '/display/' . $item['guid'] ] ); @@ -1224,11 +1242,10 @@ class Feed * @param object $entry The entry element where the elements are added * @param array $item Data of the item that is to be posted * @param array $owner Contact data of the poster - * @param bool $complete default true - * @return void + * * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function entryFooter(DOMDocument $doc, $entry, array $item, array $owner) + private static function entryFooter(DOMDocument $doc, $entry, array $item, array $owner): void { $mentioned = []; @@ -1276,7 +1293,7 @@ class Feed } } - OStatus::getAttachment($doc, $entry, $item); + self::getAttachment($doc, $entry, $item); } /** @@ -1301,14 +1318,132 @@ class Feed // Remove the share element before fetching the first line $title = trim(preg_replace("/\[share.*?\](.*?)\[\/share\]/ism", "\n$1\n", $item['body'])); - $title = BBCode::toPlaintext($title) . "\n"; - $pos = strpos($title, "\n"); + $title = BBCode::toPlaintext($title) . "\n"; + $pos = strpos($title, "\n"); $trailer = ''; if (($pos == 0) || ($pos > 100)) { - $pos = 100; + $pos = 100; $trailer = '...'; } return substr($title, 0, $pos) . $trailer; } + + private static function formatBody(string $body, string $basepath): string + { + if (!HTML::isHTML($body)) { + $html = BBCode::convert($body, false, BBCode::EXTERNAL); + if ($body != $html) { + DI::logger()->debug('Body contained no HTML', ['original' => $body, 'converted' => $html]); + $body = $html; + } + } + + $body = HTML::toBBCode($body, $basepath); + + // Remove tracking pixels + return preg_replace("/\[img=1x1\]([^\[\]]*)\[\/img\]/Usi", '', $body); + } + + private static function replaceBodyWithTitle(string $body, string $title): bool + { + // Replace the content when the title is longer than the body + $replace = (strlen($title) > strlen($body)); + + // Replace it, when there is an image in the body + if (strstr($body, '[/img]')) { + $replace = true; + } + + // Replace it, when there is a link in the body + if (strstr($body, '[/url]')) { + $replace = true; + } + return $replace; + } + + /** + * Adds attachment data to the XML document + * + * @param DOMDocument $doc XML document + * @param DOMElement $root XML root element where the hub links are added + * @param array $item Data of the item that is to be posted + * @return void + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + private static function getAttachment(DOMDocument $doc, DOMElement $root, array $item) + { + foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO, Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) { + $attributes = ['rel' => 'enclosure', + 'href' => $attachment['url'], + 'type' => $attachment['mimetype']]; + + if (!empty($attachment['size'])) { + $attributes['length'] = intval($attachment['size']); + } + if (!empty($attachment['description'])) { + $attributes['title'] = $attachment['description']; + } + + XML::addElement($doc, $root, 'link', '', $attributes); + } + } + + /** + * @TODO Picture attachments should look like this: + * https://status.pirati.ca/attachment/572819 + */ + + /** + * Returns the given activity if present - otherwise returns the "post" activity + * + * @param array $item Data of the item that is to be posted + * @return string activity + */ + private static function constructVerb(array $item): string + { + if (!empty($item['verb'])) { + return $item['verb']; + } + + return Activity::POST; + } + + /** + * Adds a header element to the XML document + * + * @param DOMDocument $doc XML document + * @param array $owner Contact data of the poster + * @param array $item + * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? + * @return DOMElement The entry element where the elements are added + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + private static function entryHeader(DOMDocument $doc, array $owner, array $item, bool $toplevel): DOMElement + { + if (!$toplevel) { + $entry = $doc->createElement('entry'); + + if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) { + $entry->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY); + + $contact = Contact::getByURL($item['author-link']) ?: $owner; + $contact['nickname'] = $contact['nickname'] ?? $contact['nick']; + $author = self::addAuthor($doc, $contact); + $entry->appendChild($author); + } + } else { + $entry = $doc->createElementNS(ActivityNamespace::ATOM1, 'entry'); + + $entry->setAttribute('xmlns:thr', ActivityNamespace::THREAD); + $entry->setAttribute('xmlns:poco', ActivityNamespace::POCO); + + $author = self::addAuthor($doc, $owner); + $entry->appendChild($author); + } + + return $entry; + } } diff --git a/src/Protocol/HTTP/MediaType.php b/src/Protocol/HTTP/MediaType.php index 285f4e6df7..3c7e3af42a 100644 --- a/src/Protocol/HTTP/MediaType.php +++ b/src/Protocol/HTTP/MediaType.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\HTTP; @@ -49,7 +35,7 @@ final class MediaType private $type; /** - * @var @string + * @var string */ private $subType; diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php deleted file mode 100644 index d6a2f2dad1..0000000000 --- a/src/Protocol/OStatus.php +++ /dev/null @@ -1,1834 +0,0 @@ -. - * - */ - -namespace Friendica\Protocol; - -use DOMDocument; -use DOMElement; -use DOMXPath; -use Friendica\App; -use Friendica\Content\Text\BBCode; -use Friendica\Content\Text\HTML; -use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; -use Friendica\Core\Protocol; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\APContact; -use Friendica\Model\Contact; -use Friendica\Model\Conversation; -use Friendica\Model\Item; -use Friendica\Model\ItemURI; -use Friendica\Model\Post; -use Friendica\Model\Tag; -use Friendica\Model\User; -use Friendica\Network\HTTPClient\Client\HttpClientAccept; -use Friendica\Network\Probe; -use Friendica\Util\DateTimeFormat; -use Friendica\Util\Images; -use Friendica\Util\Proxy; -use Friendica\Util\Strings; -use Friendica\Util\XML; - -/** - * This class contain functions for the OStatus protocol - */ -class OStatus -{ - private static $itemlist; - private static $conv_list = []; - - /** - * Fetches author data - * - * @param DOMXPath $xpath The xpath object - * @param object $context The xml context of the author details - * @param array $importer user record of the importing user - * @param array $contact Called by reference, will contain the fetched contact - * @param bool $onlyfetch Only fetch the header without updating the contact entries - * - * @return array Array of author related entries for the item - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function fetchAuthor(DOMXPath $xpath, $context, array $importer, array &$contact = null, bool $onlyfetch): array - { - $author = []; - $author['author-link'] = XML::getFirstNodeValue($xpath, 'atom:author/atom:uri/text()', $context); - $author['author-name'] = XML::getFirstNodeValue($xpath, 'atom:author/atom:name/text()', $context); - $addr = XML::getFirstNodeValue($xpath, 'atom:author/atom:email/text()', $context); - - $aliaslink = $author['author-link']; - - $alternate_item = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0); - if (is_object($alternate_item)) { - foreach ($alternate_item->attributes as $attributes) { - if (($attributes->name == 'href') && ($attributes->textContent != '')) { - $author['author-link'] = $attributes->textContent; - } - } - } - $author['author-id'] = Contact::getIdForURL($author['author-link']); - - $author['contact-id'] = ($contact['id'] ?? 0) ?: $author['author-id']; - - $contact = []; - -/* - This here would be better, but we would get problems with contacts from the statusnet addon - This is kept here as a reminder for the future - - $cid = Contact::getIdForURL($author['author-link'], $importer['uid']); - if ($cid) { - $contact = DBA::selectFirst('contact', [], ['id' => $cid]); - } -*/ - if ($aliaslink != '') { - $contact = DBA::selectFirst('contact', [], [ - "`uid` = ? AND `alias` = ? AND `rel` IN (?, ?)", - $importer['uid'], - $aliaslink, - Contact::SHARING, Contact::FRIEND, - ]); - } - - if (!DBA::isResult($contact) && $author['author-link'] != '') { - if ($aliaslink == '') { - $aliaslink = $author['author-link']; - } - - $contact = DBA::selectFirst('contact', [], [ - "`uid` = ? AND `nurl` IN (?, ?) AND `rel` IN (?, ?)", - $importer['uid'], - Strings::normaliseLink($author['author-link']), - Strings::normaliseLink($aliaslink), - Contact::SHARING, - Contact::FRIEND, - ]); - } - - if (!DBA::isResult($contact) && ($addr != '')) { - $contact = DBA::selectFirst('contact', [], [ - "`uid` = ? AND `addr` = ? AND `rel` IN (?, ?)", - $importer['uid'], - $addr, - Contact::SHARING, - Contact::FRIEND, - ]); - } - - if (DBA::isResult($contact)) { - if ($contact['blocked']) { - $contact['id'] = -1; - } elseif (!empty(APContact::getByURL($contact['url'], false))) { - ActivityPub\Receiver::switchContact($contact['id'], $importer['uid'], $contact['url']); - } - $author['contact-id'] = $contact['id']; - } - - $avatarlist = []; - $avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context); - foreach ($avatars as $avatar) { - $href = ''; - $width = 0; - foreach ($avatar->attributes as $attributes) { - if ($attributes->name == 'href') { - $href = $attributes->textContent; - } - if ($attributes->name == 'width') { - $width = $attributes->textContent; - } - } - if ($href != '') { - $avatarlist[$width] = $href; - } - } - if (count($avatarlist) > 0) { - krsort($avatarlist); - $author['author-avatar'] = Probe::fixAvatar(current($avatarlist), $author['author-link']); - } - - $displayname = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()', $context); - if ($displayname != '') { - $author['author-name'] = $displayname; - } - - $author['owner-id'] = $author['author-id']; - - // Only update the contacts if it is an OStatus contact - if (DBA::isResult($contact) && ($contact['id'] > 0) && !$onlyfetch && ($contact['network'] == Protocol::OSTATUS)) { - - // Update contact data - $current = $contact; - unset($current['name-date']); - - // This query doesn't seem to work - // $value = $xpath->query("atom:link[@rel='salmon']", $context)->item(0)->nodeValue; - // if ($value != "") - // $contact["notify"] = $value; - - // This query doesn't seem to work as well - I hate these queries - // $value = $xpath->query("atom:link[@rel='self' and @type='application/atom+xml']", $context)->item(0)->nodeValue; - // if ($value != "") - // $contact["poll"] = $value; - - $contact['url'] = $author['author-link']; - $contact['nurl'] = Strings::normaliseLink($contact['url']); - - $value = XML::getFirstNodeValue($xpath, 'atom:author/atom:uri/text()', $context); - if ($value != '') { - $contact['alias'] = $value; - } - - $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()', $context); - if ($value != '') { - $contact['name'] = $value; - } - - $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:preferredUsername/text()', $context); - if ($value != '') { - $contact['nick'] = $value; - } - - $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:note/text()', $context); - if ($value != '') { - $contact['about'] = HTML::toBBCode($value); - } - - $value = XML::getFirstNodeValue($xpath, 'atom:author/poco:address/poco:formatted/text()', $context); - if ($value != '') { - $contact['location'] = $value; - } - - $contact['name-date'] = DateTimeFormat::utcNow(); - - Contact::update($contact, ['id' => $contact['id']], $current); - - if (!empty($author['author-avatar']) && ($author['author-avatar'] != $current['avatar'])) { - Logger::info('Update profile picture for contact ' . $contact['id']); - Contact::updateAvatar($contact['id'], $author['author-avatar']); - } - - // Ensure that we are having this contact (with uid=0) - $cid = Contact::getIdForURL($aliaslink); - - if ($cid) { - $fields = ['url', 'nurl', 'name', 'nick', 'alias', 'about', 'location']; - $old_contact = DBA::selectFirst('contact', $fields, ['id' => $cid]); - - // Update it with the current values - $fields = [ - 'url' => $author['author-link'], - 'name' => $contact['name'], - 'nurl' => Strings::normaliseLink($author['author-link']), - 'nick' => $contact['nick'], - 'alias' => $contact['alias'], - 'about' => $contact['about'], - 'location' => $contact['location'], - 'success_update' => DateTimeFormat::utcNow(), - 'last-update' => DateTimeFormat::utcNow(), - ]; - - Contact::update($fields, ['id' => $cid], $old_contact); - - // Update the avatar - if (!empty($author['author-avatar'])) { - Contact::updateAvatar($cid, $author['author-avatar']); - } - } - } elseif (empty($contact['network']) || ($contact['network'] != Protocol::DFRN)) { - $contact = []; - } - - return $author; - } - - /** - * Fetches author data from a given XML string - * - * @param string $xml The XML - * @param array $importer user record of the importing user - * - * @return array Array of author related entries for the item - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function salmonAuthor(string $xml, array $importer): array - { - if (empty($xml)) { - return []; - } - - $doc = new DOMDocument(); - @$doc->loadXML($xml); - - $xpath = new DOMXPath($doc); - $xpath->registerNamespace('atom', ActivityNamespace::ATOM1); - $xpath->registerNamespace('thr', ActivityNamespace::THREAD); - $xpath->registerNamespace('georss', ActivityNamespace::GEORSS); - $xpath->registerNamespace('activity', ActivityNamespace::ACTIVITY); - $xpath->registerNamespace('media', ActivityNamespace::MEDIA); - $xpath->registerNamespace('poco', ActivityNamespace::POCO); - $xpath->registerNamespace('ostatus', ActivityNamespace::OSTATUS); - $xpath->registerNamespace('statusnet', ActivityNamespace::STATUSNET); - - $contact = ['id' => 0]; - - // Fetch the first author - $authordata = $xpath->query('//author')->item(0); - $author = self::fetchAuthor($xpath, $authordata, $importer, $contact, true); - return $author; - } - - /** - * Read attributes from element - * - * @param object $element Element object - * @return array attributes - */ - private static function readAttributes($element): array - { - $attribute = []; - - foreach ($element->attributes as $attributes) { - $attribute[$attributes->name] = $attributes->textContent; - } - - return $attribute; - } - - /** - * Imports an XML string containing OStatus elements - * - * @param string $xml The XML - * @param array $importer user record of the importing user - * @param array $contact contact - * @param string $hub Called by reference, returns the fetched hub data - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function import($xml, array $importer, array &$contact, &$hub) - { - self::process($xml, $importer, $contact, $hub, false, true, Conversation::PUSH); - } - - /** - * Internal feed processing - * - * @param string $xml The XML - * @param array $importer user record of the importing user - * @param array $contact contact - * @param string $hub Called by reference, returns the fetched hub data - * @param boolean $stored Is the post fresh imported or from the database? - * @param boolean $initialize Is it the leading post so that data has to be initialized? - * @param integer $direction Direction, default UNKNOWN(0) - * @return boolean Could the XML be processed? - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function process(string $xml, array $importer, array &$contact = null, string &$hub, bool $stored = false, bool $initialize = true, int $direction = Conversation::UNKNOWN) - { - if ($initialize) { - self::$itemlist = []; - self::$conv_list = []; - } - - Logger::info('Import OStatus message for user ' . $importer['uid']); - - if (empty($xml)) { - return false; - } - - $doc = new DOMDocument(); - @$doc->loadXML($xml); - - $xpath = new DOMXPath($doc); - $xpath->registerNamespace('atom', ActivityNamespace::ATOM1); - $xpath->registerNamespace('thr', ActivityNamespace::THREAD); - $xpath->registerNamespace('georss', ActivityNamespace::GEORSS); - $xpath->registerNamespace('activity', ActivityNamespace::ACTIVITY); - $xpath->registerNamespace('media', ActivityNamespace::MEDIA); - $xpath->registerNamespace('poco', ActivityNamespace::POCO); - $xpath->registerNamespace('ostatus', ActivityNamespace::OSTATUS); - $xpath->registerNamespace('statusnet', ActivityNamespace::STATUSNET); - - $hub = ''; - $hub_items = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0); - if (is_object($hub_items)) { - $hub_attributes = $hub_items->attributes; - if (is_object($hub_attributes)) { - foreach ($hub_attributes as $hub_attribute) { - if ($hub_attribute->name == 'href') { - $hub = $hub_attribute->textContent; - Logger::info('Found hub ', ['hub' => $hub]); - } - } - } - } - - // Initial header elements - $header = [ - 'uid' => $importer['uid'], - 'network' => Protocol::OSTATUS, - 'wall' => 0, - 'origin' => 0, - 'gravity' => Item::GRAVITY_COMMENT, - ]; - - if (!is_object($doc->firstChild) || empty($doc->firstChild->tagName)) { - return false; - } - - $first_child = $doc->firstChild->tagName; - - if ($first_child == 'feed') { - $entries = $xpath->query('/atom:feed/atom:entry'); - } else { - $entries = $xpath->query('/atom:entry'); - } - - if ($entries->length == 1) { - // We reformat the XML to make it better readable - $doc2 = new DOMDocument(); - $doc2->loadXML($xml); - $doc2->preserveWhiteSpace = false; - $doc2->formatOutput = true; - $xml2 = $doc2->saveXML(); - - $header['protocol'] = Conversation::PARCEL_SALMON; - $header['source'] = $xml2; - $header['direction'] = $direction; - } elseif (!$initialize) { - return false; - } - - // Fetch the first author - $authordata = $xpath->query('//author')->item(0); - $author = self::fetchAuthor($xpath, $authordata, $importer, $contact, $stored); - - // Reverse the order of the entries - $entrylist = []; - - foreach ($entries as $entry) { - $entrylist[] = $entry; - } - - foreach (array_reverse($entrylist) as $entry) { - // fetch the author - $authorelement = $xpath->query('/atom:entry/atom:author', $entry); - - if ($authorelement->length == 0) { - $authorelement = $xpath->query('atom:author', $entry); - } - - if ($authorelement->length > 0) { - $author = self::fetchAuthor($xpath, $entry, $importer, $contact, $stored); - } - - $item = array_merge($header, $author); - - $item['uri'] = XML::getFirstNodeValue($xpath, 'atom:id/text()', $entry); - $item['uri-id'] = ItemURI::insert(['uri' => $item['uri']]); - - $item['verb'] = XML::getFirstNodeValue($xpath, 'activity:verb/text()', $entry); - - // Delete a message - if (in_array($item['verb'], ['qvitter-delete-notice', Activity::DELETE, 'delete'])) { - self::deleteNotice($item); - continue; - } - - if (in_array($item['verb'], [Activity::O_UNFAVOURITE, Activity::UNFAVORITE])) { - // Ignore "Unfavorite" message - Logger::info('Ignore unfavorite message ', ['item' => $item]); - continue; - } - - // Deletions come with the same uri, so we check for duplicates after processing deletions - if (Post::exists(['uid' => $importer['uid'], 'uri' => $item['uri']])) { - Logger::info('Post with URI ' . $item['uri'] . ' already existed for user ' . $importer['uid'] . '.'); - continue; - } else { - Logger::info('Processing post with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . '.'); - } - - if ($item['verb'] == Activity::JOIN) { - // ignore "Join" messages - Logger::info('Ignore join message ', ['item' => $item]); - continue; - } - - if ($item['verb'] == 'http://mastodon.social/schema/1.0/block') { - // ignore mastodon "block" messages - Logger::info('Ignore block message ', ['item' => $item]); - continue; - } - - if ($item['verb'] == Activity::FOLLOW) { - Contact::addRelationship($importer, $contact, $item); - continue; - } - - if ($item['verb'] == Activity::O_UNFOLLOW) { - $dummy = null; - Contact::removeFollower($contact); - continue; - } - - if ($item['verb'] == Activity::FAVORITE) { - $orig_uri = $xpath->query('activity:object/atom:id', $entry)->item(0)->nodeValue; - Logger::notice('Favorite', ['uri' => $orig_uri, 'item' => $item]); - - $item['body'] = $item['verb'] = Activity::LIKE; - $item['thr-parent'] = $orig_uri; - $item['gravity'] = Item::GRAVITY_ACTIVITY; - $item['object-type'] = Activity\ObjectType::NOTE; - } - - // http://activitystrea.ms/schema/1.0/rsvp-yes - if (!in_array($item['verb'], [Activity::POST, Activity::LIKE, Activity::SHARE])) { - Logger::info('Unhandled verb', ['verb' => $item['verb'], 'item' => $item]); - } - - self::processPost($xpath, $entry, $item, $importer); - - if ($initialize && (count(self::$itemlist) > 0)) { - if (self::$itemlist[0]['uri'] == self::$itemlist[0]['thr-parent']) { - $uid = self::$itemlist[0]['uid']; - // We will import it everytime, when it is started by our contacts - $valid = Contact::isSharingByURL(self::$itemlist[0]['author-link'], $uid); - - if (!$valid) { - // If not, then it depends on this setting - $valid = !$uid || DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_NONE; - - if ($valid) { - Logger::info('Item with URI ' . self::$itemlist[0]['uri'] . ' will be imported due to the system settings.'); - } - } else { - Logger::info('Item with URI ' . self::$itemlist[0]['uri'] . ' belongs to a contact (' . self::$itemlist[0]['contact-id'] . '). It will be imported.'); - } - - if ($valid && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE) { - // Never post a thread when the only interaction by our contact was a like - $valid = false; - $verbs = [Activity::POST, Activity::SHARE]; - foreach (self::$itemlist as $item) { - if (in_array($item['verb'], $verbs) && Contact::isSharingByURL($item['author-link'], $item['uid'])) { - $valid = true; - } - } - if ($valid) { - Logger::info('Item with URI ' . self::$itemlist[0]['uri'] . ' will be imported since the thread contains posts or shares.'); - } - } - } else { - $valid = true; - } - - if ($valid) { - $default_contact = 0; - for ($key = count(self::$itemlist) - 1; $key >= 0; $key--) { - if (empty(self::$itemlist[$key]['contact-id'])) { - self::$itemlist[$key]['contact-id'] = $default_contact; - } else { - $default_contact = $item['contact-id']; - } - } - foreach (self::$itemlist as $item) { - $found = Post::exists(['uid' => $importer['uid'], 'uri' => $item['uri']]); - if ($found) { - Logger::notice('Item with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . ' already exists.'); - } elseif ($item['contact-id'] < 0) { - Logger::notice('Item with URI ' . $item['uri'] . ' is from a blocked contact.'); - } else { - $ret = Item::insert($item); - Logger::info('Item with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . ' stored. Return value: ' . $ret); - } - } - } - self::$itemlist = []; - } - Logger::info('Processing done for post with URI ' . $item['uri'] . ' for user '.$importer['uid'] . '.'); - } - return true; - } - - /** - * Removes notice item from database - * - * @param array $item item - * @return void - * @throws \Exception - */ - private static function deleteNotice(array $item) - { - $condition = ['uid' => $item['uid'], 'author-id' => $item['author-id'], 'uri' => $item['uri']]; - if (!Post::exists($condition)) { - Logger::notice('Item from ' . $item['author-link'] . ' with uri ' . $item['uri'] . ' for user ' . $item['uid'] . " wasn't found. We don't delete it."); - return; - } - - Item::markForDeletion($condition); - - Logger::notice('Deleted item with URI ' . $item['uri'] . ' for user ' . $item['uid']); - } - - /** - * Processes the XML for a post - * - * @param DOMXPath $xpath The xpath object - * @param object $entry The xml entry that is processed - * @param array $item The item array - * @param array $importer user record of the importing user - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function processPost(DOMXPath $xpath, $entry, array &$item, array $importer) - { - $item['body'] = HTML::toBBCode(XML::getFirstNodeValue($xpath, 'atom:content/text()', $entry)); - $item['object-type'] = XML::getFirstNodeValue($xpath, 'activity:object-type/text()', $entry); - if (($item['object-type'] == Activity\ObjectType::BOOKMARK) || ($item['object-type'] == Activity\ObjectType::EVENT)) { - $item['title'] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry); - $item['body'] = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry); - } elseif ($item['object-type'] == Activity\ObjectType::QUESTION) { - $item['title'] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry); - } - - $item['created'] = XML::getFirstNodeValue($xpath, 'atom:published/text()', $entry); - $item['edited'] = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $entry); - $item['conversation'] = XML::getFirstNodeValue($xpath, 'ostatus:conversation/text()', $entry); - - $conv = $xpath->query('ostatus:conversation', $entry); - if (is_object($conv->item(0))) { - foreach ($conv->item(0)->attributes as $attributes) { - if ($attributes->name == 'ref') { - $item['conversation'] = $attributes->textContent; - } - if ($attributes->name == 'href') { - $item['conversation'] = $attributes->textContent; - } - } - } - - $related = ''; - - $inreplyto = $xpath->query('thr:in-reply-to', $entry); - if (is_object($inreplyto->item(0))) { - foreach ($inreplyto->item(0)->attributes as $attributes) { - if ($attributes->name == 'ref') { - $item['thr-parent'] = $attributes->textContent; - } - if ($attributes->name == 'href') { - $related = $attributes->textContent; - } - } - } - - $georsspoint = $xpath->query('georss:point', $entry); - if (!empty($georsspoint) && ($georsspoint->length > 0)) { - $item['coord'] = $georsspoint->item(0)->nodeValue; - } - - $categories = $xpath->query('atom:category', $entry); - if ($categories) { - foreach ($categories as $category) { - foreach ($category->attributes as $attributes) { - if ($attributes->name == 'term') { - // Store the hashtag - Tag::store($item['uri-id'], Tag::HASHTAG, $attributes->textContent); - } - } - } - } - - $self = ''; - $add_body = ''; - - $links = $xpath->query('atom:link', $entry); - if ($links) { - $link_data = self::processLinks($links, $item); - $self = $link_data['self']; - $add_body = $link_data['add_body']; - } - - $repeat_of = ''; - - $notice_info = $xpath->query('statusnet:notice_info', $entry); - if ($notice_info && ($notice_info->length > 0)) { - foreach ($notice_info->item(0)->attributes as $attributes) { - if ($attributes->name == 'source') { - $item['app'] = strip_tags($attributes->textContent); - } - if ($attributes->name == 'repeat_of') { - $repeat_of = $attributes->textContent; - } - } - } - // Is it a repeated post? - if (($repeat_of != '') || ($item['verb'] == Activity::SHARE)) { - $link_data = self::processRepeatedItem($xpath, $entry, $item, $importer); - if (!empty($link_data['add_body'])) { - $add_body .= $link_data['add_body']; - } - } - - $item['body'] .= $add_body; - - Tag::storeFromBody($item['uri-id'], $item['body']); - - // Mastodon Content Warning - if (($item['verb'] == Activity::POST) && $xpath->evaluate('boolean(atom:summary)', $entry)) { - $clear_text = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry); - if (!empty($clear_text)) { - $item['content-warning'] = HTML::toBBCode($clear_text); - } - } - - if (isset($item['thr-parent'])) { - if (!Post::exists(['uid' => $importer['uid'], 'uri' => $item['thr-parent']])) { - if ($related != '') { - self::fetchRelated($related, $item['thr-parent'], $importer); - } - } else { - Logger::info('Reply with URI ' . $item['uri'] . ' already existed for user ' . $importer['uid'] . '.'); - } - } else { - $item['thr-parent'] = $item['uri']; - $item['gravity'] = Item::GRAVITY_PARENT; - } - - self::$itemlist[] = $item; - } - - /** - * Fetch related posts and processes them - * - * @param string $related The link to the related item - * @param string $related_uri The related item in "uri" format - * @param array $importer user record of the importing user - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function fetchRelated(string $related, string $related_uri, array $importer) - { - $stored = false; - $curlResult = DI::httpClient()->get($related, HttpClientAccept::ATOM_XML); - - if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { - return; - } - - $xml = ''; - - if ($curlResult->inHeader('Content-Type') && - in_array('application/atom+xml', $curlResult->getHeader('Content-Type'))) { - Logger::info('Directly fetched XML for URI ' . $related_uri); - $xml = $curlResult->getBody(); - } - - if ($xml == '') { - $doc = new DOMDocument(); - if (!@$doc->loadHTML($curlResult->getBody())) { - return; - } - $xpath = new DOMXPath($doc); - - $atom_file = ''; - - $links = $xpath->query('//link'); - if ($links) { - foreach ($links as $link) { - $attribute = self::readAttributes($link); - if (($attribute['rel'] == 'alternate') && ($attribute['type'] == 'application/atom+xml')) { - $atom_file = $attribute['href']; - } - } - if ($atom_file != '') { - $curlResult = DI::httpClient()->get($atom_file, HttpClientAccept::ATOM_XML); - - if ($curlResult->isSuccess()) { - Logger::info('Fetched XML for URI ' . $related_uri); - $xml = $curlResult->getBody(); - } - } - } - } - - // Workaround for older GNU Social servers - if (($xml == '') && strstr($related, '/notice/')) { - $curlResult = DI::httpClient()->get(str_replace('/notice/', '/api/statuses/show/', $related) . '.atom', HttpClientAccept::ATOM_XML); - - if ($curlResult->isSuccess()) { - Logger::info('GNU Social workaround to fetch XML for URI ' . $related_uri); - $xml = $curlResult->getBody(); - } - } - - // Even more worse workaround for GNU Social ;-) - if ($xml == '') { - $related_guess = self::convertHref($related_uri); - $curlResult = DI::httpClient()->get(str_replace('/notice/', '/api/statuses/show/', $related_guess) . '.atom', HttpClientAccept::ATOM_XML); - - if ($curlResult->isSuccess()) { - Logger::info('GNU Social workaround 2 to fetch XML for URI ' . $related_uri); - $xml = $curlResult->getBody(); - } - } - - if ($xml != '') { - $hub = ''; - self::process($xml, $importer, $contact, $hub, $stored, false, Conversation::PULL); - } else { - Logger::info('XML could not be fetched for URI: ' . $related_uri . ' - href: ' . $related); - } - return; - } - - /** - * Processes the XML for a repeated post - * - * @param DOMXPath $xpath The xpath object - * @param object $entry The xml entry that is processed - * @param array $item The item array - * @param array $importer user record of the importing user - * - * @return array with data from links - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function processRepeatedItem(DOMXPath $xpath, $entry, array &$item, array $importer): array - { - $activityobject = $xpath->query('activity:object', $entry)->item(0); - - if (!is_object($activityobject)) { - return []; - } - - $link_data = []; - - $orig_uri = XML::getFirstNodeValue($xpath, 'atom:id/text()', $activityobject); - - $links = $xpath->query('atom:link', $activityobject); - if ($links) { - $link_data = self::processLinks($links, $item); - } - - $orig_body = XML::getFirstNodeValue($xpath, 'atom:content/text()', $activityobject); - $orig_created = XML::getFirstNodeValue($xpath, 'atom:published/text()', $activityobject); - $orig_edited = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $activityobject); - - $orig_author = self::fetchAuthor($xpath, $activityobject, $importer, $dummy, false); - - $item['author-name'] = $orig_author['author-name']; - $item['author-link'] = $orig_author['author-link']; - $item['author-id'] = $orig_author['author-id']; - - $item['body'] = HTML::toBBCode($orig_body); - $item['created'] = $orig_created; - $item['edited'] = $orig_edited; - - $item['uri'] = $orig_uri; - - $item['verb'] = XML::getFirstNodeValue($xpath, 'activity:verb/text()', $activityobject); - - $item['object-type'] = XML::getFirstNodeValue($xpath, 'activity:object-type/text()', $activityobject); - - // Mastodon Content Warning - if (($item['verb'] == Activity::POST) && $xpath->evaluate('boolean(atom:summary)', $activityobject)) { - $clear_text = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $activityobject); - if (!empty($clear_text)) { - $item['content-warning'] = HTML::toBBCode($clear_text); - } - } - - $inreplyto = $xpath->query('thr:in-reply-to', $activityobject); - if (is_object($inreplyto->item(0))) { - foreach ($inreplyto->item(0)->attributes as $attributes) { - if ($attributes->name == 'ref') { - $item['thr-parent'] = $attributes->textContent; - } - } - } - - return $link_data; - } - - /** - * Processes links in the XML - * - * @param object $links The xml data that contain links - * @param array $item The item array - * @return array with data from the links - */ - private static function processLinks($links, array &$item): array - { - $link_data = ['add_body' => '', 'self' => '']; - - foreach ($links as $link) { - $attribute = self::readAttributes($link); - - if (!empty($attribute['rel']) && !empty($attribute['href'])) { - switch ($attribute['rel']) { - case 'alternate': - $item['plink'] = $attribute['href']; - if (($item['object-type'] == Activity\ObjectType::QUESTION) - || ($item['object-type'] == Activity\ObjectType::EVENT) - ) { - Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::UNKNOWN, - 'url' => $attribute['href'], 'mimetype' => $attribute['type'] ?? null, - 'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]); - } - break; - - case 'ostatus:conversation': - $link_data['conversation'] = $attribute['href']; - $item['conversation'] = $link_data['conversation']; - break; - - case 'enclosure': - $filetype = strtolower(substr($attribute['type'], 0, strpos($attribute['type'], '/'))); - if ($filetype == 'image') { - $link_data['add_body'] .= "\n[img]".$attribute['href'].'[/img]'; - } else { - Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::DOCUMENT, - 'url' => $attribute['href'], 'mimetype' => $attribute['type'], - 'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]); - } - break; - - case 'related': - if ($item['object-type'] != Activity\ObjectType::BOOKMARK) { - if (!isset($item['thr-parent'])) { - $item['thr-parent'] = $attribute['href']; - } - $link_data['related'] = $attribute['href']; - } else { - Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::UNKNOWN, - 'url' => $attribute['href'], 'mimetype' => $attribute['type'] ?? null, - 'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]); - } - break; - - case 'self': - if (empty($item['plink'])) { - $item['plink'] = $attribute['href']; - } - $link_data['self'] = $attribute['href']; - break; - - default: - Logger::notice('Unsupported rel=' . $attribute['rel'] . ', href=' . $attribute['href'] . ', object-type=' . $item['object-type']); - } - } - } - return $link_data; - } - - /** - * Create an url out of an uri - * - * @param string $href URI in the format "parameter1:parameter1:..." - * @return string URL in the format http(s)://.... - */ - private static function convertHref(string $href): string - { - $elements = explode(':', $href); - - if ((count($elements) <= 2) || ($elements[0] != 'tag')) { - return $href; - } - - $server = explode(',', $elements[1]); - $conversation = explode('=', $elements[2]); - - if ((count($elements) == 4) && ($elements[2] == 'post')) { - return 'http://' . $server[0] . '/notice/' . $elements[3]; - } - - if ((count($conversation) != 2) || ($conversation[1] == '')) { - return $href; - } - - if ($elements[3] == 'objectType=thread') { - return 'http://' . $server[0] . '/conversation/' . $conversation[1]; - } else { - return 'http://' . $server[0] . '/notice/' . $conversation[1]; - } - } - - /** - * Adds the header elements to the XML document - * - * @param DOMDocument $doc XML document - * @param array $owner Contact data of the poster - * @param string $filter The related feed filter (activity, posts or comments) - * - * @return DOMElement Header root element - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - private static function addHeader(DOMDocument $doc, array $owner, string $filter): DOMElement - { - $root = $doc->createElementNS(ActivityNamespace::ATOM1, 'feed'); - $doc->appendChild($root); - - $root->setAttribute('xmlns:thr', ActivityNamespace::THREAD); - $root->setAttribute('xmlns:georss', ActivityNamespace::GEORSS); - $root->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY); - $root->setAttribute('xmlns:media', ActivityNamespace::MEDIA); - $root->setAttribute('xmlns:poco', ActivityNamespace::POCO); - $root->setAttribute('xmlns:ostatus', ActivityNamespace::OSTATUS); - $root->setAttribute('xmlns:statusnet', ActivityNamespace::STATUSNET); - $root->setAttribute('xmlns:mastodon', ActivityNamespace::MASTODON); - - $title = ''; - $selfUri = '/feed/' . $owner['nick'] . '/'; - switch ($filter) { - case 'activity': - $title = DI::l10n()->t('%s\'s timeline', $owner['name']); - $selfUri .= $filter; - break; - - case 'posts': - $title = DI::l10n()->t('%s\'s posts', $owner['name']); - break; - - case 'comments': - $title = DI::l10n()->t('%s\'s comments', $owner['name']); - $selfUri .= $filter; - break; - } - - $selfUri = '/dfrn_poll/' . $owner['nick']; - - $attributes = [ - 'uri' => 'https://friendi.ca', - 'version' => App::VERSION . '-' . DB_UPDATE_VERSION, - ]; - XML::addElement($doc, $root, 'generator', App::PLATFORM, $attributes); - XML::addElement($doc, $root, 'id', DI::baseUrl() . '/profile/' . $owner['nick']); - XML::addElement($doc, $root, 'title', $title); - XML::addElement($doc, $root, 'subtitle', sprintf("Updates from %s on %s", $owner['name'], DI::config()->get('config', 'sitename'))); - XML::addElement($doc, $root, 'logo', User::getAvatarUrl($owner, Proxy::SIZE_SMALL)); - XML::addElement($doc, $root, 'updated', DateTimeFormat::utcNow(DateTimeFormat::ATOM)); - - $author = self::addAuthor($doc, $owner, true); - $root->appendChild($author); - - $attributes = [ - 'href' => $owner['url'], - 'rel' => 'alternate', - 'type' => 'text/html', - ]; - XML::addElement($doc, $root, 'link', '', $attributes); - - /// @TODO We have to find out what this is - /// $attributes = array("href" => DI::baseUrl()."/sup", - /// "rel" => "http://api.friendfeed.com/2008/03#sup", - /// "type" => "application/json"); - /// XML::addElement($doc, $root, "link", "", $attributes); - - self::addHubLink($doc, $root, $owner['nick']); - - $attributes = ['href' => DI::baseUrl() . '/salmon/' . $owner['nick'], 'rel' => 'salmon']; - XML::addElement($doc, $root, 'link', '', $attributes); - - $attributes = ['href' => DI::baseUrl() . '/salmon/' . $owner['nick'], 'rel' => 'http://salmon-protocol.org/ns/salmon-replies']; - XML::addElement($doc, $root, 'link', '', $attributes); - - $attributes = ['href' => DI::baseUrl() . '/salmon/' . $owner['nick'], 'rel' => 'http://salmon-protocol.org/ns/salmon-mention']; - XML::addElement($doc, $root, 'link', '', $attributes); - - $attributes = ['href' => DI::baseUrl() . $selfUri, 'rel' => 'self', 'type' => 'application/atom+xml']; - XML::addElement($doc, $root, 'link', '', $attributes); - - if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) { - $members = DBA::count('contact', [ - 'uid' => $owner['uid'], - 'self' => false, - 'pending' => false, - 'archive' => false, - 'hidden' => false, - 'blocked' => false, - ]); - XML::addElement($doc, $root, 'statusnet:group_info', '', ['member_count' => $members]); - } - - return $root; - } - - /** - * Add the link to the push hubs to the XML document - * - * @param DOMDocument $doc XML document - * @param DOMElement $root XML root element where the hub links are added - * @param string $nick Nickname - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function addHubLink(DOMDocument $doc, DOMElement $root, string $nick) - { - $h = DI::baseUrl() . '/pubsubhubbub/' . $nick; - XML::addElement($doc, $root, 'link', '', ['href' => $h, 'rel' => 'hub']); - } - - /** - * Adds attachment data to the XML document - * - * @param DOMDocument $doc XML document - * @param DOMElement $root XML root element where the hub links are added - * @param array $item Data of the item that is to be posted - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function getAttachment(DOMDocument $doc, DOMElement $root, array $item) - { - foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO, Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) { - $attributes = ['rel' => 'enclosure', - 'href' => $attachment['url'], - 'type' => $attachment['mimetype']]; - - if (!empty($attachment['size'])) { - $attributes['length'] = intval($attachment['size']); - } - if (!empty($attachment['description'])) { - $attributes['title'] = $attachment['description']; - } - - XML::addElement($doc, $root, 'link', '', $attributes); - } - } - - /** - * Adds the author element to the XML document - * - * @param DOMDocument $doc XML document - * @param array $owner Contact data of the poster - * @param bool $show_profile Whether to show profile - * @return DOMElement Author element - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - private static function addAuthor(DOMDocument $doc, array $owner, bool $show_profile = true): DOMElement - { - $profile = DBA::selectFirst('profile', ['homepage', 'publish'], ['uid' => $owner['uid']]); - $author = $doc->createElement('author'); - XML::addElement($doc, $author, 'id', $owner['url']); - if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) { - XML::addElement($doc, $author, 'activity:object-type', Activity\ObjectType::GROUP); - } else { - XML::addElement($doc, $author, 'activity:object-type', Activity\ObjectType::PERSON); - } - - XML::addElement($doc, $author, 'uri', $owner['url']); - XML::addElement($doc, $author, 'name', $owner['nick']); - XML::addElement($doc, $author, 'email', $owner['addr']); - if ($show_profile) { - XML::addElement($doc, $author, 'summary', BBCode::convertForUriId($owner['uri-id'], $owner['about'], BBCode::OSTATUS)); - } - - $attributes = [ - 'rel' => 'alternate', - 'type' => 'text/html', - 'href' => $owner['url'], - ]; - XML::addElement($doc, $author, 'link', '', $attributes); - - $attributes = [ - 'rel' => 'avatar', - 'type' => 'image/jpeg', // To-Do? - 'media:width' => Proxy::PIXEL_SMALL, - 'media:height' => Proxy::PIXEL_SMALL, - 'href' => User::getAvatarUrl($owner, Proxy::SIZE_SMALL), - ]; - XML::addElement($doc, $author, 'link', '', $attributes); - - if (isset($owner['thumb'])) { - $attributes = [ - 'rel' => 'avatar', - 'type' => 'image/jpeg', // To-Do? - 'media:width' => Proxy::PIXEL_THUMB, - 'media:height' => Proxy::PIXEL_THUMB, - 'href' => User::getAvatarUrl($owner, Proxy::SIZE_THUMB), - ]; - XML::addElement($doc, $author, 'link', '', $attributes); - } - - XML::addElement($doc, $author, 'poco:preferredUsername', $owner['nick']); - XML::addElement($doc, $author, 'poco:displayName', $owner['name']); - if ($show_profile) { - XML::addElement($doc, $author, 'poco:note', BBCode::convertForUriId($owner['uri-id'], $owner['about'], BBCode::OSTATUS)); - - if (trim($owner['location']) != '') { - $element = $doc->createElement('poco:address'); - XML::addElement($doc, $element, 'poco:formatted', $owner['location']); - $author->appendChild($element); - } - } - - if (DBA::isResult($profile) && !$show_profile) { - if (trim($profile['homepage']) != '') { - $urls = $doc->createElement('poco:urls'); - XML::addElement($doc, $urls, 'poco:type', 'homepage'); - XML::addElement($doc, $urls, 'poco:value', $profile['homepage']); - XML::addElement($doc, $urls, 'poco:primary', 'true'); - $author->appendChild($urls); - } - - XML::addElement($doc, $author, 'followers', '', ['url' => DI::baseUrl() . '/profile/' . $owner['nick'] . '/contacts/followers']); - XML::addElement($doc, $author, 'statusnet:profile_info', '', ['local_id' => $owner['uid']]); - - if ($profile['publish']) { - XML::addElement($doc, $author, 'mastodon:scope', 'public'); - } - } - - return $author; - } - - /** - * @TODO Picture attachments should look like this: - * https://status.pirati.ca/attachment/572819 - */ - - /** - * Returns the given activity if present - otherwise returns the "post" activity - * - * @param array $item Data of the item that is to be posted - * @return string activity - */ - public static function constructVerb(array $item): string - { - if (!empty($item['verb'])) { - return $item['verb']; - } - - return Activity::POST; - } - - /** - * Returns the given object type if present - otherwise returns the "note" object type - * - * @param array $item Data of the item that is to be posted - * @return string Object type - */ - private static function constructObjecttype(array $item): string - { - if (!empty($item['object-type']) && in_array($item['object-type'], [Activity\ObjectType::NOTE, Activity\ObjectType::COMMENT])) { - return $item['object-type']; - } - - return Activity\ObjectType::NOTE; - } - - /** - * Adds an entry element to the XML document - * - * @param DOMDocument $doc XML document - * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster - * @param bool $toplevel optional default false - * - * @return DOMElement Entry element - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function entry(DOMDocument $doc, array $item, array $owner, bool $toplevel = false): DOMElement - { - if ($item['verb'] == Activity::LIKE) { - return self::likeEntry($doc, $item, $owner, $toplevel); - } elseif (in_array($item['verb'], [Activity::FOLLOW, Activity::O_UNFOLLOW])) { - return self::followEntry($doc, $item, $owner, $toplevel); - } else { - return self::noteEntry($doc, $item, $owner, $toplevel); - } - } - - /** - * Adds an entry element with a "like" - * - * @param DOMDocument $doc XML document - * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster - * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? - * @return DOMElement Entry element with "like" - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function likeEntry(DOMDocument $doc, array $item, array $owner, bool $toplevel): DOMElement - { - if (($item['gravity'] != Item::GRAVITY_PARENT) && (Strings::normaliseLink($item['author-link']) != Strings::normaliseLink($owner['url']))) { - Logger::info('OStatus entry is from author ' . $owner['url'] . ' - not from ' . $item['author-link'] . '. Quitting.'); - } - - $entry = self::entryHeader($doc, $owner, $item, $toplevel); - - $verb = ActivityNamespace::ACTIVITY_SCHEMA . 'favorite'; - self::entryContent($doc, $entry, $item, $owner, 'Favorite', $verb, false); - - $parent = Post::selectFirst([], ['uri' => $item['thr-parent'], 'uid' => $item['uid']]); - if (DBA::isResult($parent)) { - $as_object = $doc->createElement('activity:object'); - - XML::addElement($doc, $as_object, 'activity:object-type', self::constructObjecttype($parent)); - - self::entryContent($doc, $as_object, $parent, $owner, 'New entry'); - - $entry->appendChild($as_object); - } - - self::entryFooter($doc, $entry, $item, $owner); - - return $entry; - } - - /** - * Adds the person object element to the XML document - * - * @param DOMDocument $doc XML document - * @param array $owner Contact data of the poster - * @param array $contact Contact data of the target - * @return DOMElement author element - */ - private static function addPersonObject(DOMDocument $doc, array $owner, array $contact): DOMElement - { - $object = $doc->createElement('activity:object'); - XML::addElement($doc, $object, 'activity:object-type', Activity\ObjectType::PERSON); - - if ($contact['network'] == Protocol::PHANTOM) { - XML::addElement($doc, $object, 'id', $contact['url']); - return $object; - } - - XML::addElement($doc, $object, 'id', $contact['alias']); - XML::addElement($doc, $object, 'title', $contact['nick']); - - XML::addElement($doc, $object, 'link', '', [ - 'rel' => 'alternate', - 'type' => 'text/html', - 'href' => $contact['url'], - ]); - - $attributes = [ - 'rel' => 'avatar', - 'type' => 'image/jpeg', // To-Do? - 'media:width' => 300, - 'media:height' => 300, - 'href' => $contact['photo'], - ]; - XML::addElement($doc, $object, 'link', '', $attributes); - - XML::addElement($doc, $object, 'poco:preferredUsername', $contact['nick']); - XML::addElement($doc, $object, 'poco:displayName', $contact['name']); - - if (trim($contact['location']) != '') { - $element = $doc->createElement('poco:address'); - XML::addElement($doc, $element, 'poco:formatted', $contact['location']); - $object->appendChild($element); - } - - return $object; - } - - /** - * Adds a follow/unfollow entry element - * - * @param DOMDocument $doc XML document - * @param array $item Data of the follow/unfollow message - * @param array $owner Contact data of the poster - * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? - * @return DOMElement Entry element - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function followEntry(DOMDocument $doc, array $item, array $owner, bool $toplevel): DOMElement - { - $item['id'] = $item['parent'] = 0; - $item['created'] = $item['edited'] = date('c'); - $item['private'] = Item::PRIVATE; - - $contact = Contact::getByURL($item['follow']); - $item['follow'] = $contact['url']; - - if ($contact['alias']) { - $item['follow'] = $contact['alias']; - } else { - $contact['alias'] = $contact['url']; - } - - $condition = ['uid' => $owner['uid'], 'nurl' => Strings::normaliseLink($contact['url'])]; - $user_contact = DBA::selectFirst('contact', ['id'], $condition); - - if (DBA::isResult($user_contact)) { - $connect_id = $user_contact['id']; - } else { - $connect_id = 0; - } - - if ($item['verb'] == Activity::FOLLOW) { - $message = DI::l10n()->t('%s is now following %s.'); - $title = DI::l10n()->t('following'); - $action = 'subscription'; - } else { - $message = DI::l10n()->t('%s stopped following %s.'); - $title = DI::l10n()->t('stopped following'); - $action = 'unfollow'; - } - - $item['uri'] = $item['parent-uri'] = $item['thr-parent'] - = 'tag:' . DI::baseUrl()->getHost() . - ','.date('Y-m-d').':'.$action.':'.$owner['uid']. - ':person:'.$connect_id.':'.$item['created']; - - $item['body'] = sprintf($message, $owner['nick'], $contact['nick']); - - $entry = self::entryHeader($doc, $owner, $item, $toplevel); - - self::entryContent($doc, $entry, $item, $owner, $title); - - $object = self::addPersonObject($doc, $owner, $contact); - $entry->appendChild($object); - - self::entryFooter($doc, $entry, $item, $owner); - - return $entry; - } - - /** - * Adds a regular entry element - * - * @param DOMDocument $doc XML document - * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster - * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? - * @return DOMElement Entry element - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - private static function noteEntry(DOMDocument $doc, array $item, array $owner, bool $toplevel): DOMElement - { - if (($item['gravity'] != Item::GRAVITY_PARENT) && (Strings::normaliseLink($item['author-link']) != Strings::normaliseLink($owner['url']))) { - Logger::info('OStatus entry is from author ' . $owner['url'] . ' - not from ' . $item['author-link'] . '. Quitting.'); - } - - if (!$toplevel) { - if (!empty($item['title'])) { - $title = BBCode::convertForUriId($item['uri-id'], $item['title'], BBCode::OSTATUS); - } else { - $title = sprintf('New note by %s', $owner['nick']); - } - } else { - $title = sprintf('New comment by %s', $owner['nick']); - } - - $entry = self::entryHeader($doc, $owner, $item, $toplevel); - - XML::addElement($doc, $entry, 'activity:object-type', Activity\ObjectType::NOTE); - - self::entryContent($doc, $entry, $item, $owner, $title, '', true); - - self::entryFooter($doc, $entry, $item, $owner, true); - - return $entry; - } - - /** - * Adds a header element to the XML document - * - * @param DOMDocument $doc XML document - * @param array $owner Contact data of the poster - * @param array $item - * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? - * @return DOMElement The entry element where the elements are added - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function entryHeader(DOMDocument $doc, array $owner, array $item, bool $toplevel): DOMElement - { - if (!$toplevel) { - $entry = $doc->createElement('entry'); - - if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) { - $entry->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY); - - $contact = Contact::getByURL($item['author-link']) ?: $owner; - $contact['nickname'] = $contact['nickname'] ?? $contact['nick']; - $author = self::addAuthor($doc, $contact, false); - $entry->appendChild($author); - } - } else { - $entry = $doc->createElementNS(ActivityNamespace::ATOM1, 'entry'); - - $entry->setAttribute('xmlns:thr', ActivityNamespace::THREAD); - $entry->setAttribute('xmlns:georss', ActivityNamespace::GEORSS); - $entry->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY); - $entry->setAttribute('xmlns:media', ActivityNamespace::MEDIA); - $entry->setAttribute('xmlns:poco', ActivityNamespace::POCO); - $entry->setAttribute('xmlns:ostatus', ActivityNamespace::OSTATUS); - $entry->setAttribute('xmlns:statusnet', ActivityNamespace::STATUSNET); - $entry->setAttribute('xmlns:mastodon', ActivityNamespace::MASTODON); - - $author = self::addAuthor($doc, $owner); - $entry->appendChild($author); - } - - return $entry; - } - - /** - * Adds elements to the XML document - * - * @param DOMDocument $doc XML document - * @param DOMElement $entry Entry element where the content is added - * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster - * @param string $title Title for the post - * @param string $verb The activity verb - * @param bool $complete Add the "status_net" element? - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - private static function entryContent(DOMDocument $doc, DOMElement $entry, array $item, array $owner, string $title, string $verb = '', bool $complete = true) - { - if ($verb == '') { - $verb = self::constructVerb($item); - } - - XML::addElement($doc, $entry, 'id', $item['uri']); - XML::addElement($doc, $entry, 'title', html_entity_decode($title, ENT_QUOTES, 'UTF-8')); - - $body = Post\Media::addAttachmentsToBody($item['uri-id'], DI::contentItem()->addSharedPost($item)); - $body = Post\Media::addHTMLLinkToBody($item['uri-id'], $body); - - if (!empty($item['title'])) { - $body = '[b]' . $item['title'] . "[/b]\n\n" . $body; - } - - $body = BBCode::convertForUriId($item['uri-id'], $body, BBCode::OSTATUS); - - XML::addElement($doc, $entry, 'content', $body, ['type' => 'html']); - - XML::addElement($doc, $entry, 'link', '', [ - 'rel' => 'alternate', - 'type' => 'text/html', - 'href' => DI::baseUrl() . '/display/' . $item['guid'], - ]); - - if ($complete && ($item['id'] > 0)) { - XML::addElement($doc, $entry, 'status_net', '', ['notice_id' => $item['id']]); - } - - XML::addElement($doc, $entry, 'activity:verb', $verb); - - XML::addElement($doc, $entry, 'published', DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM)); - XML::addElement($doc, $entry, 'updated', DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM)); - } - - /** - * Adds the elements at the foot of an entry to the XML document - * - * @param DOMDocument $doc XML document - * @param object $entry The entry element where the elements are added - * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster - * @param bool $complete default true - * @return void - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - private static function entryFooter(DOMDocument $doc, $entry, array $item, array $owner, bool $complete = true) - { - $mentioned = []; - - if ($item['gravity'] != Item::GRAVITY_PARENT) { - $parent = Post::selectFirst(['guid', 'author-link', 'owner-link'], ['id' => $item['parent']]); - - $thrparent = Post::selectFirst(['guid', 'author-link', 'owner-link', 'plink'], ['uid' => $owner['uid'], 'uri' => $item['thr-parent']]); - - if (DBA::isResult($thrparent)) { - $mentioned[$thrparent['author-link']] = $thrparent['author-link']; - $mentioned[$thrparent['owner-link']] = $thrparent['owner-link']; - $parent_plink = $thrparent['plink']; - } elseif (DBA::isResult($parent)) { - $mentioned[$parent['author-link']] = $parent['author-link']; - $mentioned[$parent['owner-link']] = $parent['owner-link']; - $parent_plink = DI::baseUrl() . '/display/' . $parent['guid']; - } else { - DI::logger()->notice('Missing parent and thr-parent for child item', ['item' => $item]); - } - - if (isset($parent_plink)) { - $attributes = [ - 'ref' => $item['thr-parent'], - 'href' => $parent_plink]; - XML::addElement($doc, $entry, 'thr:in-reply-to', '', $attributes); - - $attributes = [ - 'rel' => 'related', - 'href' => $parent_plink]; - XML::addElement($doc, $entry, 'link', '', $attributes); - } - } - - if (intval($item['parent']) > 0) { - $conversation_href = $conversation_uri = $item['conversation']; - - XML::addElement($doc, $entry, 'link', '', ['rel' => 'ostatus:conversation', 'href' => $conversation_href]); - - $attributes = [ - 'href' => $conversation_href, - 'local_id' => $item['parent'], - 'ref' => $conversation_uri, - ]; - - XML::addElement($doc, $entry, 'ostatus:conversation', $conversation_uri, $attributes); - } - - // uri-id isn't present for follow entry pseudo-items - $tags = Tag::getByURIId($item['uri-id'] ?? 0); - foreach ($tags as $tag) { - $mentioned[$tag['url']] = $tag['url']; - } - - // Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS) - $newmentions = []; - foreach ($mentioned as $mention) { - $newmentions[str_replace('http://', 'https://', $mention)] = str_replace('http://', 'https://', $mention); - $newmentions[str_replace('https://', 'http://', $mention)] = str_replace('https://', 'http://', $mention); - } - $mentioned = $newmentions; - - foreach ($mentioned as $mention) { - $contact = Contact::getByURL($mention, false, ['contact-type']); - if (!empty($contact) && ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) { - XML::addElement($doc, $entry, 'link', '', [ - 'rel' => 'mentioned', - 'ostatus:object-type' => Activity\ObjectType::GROUP, - 'href' => $mention, - ]); - } else { - XML::addElement($doc, $entry, 'link', '', [ - 'rel' => 'mentioned', - 'ostatus:object-type' => Activity\ObjectType::PERSON, - 'href' => $mention, - ]); - } - } - - if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) { - XML::addElement($doc, $entry, 'link', '', [ - 'rel' => 'mentioned', - 'ostatus:object-type' => 'http://activitystrea.ms/schema/1.0/group', - 'href' => $owner['url'] - ]); - } - - if ($item['private'] != Item::PRIVATE) { - XML::addElement($doc, $entry, 'link', '', ['rel' => 'ostatus:attention', - 'href' => 'http://activityschema.org/collection/public']); - XML::addElement($doc, $entry, 'link', '', ['rel' => 'mentioned', - 'ostatus:object-type' => 'http://activitystrea.ms/schema/1.0/collection', - 'href' => 'http://activityschema.org/collection/public']); - XML::addElement($doc, $entry, 'mastodon:scope', 'public'); - } - - foreach ($tags as $tag) { - if ($tag['type'] == Tag::HASHTAG) { - XML::addElement($doc, $entry, 'category', '', ['term' => $tag['name']]); - } - } - - self::getAttachment($doc, $entry, $item); - - if ($complete && ($item['id'] > 0)) { - $app = $item['app']; - if ($app == '') { - $app = 'web'; - } - - $attributes = ['local_id' => $item['id'], 'source' => $app]; - - if (isset($parent['id'])) { - $attributes['repeat_of'] = $parent['id']; - } - - if ($item['coord'] != '') { - XML::addElement($doc, $entry, 'georss:point', $item['coord']); - } - - XML::addElement($doc, $entry, 'statusnet:notice_info', '', $attributes); - } - } - - /** - * Creates the XML feed for a given nickname - * - * Supported filters: - * - activity (default): all the public posts - * - posts: all the public top-level posts - * - comments: all the public replies - * - * Updates the provided last_update parameter if the result comes from the - * cache or it is empty - * - * @param string $owner_nick Nickname of the feed owner - * @param string $last_update Date of the last update (in "Y-m-d H:i:s" format) - * @param integer $max_items Number of maximum items to fetch - * @param string $filter Feed items filter (activity, posts or comments) - * @param boolean $nocache Wether to bypass caching - * @return string XML feed or empty string on error - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function feed(string $owner_nick, string &$last_update, int $max_items = 300, string $filter = 'activity', bool $nocache = false): string - { - $stamp = microtime(true); - - $owner = User::getOwnerDataByNick($owner_nick); - if (!$owner) { - return ''; - } - - $cachekey = 'ostatus:feed:' . $owner_nick . ':' . $filter . ':' . $last_update; - - $previous_created = $last_update; - - // Don't cache when the last item was posted less than 15 minutes ago (Cache duration) - if ((time() - strtotime($owner['last-item'])) < 15*60) { - $result = DI::cache()->get($cachekey); - if (!$nocache && !is_null($result)) { - Logger::info('Feed duration: ' . number_format(microtime(true) - $stamp, 3) . ' - ' . $owner_nick . ' - ' . $filter . ' - ' . $previous_created . ' (cached)'); - $last_update = $result['last_update']; - return $result['feed']; - } - } - - if (!strlen($last_update)) { - $last_update = 'now -30 days'; - } - - $check_date = DateTimeFormat::utc($last_update); - $authorid = Contact::getIdForURL($owner['url']); - - $condition = [ - "`uid` = ? AND `received` > ? AND NOT `deleted` AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?)", - $owner['uid'], - $check_date, - Item::PRIVATE, - Protocol::OSTATUS, - Protocol::DFRN, - ]; - - if ($filter === 'comments') { - $condition[0] .= " AND `object-type` = ? "; - $condition[] = Activity\ObjectType::COMMENT; - } - - if ($owner['contact-type'] != Contact::TYPE_COMMUNITY) { - $condition[0] .= " AND `contact-id` = ? AND `author-id` = ?"; - $condition[] = $owner['id']; - $condition[] = $authorid; - } - - $params = ['order' => ['received' => true], 'limit' => $max_items]; - - if ($filter === 'posts') { - $ret = Post::selectThread([], $condition, $params); - } else { - $ret = Post::select([], $condition, $params); - } - - $items = Post::toArray($ret); - - $doc = new DOMDocument('1.0', 'utf-8'); - $doc->formatOutput = true; - - $root = self::addHeader($doc, $owner, $filter); - - foreach ($items as $item) { - if (DI::config()->get('system', 'ostatus_debug')) { - $item['body'] .= '🍼'; - } - - if (in_array($item['verb'], [Activity::FOLLOW, Activity::O_UNFOLLOW, Activity::LIKE])) { - continue; - } - - $entry = self::entry($doc, $item, $owner, false); - $root->appendChild($entry); - - if ($last_update < $item['created']) { - $last_update = $item['created']; - } - } - - $feeddata = trim($doc->saveXML()); - - $msg = ['feed' => $feeddata, 'last_update' => $last_update]; - DI::cache()->set($cachekey, $msg, Duration::QUARTER_HOUR); - - Logger::info('Feed duration: ' . number_format(microtime(true) - $stamp, 3) . ' - ' . $owner_nick . ' - ' . $filter . ' - ' . $previous_created); - - return $feeddata; - } - - /** - * Creates the XML for a salmon message - * - * @param array $item Data of the item that is to be posted - * @param array $owner Contact data of the poster - * - * @return string XML for the salmon - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function salmon(array $item, array $owner): string - { - $doc = new DOMDocument('1.0', 'utf-8'); - $doc->formatOutput = true; - - if (DI::config()->get('system', 'ostatus_debug')) { - $item['body'] .= '🐟'; - } - - $entry = self::entry($doc, $item, $owner, true); - - $doc->appendChild($entry); - - return trim($doc->saveXML()); - } - - /** - * Checks if the given contact url does support OStatus - * - * @param string $url profile url - * @return boolean - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function isSupportedByContactUrl(string $url): bool - { - $probe = Probe::uri($url, Protocol::OSTATUS); - return $probe['network'] == Protocol::OSTATUS; - } -} diff --git a/src/Protocol/Relay.php b/src/Protocol/Relay.php index e7e878a02d..b2396ba288 100644 --- a/src/Protocol/Relay.php +++ b/src/Protocol/Relay.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; +use Exception; use Friendica\Content\Smilies; use Friendica\Content\Text\BBCode; -use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; +use Friendica\Core\L10n; use Friendica\Core\Protocol; use Friendica\Database\DBA; use Friendica\DI; @@ -47,7 +33,7 @@ use Friendica\Util\Strings; class Relay { const SCOPE_NONE = ''; - const SCOPE_ALL = 'all'; + const SCOPE_ALL = 'all'; const SCOPE_TAGS = 'tags'; /** @@ -67,86 +53,90 @@ class Relay $config = DI::config(); if (Contact::hasFollowers($authorid)) { - Logger::info('Author has got followers on this server - accepted', ['network' => $network, 'url' => $url, 'author' => $authorid, 'tags' => $tags]); + DI::logger()->info('Author has got followers on this server - accepted', ['network' => $network, 'url' => $url, 'author' => $authorid, 'tags' => $tags]); return true; } $scope = $config->get('system', 'relay_scope'); if ($scope == self::SCOPE_NONE) { - Logger::info('Server does not accept relay posts - rejected', ['network' => $network, 'url' => $url]); + DI::logger()->info('Server does not accept relay posts - rejected', ['network' => $network, 'url' => $url]); return false; } if (Contact::isBlocked($authorid)) { - Logger::info('Author is blocked - rejected', ['author' => $authorid, 'network' => $network, 'url' => $url]); + DI::logger()->info('Author is blocked - rejected', ['author' => $authorid, 'network' => $network, 'url' => $url]); return false; } if (Contact::isHidden($authorid)) { - Logger::info('Author is hidden - rejected', ['author' => $authorid, 'network' => $network, 'url' => $url]); + DI::logger()->info('Author is hidden - rejected', ['author' => $authorid, 'network' => $network, 'url' => $url]); return false; } if (!empty($causerid)) { $contact = Contact::getById($causerid, ['url']); - $causer = $contact['url'] ?? ''; + $causer = $contact['url'] ?? ''; } else { $causer = ''; } $body = ActivityPub\Processor::normalizeMentionLinks($body); - $denyTags = []; - if ($scope == self::SCOPE_TAGS) { $tagList = self::getSubscribedTags(); } else { - $tagList = []; + $tagList = []; } - $deny_tags = $config->get('system', 'relay_deny_tags'); - $tagitems = explode(',', mb_strtolower($deny_tags)); - foreach ($tagitems as $tag) { - $tag = trim($tag, '# '); - $denyTags[] = $tag; - } + $denyTags = Strings::getTagArrayByString($config->get('system', 'relay_deny_tags')); if (!empty($tagList) || !empty($denyTags)) { $content = mb_strtolower(BBCode::toPlaintext($body, false)); + $max_tags = $config->get('system', 'relay_max_tags'); + if ($max_tags && (count($tags) > $max_tags) && preg_match('/[^@!#]\[url\=.*?\].*?\[\/url\]/ism', $body)) { + $cleaned = preg_replace('/[@!#]\[url\=.*?\].*?\[\/url\]/ism', '', $body); + $content_cleaned = mb_strtolower(BBCode::toPlaintext($cleaned, false)); + + if (strlen($content_cleaned) < strlen($content) / 2) { + DI::logger()->info('Possible hashtag spam detected - rejected', ['hashtags' => $tags, 'network' => $network, 'url' => $url, 'causer' => $causer, 'content' => $content]); + return false; + } + } + foreach ($tags as $tag) { $tag = mb_strtolower($tag); if (in_array($tag, $denyTags)) { - Logger::info('Unwanted hashtag found - rejected', ['hashtag' => $tag, 'network' => $network, 'url' => $url, 'causer' => $causer]); + DI::logger()->info('Unwanted hashtag found - rejected', ['hashtag' => $tag, 'network' => $network, 'url' => $url, 'causer' => $causer]); return false; } if (in_array($tag, $tagList)) { - Logger::info('Subscribed hashtag found - accepted', ['hashtag' => $tag, 'network' => $network, 'url' => $url, 'causer' => $causer]); + DI::logger()->info('Subscribed hashtag found - accepted', ['hashtag' => $tag, 'network' => $network, 'url' => $url, 'causer' => $causer]); return true; } // We check with "strpos" for performance issues. Only when this is true, the regular expression check is used // RegExp is taken from here: https://medium.com/@shiba1014/regex-word-boundaries-with-unicode-207794f6e7ed if ((strpos($content, $tag) !== false) && preg_match('/(?<=[\s,.:;"\']|^)' . preg_quote($tag, '/') . '(?=[\s,.:;"\']|$)/', $content)) { - Logger::info('Subscribed hashtag found in content - accepted', ['hashtag' => $tag, 'network' => $network, 'url' => $url, 'causer' => $causer]); + DI::logger()->info('Subscribed hashtag found in content - accepted', ['hashtag' => $tag, 'network' => $network, 'url' => $url, 'causer' => $causer]); return true; } } } if (!self::isWantedLanguage($body, 0, $authorid, $languages)) { - Logger::info('Unwanted or Undetected language found - rejected', ['network' => $network, 'url' => $url, 'causer' => $causer, 'tags' => $tags]); + DI::logger()->info('Unwanted or Undetected language found - rejected', ['network' => $network, 'url' => $url, 'causer' => $causer, 'tags' => $tags]); return false; } if ($scope == self::SCOPE_ALL) { - Logger::info('Server accept all posts - accepted', ['network' => $network, 'url' => $url, 'causer' => $causer, 'tags' => $tags]); + DI::logger()->info('Server accept all posts - accepted', ['network' => $network, 'url' => $url, 'causer' => $causer, 'tags' => $tags]); return true; } - Logger::info('No matching hashtags found - rejected', ['network' => $network, 'url' => $url, 'causer' => $causer, 'tags' => $tags]); + DI::logger()->info('No matching hashtags found - rejected', ['network' => $network, 'url' => $url, 'causer' => $causer, 'tags' => $tags]); return false; } @@ -157,20 +147,13 @@ class Relay */ public static function getSubscribedTags(): array { - $systemTags = []; - $server_tags = DI::config()->get('system', 'relay_server_tags'); - - foreach (explode(',', mb_strtolower($server_tags)) as $tag) { - $systemTags[] = trim($tag, '# '); - } + $tags = Strings::getTagArrayByString(DI::config()->get('system', 'relay_server_tags')); if (DI::config()->get('system', 'relay_user_tags')) { - $userTags = Search::getUserTags(); - } else { - $userTags = []; + $tags = array_merge($tags, Search::getUserTags()); } - return array_unique(array_merge($systemTags, $userTags)); + return array_unique($tags); } /** @@ -192,34 +175,31 @@ class Relay } } - if (empty($languages) && empty($detected) && (empty($body) || Smilies::isEmojiPost($body))) { - Logger::debug('Empty body or only emojis', ['body' => $body]); + if (empty($detected) && empty($languages)) { + $detected = [L10n::UNDETERMINED_LANGUAGE]; + } + + if (empty($body) || Smilies::isEmojiPost($body)) { + DI::logger()->debug('Empty body or only emojis', ['body' => $body]); return true; } - if (!empty($languages) || !empty($detected)) { - $user_languages = User::getLanguages(); + $user_languages = User::getLanguages(); - foreach ($detected as $language) { - if (in_array($language, $user_languages)) { - Logger::debug('Wanted language found in detected languages', ['language' => $language, 'detected' => $detected, 'userlang' => $user_languages, 'body' => $body]); - return true; - } + foreach ($detected as $language) { + if (in_array($language, $user_languages)) { + DI::logger()->debug('Wanted language found in detected languages', ['language' => $language, 'detected' => $detected, 'userlang' => $user_languages, 'body' => $body]); + return true; } - foreach ($languages as $language) { - if (in_array($language, $user_languages)) { - Logger::debug('Wanted language found in defined languages', ['language' => $language, 'languages' => $languages, 'detected' => $detected, 'userlang' => $user_languages, 'body' => $body]); - return true; - } - } - Logger::debug('No wanted language found', ['languages' => $languages, 'detected' => $detected, 'userlang' => $user_languages, 'body' => $body]); - return false; - } elseif (DI::config()->get('system', 'relay_deny_undetected_language')) { - Logger::info('Undetected language found', ['body' => $body]); - return false; } - - return true; + foreach ($languages as $language) { + if (in_array($language, $user_languages)) { + DI::logger()->debug('Wanted language found in defined languages', ['language' => $language, 'languages' => $languages, 'detected' => $detected, 'userlang' => $user_languages, 'body' => $body]); + return true; + } + } + DI::logger()->debug('No wanted language found', ['languages' => $languages, 'detected' => $detected, 'userlang' => $user_languages, 'body' => $body]); + return false; } /** @@ -235,47 +215,49 @@ class Relay if (in_array($gserver['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) { $system = APContact::getByURL($gserver['url'] . '/friendica'); if (!empty($system['sharedinbox'])) { - Logger::info('Successfully probed for relay contact', ['server' => $gserver['url']]); + DI::logger()->info('Successfully probed for relay contact', ['server' => $gserver['url']]); $id = Contact::updateFromProbeByURL($system['url']); - Logger::info('Updated relay contact', ['server' => $gserver['url'], 'id' => $id]); + DI::logger()->info('Updated relay contact', ['server' => $gserver['url'], 'id' => $id]); return; } } $condition = ['uid' => 0, 'gsid' => $gserver['id'], 'contact-type' => Contact::TYPE_RELAY]; - $old = DBA::selectFirst('contact', [], $condition); + $old = DBA::selectFirst('contact', [], $condition); if (!DBA::isResult($old)) { $condition = ['uid' => 0, 'nurl' => Strings::normaliseLink($gserver['url'])]; - $old = DBA::selectFirst('contact', [], $condition); + $old = DBA::selectFirst('contact', [], $condition); if (DBA::isResult($old)) { - $fields['gsid'] = $gserver['id']; + $fields['gsid'] = $gserver['id']; $fields['contact-type'] = Contact::TYPE_RELAY; - Logger::info('Assigning missing data for relay contact', ['server' => $gserver['url'], 'id' => $old['id']]); + DI::logger()->info('Assigning missing data for relay contact', ['server' => $gserver['url'], 'id' => $old['id']]); } - } elseif (empty($fields)) { - Logger::info('No content to update, quitting', ['server' => $gserver['url']]); + } elseif (empty($fields) && $old['unsearchable']) { + DI::logger()->info('No content to update, quitting', ['server' => $gserver['url']]); return; } if (DBA::isResult($old)) { - $fields['updated'] = DateTimeFormat::utcNow(); + $fields['updated'] = DateTimeFormat::utcNow(); + $fields['unsearchable'] = true; - Logger::info('Update relay contact', ['server' => $gserver['url'], 'id' => $old['id'], 'fields' => $fields]); + DI::logger()->info('Update relay contact', ['server' => $gserver['url'], 'id' => $old['id'], 'fields' => $fields]); Contact::update($fields, ['id' => $old['id']], $old); } else { $default = ['created' => DateTimeFormat::utcNow(), - 'name' => 'relay', 'nick' => 'relay', 'url' => $gserver['url'], - 'nurl' => Strings::normaliseLink($gserver['url']), - 'network' => Protocol::DIASPORA, 'uid' => 0, - 'batch' => $gserver['url'] . '/receive/public', - 'rel' => Contact::FOLLOWER, 'blocked' => false, - 'pending' => false, 'writable' => true, - 'gsid' => $gserver['id'], - 'baseurl' => $gserver['url'], 'contact-type' => Contact::TYPE_RELAY]; + 'name' => 'relay', 'nick' => 'relay', 'url' => $gserver['url'], + 'nurl' => Strings::normaliseLink($gserver['url']), + 'network' => Protocol::DIASPORA, 'uid' => 0, + 'batch' => $gserver['url'] . '/receive/public', + 'rel' => Contact::FOLLOWER, 'blocked' => false, + 'pending' => false, 'writable' => true, + 'gsid' => $gserver['id'], + 'unsearchable' => true, + 'baseurl' => $gserver['url'], 'contact-type' => Contact::TYPE_RELAY]; $fields = array_merge($default, $fields); - Logger::info('Create relay contact', ['server' => $gserver['url'], 'fields' => $fields]); + DI::logger()->info('Create relay contact', ['server' => $gserver['url'], 'fields' => $fields]); Contact::insert($fields); } } @@ -296,19 +278,19 @@ class Relay $relay_contact = $contact; } elseif (empty($contact['baseurl'])) { if (!empty($contact['batch'])) { - $condition = ['uid' => 0, 'network' => Protocol::FEDERATED, 'batch' => $contact['batch'], 'contact-type' => Contact::TYPE_RELAY]; + $condition = ['uid' => 0, 'network' => Protocol::FEDERATED, 'batch' => $contact['batch'], 'contact-type' => Contact::TYPE_RELAY]; $relay_contact = DBA::selectFirst('contact', [], $condition); } else { return; } } else { $gserver = ['id' => $contact['gsid'] ?: GServer::getID($contact['baseurl'], true), - 'url' => $contact['baseurl'], 'network' => $contact['network']]; + 'url' => $contact['baseurl'], 'network' => $contact['network']]; $relay_contact = self::getContact($gserver, []); } if (!empty($relay_contact)) { - Logger::info('Relay contact will be marked for archival', ['id' => $relay_contact['id'], 'url' => $relay_contact['url']]); + DI::logger()->info('Relay contact will be marked for archival', ['id' => $relay_contact['id'], 'url' => $relay_contact['url']]); Contact::markForArchival($relay_contact); } } @@ -317,8 +299,6 @@ class Relay * Return a list of servers that we serve via the direct relay * * @param integer $item_id id of the item that is sent - * @param array $contacts Previously fetched contacts - * @param array $networks Networks of the relay servers * @return array of relay servers * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ @@ -344,7 +324,7 @@ class Relay DBA::close($servers); // All tags of the current post - $tags = DBA::select('tag-view', ['name'], ['uri-id' => $parent['uri-id'], 'type' => Tag::HASHTAG]); + $tags = DBA::select('tag-view', ['name'], ['uri-id' => $parent['uri-id'], 'type' => Tag::HASHTAG]); $taglist = []; while ($tag = DBA::fetch($tags)) { $taglist[] = $tag['name']; @@ -396,8 +376,11 @@ class Relay */ public static function getList(array $fields = []): array { - return DBA::selectToArray('apcontact', $fields, - ["`type` IN (?, ?) AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` = ?)", 'Application', 'Service', 0, Contact::FRIEND]); + return DBA::selectToArray( + 'apcontact', + $fields, + ["`type` IN (?, ?) AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` = ?)", 'Application', 'Service', 0, Contact::FRIEND] + ); } /** @@ -412,7 +395,7 @@ class Relay { // Fetch the relay contact $condition = ['uid' => 0, 'gsid' => $gserver['id'], 'contact-type' => Contact::TYPE_RELAY]; - $contact = DBA::selectFirst('contact', $fields, $condition); + $contact = DBA::selectFirst('contact', $fields, $condition); if (DBA::isResult($contact)) { if ($contact['archive'] || $contact['blocked']) { return false; @@ -440,7 +423,7 @@ class Relay { foreach (self::getList() as $server) { $success = ActivityPub\Transmitter::sendRelayFollow($server['url']); - Logger::debug('Resubscribed', ['profile' => $server['url'], 'success' => $success]); + DI::logger()->debug('Resubscribed', ['profile' => $server['url'], 'success' => $success]); } } } diff --git a/src/Protocol/Salmon.php b/src/Protocol/Salmon.php index 5047375ea0..5d8673e8a6 100644 --- a/src/Protocol/Salmon.php +++ b/src/Protocol/Salmon.php @@ -1,34 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; -use Friendica\Core\Logger; -use Friendica\DI; -use Friendica\Network\HTTPClient\Client\HttpClientAccept; -use Friendica\Network\Probe; use Friendica\Protocol\Salmon\Format\Magic; -use Friendica\Util\Crypto; -use Friendica\Util\Strings; -use Friendica\Util\XML; use phpseclib3\Crypt\PublicKeyLoader; /** @@ -39,201 +18,6 @@ use phpseclib3\Crypt\PublicKeyLoader; */ class Salmon { - /** - * @param string $uri Uniform Resource Identifier - * @param string $keyhash encoded key - * @return string Key or empty string on any errors - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function getKey(string $uri, string $keyhash): string - { - $ret = []; - - Logger::info('Fetching salmon key for '.$uri); - - $arr = Probe::lrdd($uri); - - if (is_array($arr)) { - foreach ($arr as $a) { - if ($a['@attributes']['rel'] === 'magic-public-key') { - $ret[] = $a['@attributes']['href']; - } - } - } else { - return ''; - } - - // We have found at least one key URL - // If it's inline, parse it - otherwise get the key - - if (count($ret) > 0) { - for ($x = 0; $x < count($ret); $x ++) { - if (substr($ret[$x], 0, 5) === 'data:') { - if (strstr($ret[$x], ',')) { - $ret[$x] = substr($ret[$x], strpos($ret[$x], ',') + 1); - } else { - $ret[$x] = substr($ret[$x], 5); - } - } elseif (Strings::normaliseLink($ret[$x]) == 'http://') { - $ret[$x] = DI::httpClient()->fetch($ret[$x], HttpClientAccept::MAGIC_KEY); - Logger::debug('Fetched public key', ['url' => $ret[$x]]); - } - } - } - - - Logger::notice('Key located', ['ret' => $ret]); - - if (count($ret) == 1) { - /* We only found one key so we don't care if the hash matches. - * If it's the wrong key we'll find out soon enough because - * message verification will fail. This also covers some older - * software which don't supply a keyhash. As long as they only - * have one key we'll be right. - */ - return (string) $ret[0]; - } else { - foreach ($ret as $a) { - $hash = Strings::base64UrlEncode(hash('sha256', $a)); - if ($hash == $keyhash) { - return $a; - } - } - } - - return ''; - } - - /** - * @param array $owner owner - * @param string $url url - * @param string $slap slap - * @return integer - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function slapper(array $owner, string $url, string $slap): int - { - // does contact have a salmon endpoint? - - if (!strlen($url)) { - return -1; - } - - if (!$owner['sprvkey']) { - Logger::notice(sprintf("user '%s' (%d) does not have a salmon private key. Send failed.", - $owner['name'], $owner['uid'])); - return -1; - } - - Logger::info('slapper called for '.$url.'. Data: ' . $slap); - - // create a magic envelope - - $data = Strings::base64UrlEncode($slap); - $data_type = 'application/atom+xml'; - $encoding = 'base64url'; - $algorithm = 'RSA-SHA256'; - $keyhash = Strings::base64UrlEncode(hash('sha256', self::salmonKey($owner['spubkey'])), true); - - $precomputed = '.' . Strings::base64UrlEncode($data_type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($algorithm); - - // GNU Social format - $signature = Strings::base64UrlEncode(Crypto::rsaSign($data . $precomputed, $owner['sprvkey'])); - - // Compliant format - $signature2 = Strings::base64UrlEncode(Crypto::rsaSign(str_replace('=', '', $data . $precomputed), $owner['sprvkey'])); - - // Old Status.net format - $signature3 = Strings::base64UrlEncode(Crypto::rsaSign($data, $owner['sprvkey'])); - - // At first try the non compliant method that works for GNU Social - $xmldata = [ - 'me:env' => [ - 'me:data' => $data, - '@attributes' => ['type' => $data_type], - 'me:encoding' => $encoding, - 'me:alg' => $algorithm, - 'me:sig' => $signature, - '@attributes2' => ['key_id' => $keyhash], - ] - ]; - - $namespaces = ['me' => ActivityNamespace::SALMON_ME]; - - $salmon = XML::fromArray($xmldata, $dummy, false, $namespaces); - - // slap them - $postResult = DI::httpClient()->post($url, $salmon, [ - 'Content-type' => 'application/magic-envelope+xml', - 'Content-length' => strlen($salmon), - ]); - - $return_code = $postResult->getReturnCode(); - - // check for success, e.g. 2xx - - if ($return_code > 299) { - Logger::notice('GNU Social salmon failed. Falling back to compliant mode'); - - // Now try the compliant mode that normally isn't used for GNU Social - $xmldata = [ - 'me:env' => [ - 'me:data' => $data, - '@attributes' => ['type' => $data_type], - 'me:encoding' => $encoding, - 'me:alg' => $algorithm, - 'me:sig' => $signature2, - '@attributes2' => ['key_id' => $keyhash] - ] - ]; - - $salmon = XML::fromArray($xmldata, $dummy, false, $namespaces); - - // slap them - $postResult = DI::httpClient()->post($url, $salmon, [ - 'Content-type' => 'application/magic-envelope+xml', - 'Content-length' => strlen($salmon), - ]); - $return_code = $postResult->getReturnCode(); - } - - if ($return_code > 299) { - Logger::notice('compliant salmon failed. Falling back to old status.net'); - - // Last try. This will most likely fail as well. - $xmldata = [ - 'me:env' => [ - 'me:data' => $data, - '@attributes' => ['type' => $data_type], - 'me:encoding' => $encoding, - 'me:alg' => $algorithm, - 'me:sig' => $signature3, - '@attributes2' => ['key_id' => $keyhash], - ] - ]; - - $salmon = XML::fromArray($xmldata, $dummy, false, $namespaces); - - // slap them - $postResult = DI::httpClient()->post($url, $salmon, [ - 'Content-type' => 'application/magic-envelope+xml', - 'Content-length' => strlen($salmon)]); - $return_code = $postResult->getReturnCode(); - } - - Logger::info('slapper for '.$url.' returned ' . $return_code); - - if (! $return_code) { - return -1; - } - - if (($return_code == 503) && $postResult->inHeader('retry-after')) { - return -1; - } - - return (($return_code >= 200) && ($return_code < 300)) ? 0 : 1; - } - /** * @param string $pubkey public key * @return string diff --git a/src/Protocol/Salmon/Format/Magic.php b/src/Protocol/Salmon/Format/Magic.php index 9d5ebb4aab..c8e812739e 100644 --- a/src/Protocol/Salmon/Format/Magic.php +++ b/src/Protocol/Salmon/Format/Magic.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol\Salmon\Format; diff --git a/src/Protocol/WebFingerUri.php b/src/Protocol/WebFingerUri.php index 6d3db41e14..23ae4cfa3b 100644 --- a/src/Protocol/WebFingerUri.php +++ b/src/Protocol/WebFingerUri.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Protocol; diff --git a/src/Protocol/ZOT.php b/src/Protocol/ZOT.php new file mode 100644 index 0000000000..28fa493454 --- /dev/null +++ b/src/Protocol/ZOT.php @@ -0,0 +1,69 @@ +debug('Is ZOT request', ['accept' => $_SERVER['HTTP_ACCEPT'], 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '']); + return true; + } + + return false; + } + + /** + * Get information about this site + * + * @return array + */ + public static function getSiteInfo(): array + { + $baseUrl = (string) DI::baseUrl(); + $keyValue = DI::keyValue(); + $addonHelper = DI::addonHelper(); + $config = DI::config(); + + $policies = [ + Module\Register::OPEN => 'open', + Module\Register::APPROVE => 'approve', + Module\Register::CLOSED => 'closed', + ]; + + return [ + 'url' => $baseUrl, + 'openWebAuth' => $baseUrl . '/owa', + 'authRedirect' => $baseUrl . '/magic', + 'register_policy' => $policies[Register::getPolicy()], + 'accounts' => $keyValue->get('nodeinfo_total_users'), + 'plugins' => $addonHelper->getVisibleEnabledAddons(), + 'sitename' => $config->get('config', 'sitename'), + 'about' => $config->get('config', 'info'), + 'project' => App::PLATFORM, + 'version' => App::VERSION, + ]; + } +} diff --git a/src/Render/FriendicaSmarty.php b/src/Render/FriendicaSmarty.php index 1df1ffe4a8..e99632cba1 100644 --- a/src/Render/FriendicaSmarty.php +++ b/src/Render/FriendicaSmarty.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Render; diff --git a/src/Render/FriendicaSmartyEngine.php b/src/Render/FriendicaSmartyEngine.php index 5d7ed6c0c9..6b57b58d1d 100644 --- a/src/Render/FriendicaSmartyEngine.php +++ b/src/Render/FriendicaSmartyEngine.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Render; @@ -55,7 +41,7 @@ final class FriendicaSmartyEngine extends TemplateEngine if (!is_writable($work_dir)) { $admin_message = DI::l10n()->t('The folder %s must be writable by webserver.', $work_dir); DI::logger()->critical($admin_message); - $message = DI::app()->isSiteAdmin() ? + $message = DI::userSession()->isSiteAdmin() ? $admin_message : DI::l10n()->t('Friendica can\'t display this page at the moment, please contact the administrator.'); throw new ServiceUnavailableException($message); diff --git a/src/Render/TemplateEngine.php b/src/Render/TemplateEngine.php index 2e8f0021f4..dc3da174f7 100644 --- a/src/Render/TemplateEngine.php +++ b/src/Render/TemplateEngine.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Render; diff --git a/src/Security/Authentication.php b/src/Security/Authentication.php index 91963ee399..2f383cfefb 100644 --- a/src/Security/Authentication.php +++ b/src/Security/Authentication.php @@ -1,28 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security; use Exception; -use Friendica\App; +use Friendica\App\BaseURL; +use Friendica\App\Mode; +use Friendica\App\Request; +use Friendica\AppHelper; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Core\Hook; @@ -34,12 +23,13 @@ use Friendica\DI; use Friendica\Model\User; use Friendica\Network\HTTPException; use Friendica\Security\TwoFactor\Repository\TrustedBrowser; -use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use LightOpenID; use Friendica\Core\L10n; use Friendica\Core\Worker; use Friendica\Model\Contact; +use Friendica\Model\User\Cookie; +use Friendica\Util\Strings; use Psr\Log\LoggerInterface; /** @@ -49,9 +39,9 @@ class Authentication { /** @var IManageConfigValues */ private $config; - /** @var App\Mode */ + /** @var Mode */ private $mode; - /** @var App\BaseURL */ + /** @var BaseURL */ private $baseUrl; /** @var L10n */ private $l10n; @@ -59,12 +49,14 @@ class Authentication private $dba; /** @var LoggerInterface */ private $logger; - /** @var User\Cookie */ + /** @var Cookie */ private $cookie; /** @var IHandleUserSessions */ private $session; /** @var IManagePersonalConfigValues */ private $pConfig; + /** @var AppHelper */ + private $appHelper; /** @var string */ private $remoteAddress; @@ -84,18 +76,29 @@ class Authentication * Authentication constructor. * * @param IManageConfigValues $config - * @param App\Mode $mode - * @param App\BaseURL $baseUrl + * @param Mode $mode + * @param BaseURL $baseUrl * @param L10n $l10n * @param Database $dba * @param LoggerInterface $logger - * @param User\Cookie $cookie + * @param Cookie $cookie * @param IHandleUserSessions $session * @param IManagePersonalConfigValues $pConfig - * @param App\Request $request + * @param Request $request */ - public function __construct(IManageConfigValues $config, App\Mode $mode, App\BaseURL $baseUrl, L10n $l10n, Database $dba, LoggerInterface $logger, User\Cookie $cookie, IHandleUserSessions $session, IManagePersonalConfigValues $pConfig, App\Request $request) - { + public function __construct( + IManageConfigValues $config, + Mode $mode, + BaseURL $baseUrl, + L10n $l10n, + Database $dba, + LoggerInterface $logger, + Cookie $cookie, + IHandleUserSessions $session, + IManagePersonalConfigValues $pConfig, + AppHelper $appHelper, + Request $request + ) { $this->config = $config; $this->mode = $mode; $this->baseUrl = $baseUrl; @@ -105,18 +108,17 @@ class Authentication $this->cookie = $cookie; $this->session = $session; $this->pConfig = $pConfig; + $this->appHelper = $appHelper; $this->remoteAddress = $request->getRemoteAddress(); } /** * Tries to auth the user from the cookie or session * - * @param App $a The Friendica Application context - * * @throws HttpException\InternalServerErrorException In case of Friendica internal exceptions * @throws Exception In case of general exceptions (like SQL Grammar) */ - public function withSession(App $a) + public function withSession() { // When the "Friendica" cookie is set, take the value to authenticate and renew the cookie. if ($this->cookie->get('uid')) { @@ -146,8 +148,8 @@ class Authentication $this->cookie->send(); // Do the authentication if not done by now - if (!$this->session->get('authenticated')) { - $this->setForUser($a, $user); + if (!$this->session->isAuthenticated()) { + $this->setForUser($user); if ($this->config->get('system', 'paranoia')) { $this->session->set('addr', $this->cookie->get('ip')); @@ -156,57 +158,44 @@ class Authentication } } - if ($this->session->get('authenticated')) { - if ($this->session->get('visitor_id') && !$this->session->get('uid')) { - $contact = $this->dba->selectFirst('contact', ['id'], ['id' => $this->session->get('visitor_id')]); - if ($this->dba->isResult($contact)) { - $a->setContactId($contact['id']); - } + if ($this->session->isVisitor()) { + $contact = $this->dba->selectFirst('contact', ['id'], ['id' => $this->session->get('visitor_id')]); + if ($this->dba->isResult($contact)) { + $this->appHelper->setContactId($contact['id']); } + } - if ($this->session->get('uid')) { - // already logged in user returning - $check = $this->config->get('system', 'paranoia'); - // extra paranoia - if the IP changed, log them out - if ($check && ($this->session->get('addr') != $this->remoteAddress)) { - $this->logger->notice('Session address changed. Paranoid setting in effect, blocking session. ', [ - 'addr' => $this->session->get('addr'), - 'remote_addr' => $this->remoteAddress - ] - ); - $this->session->clear(); - $this->baseUrl->redirect(); - } - - $user = $this->dba->selectFirst( - 'user', - [], - [ - 'uid' => $this->session->get('uid'), - 'blocked' => false, - 'account_expired' => false, - 'account_removed' => false, - 'verified' => true, - ] + if ($this->session->isAuthenticated()) { + // already logged in user returning + $check = $this->config->get('system', 'paranoia'); + // extra paranoia - if the IP changed, log them out + if ($check && ($this->session->get('addr') != $this->remoteAddress)) { + $this->logger->notice('Session address changed. Paranoid setting in effect, blocking session. ', [ + 'addr' => $this->session->get('addr'), + 'remote_addr' => $this->remoteAddress + ] ); - if (!$this->dba->isResult($user)) { - $this->session->clear(); - $this->baseUrl->redirect(); - } - - // Make sure to refresh the last login time for the user if the user - // stays logged in for a long time, e.g. with "Remember Me" - $login_refresh = false; - if (!$this->session->get('last_login_date')) { - $this->session->set('last_login_date', DateTimeFormat::utcNow()); - } - if (strcmp(DateTimeFormat::utc('now - 12 hours'), $this->session->get('last_login_date')) > 0) { - $this->session->set('last_login_date', DateTimeFormat::utcNow()); - $login_refresh = true; - } - - $this->setForUser($a, $user, false, false, $login_refresh); + $this->session->clear(); + $this->baseUrl->redirect(); } + + $user = $this->dba->selectFirst( + 'user', + [], + [ + 'uid' => $this->session->get('uid'), + 'blocked' => false, + 'account_expired' => false, + 'account_removed' => false, + 'verified' => true, + ] + ); + if (!$this->dba->isResult($user)) { + $this->session->clear(); + $this->baseUrl->redirect(); + } + + $this->setForUser($user); } } @@ -231,6 +220,7 @@ class Authentication // Otherwise it's probably an openid. try { $openid = new LightOpenID($this->baseUrl->getHost()); + /** @phpstan-ignore-next-line $openid->identity is private, but will be set via magic setter */ $openid->identity = $openid_url; $this->session->set('openid', $openid_url); $this->session->set('remember', $remember); @@ -245,7 +235,6 @@ class Authentication /** * Attempts to authenticate using login/password * - * @param App $a The Friendica Application context * @param string $username * @param string $password Clear password * @param bool $remember Whether to set the session remember flag @@ -257,7 +246,7 @@ class Authentication * @throws HTTPException\MovedPermanentlyException * @throws HTTPException\TemporaryRedirectException */ - public function withPassword(App $a, string $username, string $password, bool $remember, string $return_path = '') + public function withPassword(string $username, string $password, bool $remember, string $return_path = '') { $record = null; @@ -265,7 +254,7 @@ class Authentication $record = $this->dba->selectFirst( 'user', [], - ['uid' => User::getIdFromPasswordAuthentication($username, $password)] + ['uid' => User::getIdFromPasswordAuthentication($username, $password, false, true)] ); } catch (Exception $e) { $this->logger->warning('authenticate: failed login attempt', ['action' => 'login', 'username' => $username, 'ip' => $this->remoteAddress]); @@ -273,6 +262,12 @@ class Authentication $this->baseUrl->redirect(); } + if ($record['blocked']) { + $this->logger->warning('authenticate: user is blocked', ['action' => 'login', 'username' => $username, 'ip' => $this->remoteAddress]); + DI::sysmsg()->addNotice($this->l10n->t('Login failed because your account is blocked.')); + $this->baseUrl->redirect(); + } + if (!$remember) { $trusted = $this->cookie->get('2fa_cookie_hash') ?? null; $this->cookie->clear(); @@ -283,7 +278,6 @@ class Authentication // if we haven't failed up this point, log them in. $this->session->set('remember', $remember); - $this->session->set('last_login_date', DateTimeFormat::utcNow()); $openid_identity = $this->session->get('openid_identity'); $openid_server = $this->session->get('openid_server'); @@ -299,7 +293,7 @@ class Authentication $return_path = '/security/password_too_long?' . http_build_query(['return_path' => $return_path]); } - $this->setForUser($a, $record, true, true); + $this->setForUser($record, true, true); $this->baseUrl->redirect($return_path); } @@ -307,11 +301,10 @@ class Authentication /** * Sets the provided user's authenticated session * - * @param App $a The Friendica application context * @param array $user_record The current "user" record * @param bool $login_initial * @param bool $interactive - * @param bool $login_refresh + * @param bool $refresh_login * * @throws HTTPException\FoundException * @throws HTTPException\MovedPermanentlyException @@ -321,7 +314,7 @@ class Authentication * @throws HTTPException\InternalServerErrorException In case of Friendica specific exceptions * */ - public function setForUser(App $a, array $user_record, bool $login_initial = false, bool $interactive = false, bool $login_refresh = false) + public function setForUser(array $user_record, bool $login_initial = false, bool $interactive = false, bool $refresh_login = true) { $my_url = $this->baseUrl . '/profile/' . $user_record['nickname']; @@ -343,24 +336,20 @@ class Authentication $this->session->set('new_member', time() < ($member_since + (60 * 60 * 24 * 14))); if (strlen($user_record['timezone'])) { - $a->setTimeZone($user_record['timezone']); + $this->appHelper->setTimeZone($user_record['timezone']); } $contact = $this->dba->selectFirst('contact', ['id'], ['uid' => $user_record['uid'], 'self' => true]); if ($this->dba->isResult($contact)) { - $a->setContactId($contact['id']); + $this->appHelper->setContactId($contact['id']); $this->session->set('cid', $contact['id']); } $this->setXAccMgmtStatusHeader($user_record); - if ($login_initial || $login_refresh) { - $this->dba->update('user', ['last-activity' => DateTimeFormat::utcNow('Y-m-d'), 'login_date' => DateTimeFormat::utcNow()], ['uid' => $user_record['uid']]); - - // Set the login date for all identities of the user - $this->dba->update('user', ['last-activity' => DateTimeFormat::utcNow('Y-m-d'), 'login_date' => DateTimeFormat::utcNow()], - ['parent-uid' => $user_record['uid'], 'account_removed' => false]); + User::updateLastActivity($user_record, $refresh_login); + if ($login_initial) { // Regularly update suggestions if (Contact\Relation::areSuggestionsOutdated($user_record['uid'])) { Worker::add(Worker::PRIORITY_MEDIUM, 'UpdateSuggestions', $user_record['uid']); @@ -462,4 +451,25 @@ class Authentication $this->baseUrl->redirect('2fa'); } } + + /** + * Set the URL of an unauthenticated visitor + * + * @param string $url + * @return void + */ + public function setUnauthenticatedVisitor(string $url) + { + if (Strings::compareLink($this->session->get('visitor_home') ?: '', $url)) { + return; + } + + $this->session->set('my_url', $url); + $this->session->set('authenticated', 0); + + $remote_contact = Contact::getByURL($url, false, ['subscribe']); + if (!empty($remote_contact['subscribe'])) { + $this->session->set('remote_comment', $remote_contact['subscribe']); + } + } } diff --git a/src/Security/BasicAuth.php b/src/Security/BasicAuth.php index 51da7a98ad..52a0f75026 100644 --- a/src/Security/BasicAuth.php +++ b/src/Security/BasicAuth.php @@ -1,34 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security; use Exception; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\User; use Friendica\Network\HTTPException\UnauthorizedException; -use Friendica\Util\DateTimeFormat; /** * Authentication via the basic auth method @@ -89,9 +74,9 @@ class BasicAuth $source = 'Twidere'; } - Logger::info('Unrecognized user-agent', ['http_user_agent' => $_SERVER['HTTP_USER_AGENT']]); + DI::logger()->info('Unrecognized user-agent', ['http_user_agent' => $_SERVER['HTTP_USER_AGENT']]); } else { - Logger::info('Empty user-agent'); + DI::logger()->info('Empty user-agent'); } if (empty($source)) { @@ -119,23 +104,22 @@ class BasicAuth * * @return integer User ID */ - private static function getUserIdByAuth(bool $do_login = true):int + private static function getUserIdByAuth(bool $do_login = true): int { - $a = DI::app(); self::$current_user_id = 0; // workaround for HTTP-auth in CGI mode if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) { $userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6)); if (!empty($userpass) && strpos($userpass, ':')) { - list($name, $password) = explode(':', $userpass); + list($name, $password) = explode(':', $userpass); $_SERVER['PHP_AUTH_USER'] = $name; - $_SERVER['PHP_AUTH_PW'] = $password; + $_SERVER['PHP_AUTH_PW'] = $password; } } $user = $_SERVER['PHP_AUTH_USER'] ?? ''; - $password = $_SERVER['PHP_AUTH_PW'] ?? ''; + $password = $_SERVER['PHP_AUTH_PW'] ?? ''; // allow "user@server" login (but ignore 'server' part) $at = strstr($user, "@", true); @@ -147,25 +131,29 @@ class BasicAuth $record = null; $addon_auth = [ - 'username' => trim($user), - 'password' => trim($password), + 'username' => trim($user), + 'password' => trim($password), 'authenticated' => 0, - 'user_record' => null, + 'user_record' => null, ]; - /* - * An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record - * Addons should never set 'authenticated' except to indicate success - as hooks may be chained - * and later addons should not interfere with an earlier one that succeeded. - */ - Hook::callAll('authenticate', $addon_auth); + $eventDispatcher = DI::eventDispatcher(); + + /** + * An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record + * Addons should never set 'authenticated' except to indicate success - as hooks may be chained + * and later addons should not interfere with an earlier one that succeeded. + */ + $addon_auth = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::ACCOUNT_AUTHENTICATE, $addon_auth), + )->getArray(); if ($addon_auth['authenticated'] && !empty($addon_auth['user_record'])) { $record = $addon_auth['user_record']; } else { try { $user_id = User::getIdFromPasswordAuthentication(trim($user), trim($password), true); - $record = DBA::selectFirst('user', [], ['uid' => $user_id]); + $record = DBA::selectFirst('user', [], ['uid' => $user_id]); } catch (Exception $ex) { $record = []; } @@ -175,7 +163,7 @@ class BasicAuth if (!$do_login) { return 0; } - Logger::debug('Access denied', ['parameters' => $_SERVER]); + DI::logger()->debug('Access denied', ['parameters' => $_SERVER]); // Checking for commandline for the tests, we have to avoid to send a header if (DI::config()->get('system', 'basicauth') && (php_sapi_name() !== 'cli')) { header('WWW-Authenticate: Basic realm="Friendica"'); @@ -183,10 +171,7 @@ class BasicAuth throw new UnauthorizedException("This API requires login"); } - // Don't refresh the login date more often than twice a day to spare database writes - $login_refresh = strcmp(DateTimeFormat::utc('now - 12 hours'), $record['login_date']) > 0; - - DI::auth()->setForUser($a, $record, false, false, $login_refresh); + DI::auth()->setForUser($record, false, false, false); Hook::callAll('logged_in', $record); diff --git a/src/Security/ExAuth.php b/src/Security/ExAuth.php index cc1f03f8cc..43aff95487 100644 --- a/src/Security/ExAuth.php +++ b/src/Security/ExAuth.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: GPL-2.0-only * * ejabberd extauth script for the integration with friendica * @@ -58,6 +46,8 @@ use Friendica\Database\Database; use Friendica\DI; use Friendica\Model\User; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Network\HTTPException; use Friendica\Util\PidFile; @@ -240,7 +230,11 @@ class ExAuth $url = ($ssl ? 'https' : 'http') . '://' . $host . '/noscrape/' . $user; - $curlResult = DI::httpClient()->get($url, HttpClientAccept::JSON); + try { + $curlResult = DI::httpClient()->get($url, HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::CONTACTVERIFIER]); + } catch (\Throwable $th) { + return false; + } if (!$curlResult->isSuccess()) { return false; @@ -250,7 +244,7 @@ class ExAuth return false; } - $json = @json_decode($curlResult->getBody()); + $json = @json_decode($curlResult->getBodyString()); if (!is_object($json)) { return false; } diff --git a/src/Security/OAuth.php b/src/Security/OAuth.php index 7a0edfef2c..5fd93936ad 100644 --- a/src/Security/OAuth.php +++ b/src/Security/OAuth.php @@ -1,35 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\User; use Friendica\Module\BaseApi; use Friendica\Util\DateTimeFormat; -use GuzzleHttp\Psr7\Uri; /** * OAuth Server @@ -100,12 +85,15 @@ class OAuth $token = DBA::selectFirst('application-view', ['uid', 'id', 'name', 'website', 'created_at', 'read', 'write', 'follow', 'push'], $condition); if (!DBA::isResult($token)) { - Logger::notice('Token not found', $condition); + DI::logger()->notice('Token not found', $condition); return []; } - Logger::debug('Token found', $token); + DI::logger()->debug('Token found', $token); - User::updateLastActivity($token['uid']); + $user = User::getById($token['uid'], ['uid', 'parent-uid', 'last-activity', 'login_date']); + if (!empty($user)) { + User::updateLastActivity($user, false); + } // Regularly update suggestions if (Contact\Relation::areSuggestionsOutdated($token['uid'])) { @@ -132,17 +120,19 @@ class OAuth if (!empty($redirect_uri)) { $redirect_uri = strtok($redirect_uri, '?'); - $condition = DBA::mergeConditions($condition, ["`redirect_uri` LIKE ?", '%' . $redirect_uri . '%']); + $condition = DBA::mergeConditions($condition, ["`redirect_uri` LIKE ?", '%' . $redirect_uri . '%']); } $application = DBA::selectFirst('application', [], $condition); if (!DBA::isResult($application)) { - Logger::warning('Application not found', $condition); + DI::logger()->warning('Application not found', $condition); return []; } - // The redirect_uri could contain several URI that are separated by spaces. - if (($application['redirect_uri'] != $redirect_uri) && !in_array($redirect_uri, explode(' ', $application['redirect_uri']))) { + // The redirect_uri could contain several URI that are separated by spaces or new lines. + $uris = explode(' ', str_replace(["\n", "\r", "\t"], ' ', $application['redirect_uri'])); + if (!in_array($redirect_uri, $uris)) { + DI::logger()->warning('Redirection uri does not match', ['redirect_uri' => $redirect_uri, 'application-redirect_uri' => $application['redirect_uri']]); return []; } @@ -199,9 +189,9 @@ class OAuth 'created_at' => DateTimeFormat::utcNow() ]; - foreach ([BaseApi::SCOPE_READ, BaseApi::SCOPE_WRITE, BaseApi::SCOPE_WRITE, BaseApi::SCOPE_PUSH] as $scope) { + foreach ([BaseApi::SCOPE_READ, BaseApi::SCOPE_WRITE, BaseApi::SCOPE_FOLLOW, BaseApi::SCOPE_PUSH] as $scope) { if ($fields[$scope] && !$application[$scope]) { - Logger::warning('Requested token scope is not allowed for the application', ['token' => $fields, 'application' => $application]); + DI::logger()->warning('Requested token scope is not allowed for the application', ['token' => $fields, 'application' => $application]); } } diff --git a/src/Security/OAuth1/OAuthConsumer.php b/src/Security/OAuth1/OAuthConsumer.php index a827180349..910e5f4aac 100644 --- a/src/Security/OAuth1/OAuthConsumer.php +++ b/src/Security/OAuth1/OAuthConsumer.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1; diff --git a/src/Security/OAuth1/OAuthDataStore.php b/src/Security/OAuth1/OAuthDataStore.php index bed9a20cbf..6415ae5bd1 100644 --- a/src/Security/OAuth1/OAuthDataStore.php +++ b/src/Security/OAuth1/OAuthDataStore.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1; diff --git a/src/Security/OAuth1/OAuthException.php b/src/Security/OAuth1/OAuthException.php index f6de7c983b..a3ef31cf78 100644 --- a/src/Security/OAuth1/OAuthException.php +++ b/src/Security/OAuth1/OAuthException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1; diff --git a/src/Security/OAuth1/OAuthRequest.php b/src/Security/OAuth1/OAuthRequest.php index 9e5892e46e..b3564d2d1b 100644 --- a/src/Security/OAuth1/OAuthRequest.php +++ b/src/Security/OAuth1/OAuthRequest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1; diff --git a/src/Security/OAuth1/OAuthToken.php b/src/Security/OAuth1/OAuthToken.php index 45530345e5..f18a967ba4 100644 --- a/src/Security/OAuth1/OAuthToken.php +++ b/src/Security/OAuth1/OAuthToken.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1; diff --git a/src/Security/OAuth1/OAuthUtil.php b/src/Security/OAuth1/OAuthUtil.php index 06847eceb3..e58522117a 100644 --- a/src/Security/OAuth1/OAuthUtil.php +++ b/src/Security/OAuth1/OAuthUtil.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1; diff --git a/src/Security/OAuth1/Signature/OAuthSignatureMethod.php b/src/Security/OAuth1/Signature/OAuthSignatureMethod.php index fabad7051c..0aae613c4f 100644 --- a/src/Security/OAuth1/Signature/OAuthSignatureMethod.php +++ b/src/Security/OAuth1/Signature/OAuthSignatureMethod.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1\Signature; diff --git a/src/Security/OAuth1/Signature/OAuthSignatureMethod_HMAC_SHA1.php b/src/Security/OAuth1/Signature/OAuthSignatureMethod_HMAC_SHA1.php index b6b715de78..5107fb1efe 100644 --- a/src/Security/OAuth1/Signature/OAuthSignatureMethod_HMAC_SHA1.php +++ b/src/Security/OAuth1/Signature/OAuthSignatureMethod_HMAC_SHA1.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1\Signature; diff --git a/src/Security/OAuth1/Signature/OAuthSignatureMethod_PLAINTEXT.php b/src/Security/OAuth1/Signature/OAuthSignatureMethod_PLAINTEXT.php index d74d513283..db64ef5d5d 100644 --- a/src/Security/OAuth1/Signature/OAuthSignatureMethod_PLAINTEXT.php +++ b/src/Security/OAuth1/Signature/OAuthSignatureMethod_PLAINTEXT.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1\Signature; diff --git a/src/Security/OAuth1/Signature/OAuthSignatureMethod_RSA_SHA1.php b/src/Security/OAuth1/Signature/OAuthSignatureMethod_RSA_SHA1.php index 486ae0cdc3..aafbe1a869 100644 --- a/src/Security/OAuth1/Signature/OAuthSignatureMethod_RSA_SHA1.php +++ b/src/Security/OAuth1/Signature/OAuthSignatureMethod_RSA_SHA1.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\OAuth1\Signature; diff --git a/src/Security/OpenWebAuth.php b/src/Security/OpenWebAuth.php new file mode 100644 index 0000000000..8ca4baf5d1 --- /dev/null +++ b/src/Security/OpenWebAuth.php @@ -0,0 +1,238 @@ +getMyUrl(); + $my_url = Network::isUrlValid($my_url); + + if (empty($my_url) || DI::userSession()->getLocalUserId()) { + return; + } + + $addr = $_GET['addr'] ?? $my_url; + + $arr = ['zrl' => $my_url, 'url' => DI::args()->getCommand()]; + Hook::callAll('zrl_init', $arr); + + // Try to find the public contact entry of the visitor. + $contact = Contact::getByURL($my_url, null, ['id', 'url', 'gsid']); + if (empty($contact)) { + DI::logger()->info('No contact record found', ['url' => $my_url]); + return; + } + + if (DI::userSession()->getRemoteUserId() && DI::userSession()->getRemoteUserId() == $contact['id']) { + DI::logger()->info('The visitor is already authenticated', ['url' => $my_url]); + return; + } + + $gserver = DBA::selectFirst('gserver', ['url', 'authredirect'], ['id' => $contact['gsid']]); + if (empty($gserver) || empty($gserver['authredirect'])) { + DI::logger()->info('No server record found or magic path not defined for server', ['id' => $contact['gsid'], 'gserver' => $gserver]); + return; + } + + // Avoid endless loops + $cachekey = 'zrlInit:' . $my_url; + if (DI::cache()->get($cachekey)) { + DI::logger()->info('URL ' . $my_url . ' already tried to authenticate.'); + return; + } else { + DI::cache()->set($cachekey, true, Duration::MINUTE); + } + + DI::logger()->info('Not authenticated. Invoking reverse magic-auth', ['url' => $my_url]); + + // Remove the "addr" parameter from the destination. It is later added as separate parameter again. + $addr_request = 'addr=' . urlencode($addr); + $query = rtrim(str_replace($addr_request, '', DI::args()->getQueryString()), '?&'); + + // The other instance needs to know where to redirect. + $dest = urlencode(DI::baseUrl() . '/' . $query); + + if ($gserver['url'] != DI::baseUrl() && !strstr($dest, '/magic')) { + $magic_path = $gserver['authredirect'] . '?f=&rev=1&owa=1&dest=' . $dest . '&' . $addr_request; + + DI::logger()->info('Doing magic auth for visitor ' . $my_url . ' to ' . $magic_path); + System::externalRedirect($magic_path); + } + } + + /** + * OpenWebAuth authentication. + * + * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/zid.php + * + * @param string $token + * + * @return void + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function init(string $token) + { + $appHelper = DI::appHelper(); + + // Clean old OpenWebAuthToken entries. + OpenWebAuthToken::purge('owt', '3 MINUTE'); + + // Check if the token we got is the same one + // we have stored in the database. + $visitor_handle = OpenWebAuthToken::getMeta('owt', 0, $token); + + if ($visitor_handle === false) { + return; + } + + $visitor = self::addVisitorCookieForHandle($visitor_handle); + if (empty($visitor)) { + return; + } + + $arr = [ + 'visitor' => $visitor, + 'url' => DI::args()->getQueryString() + ]; + /** + * @hooks magic_auth_success + * Called when a magic-auth was successful. + * * \e array \b visitor + * * \e string \b url + */ + Hook::callAll('magic_auth_success', $arr); + + $appHelper->setContactId($arr['visitor']['id']); + + DI::sysmsg()->addInfo(DI::l10n()->t('OpenWebAuth: %1$s welcomes %2$s', DI::baseUrl()->getHost(), $visitor['name'])); + + DI::logger()->info('OpenWebAuth: auth success from ' . $visitor['addr']); + } + + /** + * Set the visitor cookies (see remote_user()) for the given handle + * + * @param string $handle Visitor handle + * + * @return array Visitor contact array + */ + public static function addVisitorCookieForHandle(string $handle): array + { + $appHelper = DI::appHelper(); + + // Try to find the public contact entry of the visitor. + $cid = Contact::getIdForURL($handle); + if (!$cid) { + DI::logger()->info('Handle not found', ['handle' => $handle]); + return []; + } + + $visitor = Contact::getById($cid); + + // Authenticate the visitor. + DI::userSession()->setMultiple([ + 'authenticated' => 1, + 'visitor_id' => $visitor['id'], + 'visitor_handle' => $visitor['addr'], + 'visitor_home' => $visitor['url'], + 'my_url' => $visitor['url'], + 'remote_comment' => $visitor['subscribe'], + ]); + + DI::userSession()->setVisitorsContacts($visitor['url']); + + $appHelper->setContactId($visitor['id']); + + DI::logger()->info('Authenticated visitor', ['url' => $visitor['url']]); + + return $visitor; + } + + /** + * Set the visitor cookies (see remote_user()) for signed HTTP requests + * + * @param array $server The content of the $_SERVER superglobal + * @return array Visitor contact array + * @throws InternalServerErrorException + */ + public static function addVisitorCookieForHTTPSigner(array $server): array + { + $requester = HTTPSignature::getSigner('', $server); + if (empty($requester)) { + return []; + } + return self::addVisitorCookieForHandle($requester); + } + + /** + * Returns URL with URL-encoded zrl parameter + * + * @param string $url URL to enhance + * @param bool $force Either to force adding zrl parameter + * + * @return string URL with 'zrl' parameter or original URL in case of no Friendica profile URL + */ + public static function getZrlUrl(string $url, bool $force = false): string + { + if (!strlen($url)) { + return $url; + } + if (!strpos($url, '/profile/') && !$force) { + return $url; + } + if ($force && substr($url, -1, 1) !== '/') { + $url = $url . '/'; + } + + $achar = strpos($url, '?') ? '&' : '?'; + $mine = DI::userSession()->getMyUrl(); + + if ($mine && !Strings::compareLink($mine, $url)) { + return $url . $achar . 'zrl=' . urlencode($mine); + } + + return $url; + } +} diff --git a/src/Security/PermissionSet/Collection/PermissionSets.php b/src/Security/PermissionSet/Collection/PermissionSets.php index fc9c7c0454..445eafcf93 100644 --- a/src/Security/PermissionSet/Collection/PermissionSets.php +++ b/src/Security/PermissionSet/Collection/PermissionSets.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\PermissionSet\Collection; diff --git a/src/Security/PermissionSet/Entity/PermissionSet.php b/src/Security/PermissionSet/Entity/PermissionSet.php index cf60732a0e..4a3abdbe00 100644 --- a/src/Security/PermissionSet/Entity/PermissionSet.php +++ b/src/Security/PermissionSet/Entity/PermissionSet.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\PermissionSet\Entity; diff --git a/src/Security/PermissionSet/Exception/PermissionSetNotFoundException.php b/src/Security/PermissionSet/Exception/PermissionSetNotFoundException.php index a8daf5ba75..4c55d1d0cc 100644 --- a/src/Security/PermissionSet/Exception/PermissionSetNotFoundException.php +++ b/src/Security/PermissionSet/Exception/PermissionSetNotFoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\PermissionSet\Exception; diff --git a/src/Security/PermissionSet/Exception/PermissionSetPersistenceException.php b/src/Security/PermissionSet/Exception/PermissionSetPersistenceException.php index 40c1fcfcce..420c27aa20 100644 --- a/src/Security/PermissionSet/Exception/PermissionSetPersistenceException.php +++ b/src/Security/PermissionSet/Exception/PermissionSetPersistenceException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\PermissionSet\Exception; diff --git a/src/Security/PermissionSet/Factory/PermissionSet.php b/src/Security/PermissionSet/Factory/PermissionSet.php index 2ef65d8ac8..dde416fecc 100644 --- a/src/Security/PermissionSet/Factory/PermissionSet.php +++ b/src/Security/PermissionSet/Factory/PermissionSet.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\PermissionSet\Factory; diff --git a/src/Security/PermissionSet/Repository/PermissionSet.php b/src/Security/PermissionSet/Repository/PermissionSet.php index 5abfb9305d..8eca294662 100644 --- a/src/Security/PermissionSet/Repository/PermissionSet.php +++ b/src/Security/PermissionSet/Repository/PermissionSet.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\PermissionSet\Repository; @@ -29,9 +15,9 @@ use Friendica\Model\Circle; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Security\PermissionSet\Exception\PermissionSetNotFoundException; use Friendica\Security\PermissionSet\Exception\PermissionSetPersistenceException; -use Friendica\Security\PermissionSet\Factory; -use Friendica\Security\PermissionSet\Collection; -use Friendica\Security\PermissionSet\Entity; +use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory; +use Friendica\Security\PermissionSet\Collection\PermissionSets as PermissionSetsCollection; +use Friendica\Security\PermissionSet\Entity\PermissionSet as PermissionSetEntity; use Friendica\Util\ACLFormatter; use Psr\Log\LoggerInterface; @@ -40,7 +26,7 @@ class PermissionSet extends BaseRepository /** @var int Virtual permission set id for public permission */ const PUBLIC = 0; - /** @var Factory\PermissionSet */ + /** @var PermissionSetFactory */ protected $factory; protected static $table_name = 'permissionset'; @@ -48,7 +34,7 @@ class PermissionSet extends BaseRepository /** @var ACLFormatter */ private $aclFormatter; - public function __construct(Database $database, LoggerInterface $logger, Factory\PermissionSet $factory, ACLFormatter $aclFormatter) + public function __construct(Database $database, LoggerInterface $logger, PermissionSetFactory $factory, ACLFormatter $aclFormatter) { parent::__construct($database, $logger, $factory); @@ -56,34 +42,28 @@ class PermissionSet extends BaseRepository } /** - * @param array $condition - * @param array $params - * - * @return Entity\PermissionSet * @throws NotFoundException * @throws Exception */ - private function selectOne(array $condition, array $params = []): Entity\PermissionSet + private function selectOne(array $condition, array $params = []): PermissionSetEntity { - return parent::_selectOne($condition, $params); + $fields = parent::_selectFirstRowAsArray($condition, $params); + + return $this->factory->createFromTableRow($fields); } /** * @throws Exception */ - private function select(array $condition, array $params = []): Collection\PermissionSets + private function select(array $condition, array $params = []): PermissionSetsCollection { - return new Collection\PermissionSets(parent::_select($condition, $params)->getArrayCopy()); + return new PermissionSetsCollection(parent::_select($condition, $params)->getArrayCopy()); } /** * Converts a given PermissionSet into a DB compatible row array - * - * @param Entity\PermissionSet $permissionSet - * - * @return array */ - protected function convertToTableRow(Entity\PermissionSet $permissionSet): array + protected function convertToTableRow(PermissionSetEntity $permissionSet): array { return [ 'uid' => $permissionSet->uid, @@ -97,12 +77,11 @@ class PermissionSet extends BaseRepository /** * @param int $id A PermissionSet table row id or self::PUBLIC * @param int $uid The owner of the PermissionSet - * @return Entity\PermissionSet * * @throws PermissionSetNotFoundException * @throws PermissionSetPersistenceException */ - public function selectOneById(int $id, int $uid): Entity\PermissionSet + public function selectOneById(int $id, int $uid): PermissionSetEntity { if ($id === self::PUBLIC) { return $this->factory->createFromString($uid); @@ -123,11 +102,9 @@ class PermissionSet extends BaseRepository * @param int $cid Contact id of the visitor * @param int $uid User id whom the items belong, used for ownership check. * - * @return Collection\PermissionSets - * * @throws PermissionSetPersistenceException */ - public function selectByContactId(int $cid, int $uid): Collection\PermissionSets + public function selectByContactId(int $cid, int $uid): PermissionSetsCollection { try { $cdata = Contact::getPublicAndUserContactID($cid, $uid); @@ -142,8 +119,8 @@ class PermissionSet extends BaseRepository $circle_ids = []; if (!empty($user_contact_str) && $this->db->exists('contact', [ - 'id' => $cid, - 'uid' => $uid, + 'id' => $cid, + 'uid' => $uid, 'blocked' => false ])) { $circle_ids = Circle::getIdsByContactId($cid); @@ -155,13 +132,13 @@ class PermissionSet extends BaseRepository } if (!empty($user_contact_str)) { - $condition = ["`uid` = ? AND (NOT (LOCATE(?, `deny_cid`) OR LOCATE(?, `deny_cid`) OR deny_gid REGEXP ?) - AND (LOCATE(?, allow_cid) OR LOCATE(?, allow_cid) OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))", + $condition = ["`uid` = ? AND (NOT (LOCATE(?, `deny_cid`) OR LOCATE(?, `deny_cid`) OR CAST(deny_gid AS BINARY) REGEXP BINARY ?) + AND (LOCATE(?, allow_cid) OR LOCATE(?, allow_cid) OR CAST(allow_gid AS BINARY) REGEXP BINARY ? OR (allow_cid = '' AND allow_gid = '')))", $uid, $user_contact_str, $public_contact_str, $circle_str, $user_contact_str, $public_contact_str, $circle_str]; } else { - $condition = ["`uid` = ? AND (NOT (LOCATE(?, `deny_cid`) OR deny_gid REGEXP ?) - AND (LOCATE(?, allow_cid) OR allow_gid REGEXP ? OR (allow_cid = '' AND allow_gid = '')))", + $condition = ["`uid` = ? AND (NOT (LOCATE(?, `deny_cid`) OR CAST(deny_gid AS BINARY) REGEXP BINARY ?) + AND (LOCATE(?, allow_cid) OR CAST(allow_gid AS BINARY) REGEXP BINARY ? OR (allow_cid = '' AND allow_gid = '')))", $uid, $public_contact_str, $circle_str, $public_contact_str, $circle_str]; } @@ -176,16 +153,14 @@ class PermissionSet extends BaseRepository * * @param int $uid * - * @return Entity\PermissionSet - * * @throws PermissionSetPersistenceException */ - public function selectDefaultForUser(int $uid): Entity\PermissionSet + public function selectDefaultForUser(int $uid): PermissionSetEntity { try { $self_contact = Contact::selectFirst(['id'], ['uid' => $uid, 'self' => true]); } catch (Exception $exception) { - throw new PermissionSetPersistenceException(sprintf('Cannot select Contact for user %d', $uid)); + throw new PermissionSetPersistenceException(sprintf('Cannot select Contact for user %d', $uid), $exception); } if (!$this->db->isResult($self_contact)) { @@ -202,10 +177,8 @@ class PermissionSet extends BaseRepository * Fetch the public PermissionSet * * @param int $uid - * - * @return Entity\PermissionSet */ - public function selectPublicForUser(int $uid): Entity\PermissionSet + public function selectPublicForUser(int $uid): PermissionSetEntity { return $this->factory->createFromString($uid, '', '', '', '', self::PUBLIC); } @@ -213,13 +186,9 @@ class PermissionSet extends BaseRepository /** * Selects or creates a PermissionSet based on its fields * - * @param Entity\PermissionSet $permissionSet - * - * @return Entity\PermissionSet - * * @throws PermissionSetPersistenceException */ - public function selectOrCreate(Entity\PermissionSet $permissionSet): Entity\PermissionSet + public function selectOrCreate(PermissionSetEntity $permissionSet): PermissionSetEntity { if ($permissionSet->id) { return $permissionSet; @@ -240,13 +209,9 @@ class PermissionSet extends BaseRepository } /** - * @param Entity\PermissionSet $permissionSet - * - * @return Entity\PermissionSet - * * @throws PermissionSetPersistenceException */ - public function save(Entity\PermissionSet $permissionSet): Entity\PermissionSet + public function save(PermissionSetEntity $permissionSet): PermissionSetEntity { // Don't save/update the common public PermissionSet if ($permissionSet->isPublic()) { diff --git a/src/Security/Security.php b/src/Security/Security.php index 0774491d02..96e3386141 100644 --- a/src/Security/Security.php +++ b/src/Security/Security.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security; @@ -59,25 +45,25 @@ class Security return true; } elseif ($verified === 1) { return false; + } + + $user = User::getById($owner); + if (!$user || $user['blockwall']) { + $verified = 1; + return false; + } + + $contact = Contact::getById($cid); + if (!is_array($contact) || $contact['blocked'] || $contact['readonly'] || $contact['pending']) { + $verified = 1; + return false; + } + + if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]) || ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY)) { + $verified = 2; + return true; } else { - $user = User::getById($owner); - if (!$user || $user['blockwall']) { - $verified = 1; - return false; - } - - $contact = Contact::getById($cid); - if ($contact || $contact['blocked'] || $contact['readonly'] || $contact['pending']) { - $verified = 1; - return false; - } - - if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]) || ($user['page-flags'] == User::PAGE_FLAGS_COMMUNITY)) { - $verified = 2; - return true; - } else { - $verified = 1; - } + $verified = 1; } } @@ -93,33 +79,25 @@ class Security */ public static function getPermissionsSQLByUserId(int $owner_id, bool $accessible = false) { - $local_user = DI::userSession()->getLocalUserId(); + $local_user = DI::userSession()->getLocalUserId(); $remote_contact = DI::userSession()->getRemoteContactID($owner_id); - $acc_sql = ''; + $acc_sql = ''; if ($accessible) { $acc_sql = ' OR `accessible`'; } - /* - * Construct permissions - * - * default permissions - anonymous user - */ + // Construct permissions: default permissions - anonymous user $sql = " AND (allow_cid = '' AND allow_gid = '' AND deny_cid = '' AND deny_gid = ''" . $acc_sql . ") "; - /* - * Profile owner - everything is visible - */ if ($local_user && $local_user == $owner_id) { + // Profile owner - everything is visible $sql = ''; - /* - * Authenticated visitor. Load the circles the visitor belongs to. - */ } elseif ($remote_contact) { + // Authenticated visitor. Load the circles the visitor belongs to. $circleIds = '<<>>'; // should be impossible to match foreach (Circle::getIdsByContactId($remote_contact) as $circleId) { @@ -127,8 +105,8 @@ class Security } $sql = sprintf( - " AND (NOT (deny_cid REGEXP '<%d>' OR deny_gid REGEXP '%s') - AND (allow_cid REGEXP '<%d>' OR allow_gid REGEXP '%s' + " AND (NOT (CAST(deny_cid AS BINARY) REGEXP BINARY '<%d>' OR CAST(deny_gid AS BINARY) REGEXP BINARY '%s') + AND (CAST(allow_cid AS BINARY) REGEXP BINARY '<%d>' OR CAST(allow_gid AS BINARY) REGEXP BINARY '%s' OR (allow_cid = '' AND allow_gid = ''))" . $acc_sql . ") ", intval($remote_contact), DBA::escape($circleIds), diff --git a/src/Security/TwoFactor/Collection/TrustedBrowsers.php b/src/Security/TwoFactor/Collection/TrustedBrowsers.php index 71bee2c3f4..712cf1319c 100644 --- a/src/Security/TwoFactor/Collection/TrustedBrowsers.php +++ b/src/Security/TwoFactor/Collection/TrustedBrowsers.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Collection; diff --git a/src/Security/TwoFactor/Exception/TrustedBrowserNotFoundException.php b/src/Security/TwoFactor/Exception/TrustedBrowserNotFoundException.php index c16829a534..ce0c39788e 100644 --- a/src/Security/TwoFactor/Exception/TrustedBrowserNotFoundException.php +++ b/src/Security/TwoFactor/Exception/TrustedBrowserNotFoundException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Exception; diff --git a/src/Security/TwoFactor/Exception/TrustedBrowserPersistenceException.php b/src/Security/TwoFactor/Exception/TrustedBrowserPersistenceException.php index 5a96a0c226..4076642f51 100644 --- a/src/Security/TwoFactor/Exception/TrustedBrowserPersistenceException.php +++ b/src/Security/TwoFactor/Exception/TrustedBrowserPersistenceException.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Exception; diff --git a/src/Security/TwoFactor/Factory/TrustedBrowser.php b/src/Security/TwoFactor/Factory/TrustedBrowser.php index fa822b9652..3cd58eaad2 100644 --- a/src/Security/TwoFactor/Factory/TrustedBrowser.php +++ b/src/Security/TwoFactor/Factory/TrustedBrowser.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Factory; diff --git a/src/Security/TwoFactor/Model/AppSpecificPassword.php b/src/Security/TwoFactor/Model/AppSpecificPassword.php index affabb3464..b7fe69ecea 100644 --- a/src/Security/TwoFactor/Model/AppSpecificPassword.php +++ b/src/Security/TwoFactor/Model/AppSpecificPassword.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Model; diff --git a/src/Security/TwoFactor/Model/RecoveryCode.php b/src/Security/TwoFactor/Model/RecoveryCode.php index b3c197f98e..a06192ad86 100644 --- a/src/Security/TwoFactor/Model/RecoveryCode.php +++ b/src/Security/TwoFactor/Model/RecoveryCode.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Model; diff --git a/src/Security/TwoFactor/Model/TrustedBrowser.php b/src/Security/TwoFactor/Model/TrustedBrowser.php index c01e41fec7..dccf22d331 100644 --- a/src/Security/TwoFactor/Model/TrustedBrowser.php +++ b/src/Security/TwoFactor/Model/TrustedBrowser.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Model; @@ -28,12 +14,12 @@ use Friendica\Util\DateTimeFormat; * Class TrustedBrowser * * - * @property-read $cookie_hash - * @property-read $uid - * @property-read $user_agent - * @property-read $trusted - * @property-read $created - * @property-read $last_used + * @property-read string $cookie_hash + * @property-read int $uid + * @property-read string $user_agent + * @property-read bool $trusted + * @property-read string $created + * @property-read string|null $last_used * @package Friendica\Model\TwoFactor */ class TrustedBrowser extends BaseEntity diff --git a/src/Security/TwoFactor/Repository/TrustedBrowser.php b/src/Security/TwoFactor/Repository/TrustedBrowser.php index d913be3c9b..2f5fb5f02b 100644 --- a/src/Security/TwoFactor/Repository/TrustedBrowser.php +++ b/src/Security/TwoFactor/Repository/TrustedBrowser.php @@ -1,30 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Security\TwoFactor\Repository; -use Friendica\Security\TwoFactor; use Friendica\Database\Database; use Friendica\Security\TwoFactor\Exception\TrustedBrowserNotFoundException; use Friendica\Security\TwoFactor\Exception\TrustedBrowserPersistenceException; +use Friendica\Security\TwoFactor\Collection\TrustedBrowsers as TrustedBrowsersCollection; +use Friendica\Security\TwoFactor\Factory\TrustedBrowser as TrustedBrowserFactory; +use Friendica\Security\TwoFactor\Model\TrustedBrowser as TrustedBrowserModel; use Psr\Log\LoggerInterface; class TrustedBrowser @@ -35,27 +23,27 @@ class TrustedBrowser /** @var LoggerInterface */ protected $logger; - /** @var TwoFactor\Factory\TrustedBrowser */ + /** @var TrustedBrowserFactory */ protected $factory; protected static $table_name = '2fa_trusted_browser'; - public function __construct(Database $database, LoggerInterface $logger, TwoFactor\Factory\TrustedBrowser $factory = null) + public function __construct(Database $database, LoggerInterface $logger, TrustedBrowserFactory $factory = null) { $this->db = $database; $this->logger = $logger; - $this->factory = $factory ?? new TwoFactor\Factory\TrustedBrowser($logger); + $this->factory = $factory ?? new TrustedBrowserFactory($logger); } /** * @param string $cookie_hash * - * @return TwoFactor\Model\TrustedBrowser|null + * @return TrustedBrowserModel * * @throws TrustedBrowserPersistenceException * @throws TrustedBrowserNotFoundException */ - public function selectOneByHash(string $cookie_hash): TwoFactor\Model\TrustedBrowser + public function selectOneByHash(string $cookie_hash): TrustedBrowserModel { try { $fields = $this->db->selectFirst(self::$table_name, [], ['cookie_hash' => $cookie_hash]); @@ -70,13 +58,9 @@ class TrustedBrowser } /** - * @param int $uid - * - * @return TwoFactor\Collection\TrustedBrowsers - * * @throws TrustedBrowserPersistenceException */ - public function selectAllByUid(int $uid): TwoFactor\Collection\TrustedBrowsers + public function selectAllByUid(int $uid): TrustedBrowsersCollection { try { $rows = $this->db->selectToArray(self::$table_name, [], ['uid' => $uid]); @@ -85,7 +69,7 @@ class TrustedBrowser foreach ($rows as $fields) { $trustedBrowsers[] = $this->factory->createFromTableRow($fields); } - return new TwoFactor\Collection\TrustedBrowsers($trustedBrowsers); + return new TrustedBrowsersCollection($trustedBrowsers); } catch (\Exception $exception) { throw new TrustedBrowserPersistenceException(sprintf('selection for uid \'%s\' wasn\'t successful.', $uid)); @@ -93,13 +77,9 @@ class TrustedBrowser } /** - * @param TwoFactor\Model\TrustedBrowser $trustedBrowser - * - * @return bool - * * @throws TrustedBrowserPersistenceException */ - public function save(TwoFactor\Model\TrustedBrowser $trustedBrowser): bool + public function save(TrustedBrowserModel $trustedBrowser): bool { try { return $this->db->insert(self::$table_name, $trustedBrowser->toArray(), $this->db::INSERT_UPDATE); @@ -109,13 +89,9 @@ class TrustedBrowser } /** - * @param TwoFactor\Model\TrustedBrowser $trustedBrowser - * - * @return bool - * * @throws TrustedBrowserPersistenceException */ - public function remove(TwoFactor\Model\TrustedBrowser $trustedBrowser): bool + public function remove(TrustedBrowserModel $trustedBrowser): bool { try { return $this->db->delete(self::$table_name, ['cookie_hash' => $trustedBrowser->cookie_hash]); @@ -125,11 +101,6 @@ class TrustedBrowser } /** - * @param int $local_user - * @param string $cookie_hash - * - * @return bool - * * @throws TrustedBrowserPersistenceException */ public function removeForUser(int $local_user, string $cookie_hash): bool @@ -141,11 +112,6 @@ class TrustedBrowser } } - /** - * @param int $local_user - * - * @return bool - */ public function removeAllForUser(int $local_user): bool { try { diff --git a/src/System/Daemon.php b/src/System/Daemon.php new file mode 100644 index 0000000000..166d9400ef --- /dev/null +++ b/src/System/Daemon.php @@ -0,0 +1,210 @@ +pid; + } + + /** + * The path to the PID file (null if not set) + * + * @return string|null + */ + public function getPidfile(): ?string + { + return $this->pidfile; + } + + public function __construct(LoggerInterface $logger, Database $dba) + { + $this->logger = $logger; + $this->dba = $dba; + } + + /** + * Initialize the current daemon class with a given PID file + * + * @param string|null $pidfile the path to the PID file - using a given path if not directly set here + * + * @return void + */ + public function init(string $pidfile = null): void + { + if (!empty($pidfile)) { + $this->pid = null; + $this->pidfile = $pidfile; + } + + if (!empty($this->pid)) { + return; + } + + if (is_readable($this->pidfile)) { + $this->pid = intval(file_get_contents($this->pidfile)); + } + } + + /** + * Starts the daemon + * + * @param callable $daemonLogic the business logic of the daemon + * @param bool $foreground true, if started in foreground, otherwise spawned in the background + * + * @return bool true, if successfully started, otherwise false + */ + public function start(callable $daemonLogic, bool $foreground = false): bool + { + $this->init(); + + if (!empty($this->pid)) { + $this->logger->notice('process is already running', ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + return false; + } + + $this->logger->notice('starting daemon', ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + + if (!$foreground) { + $this->dba->disconnect(); + + // fork a daemon process + $this->pid = pcntl_fork(); + if ($this->pid < 0) { + $this->logger->warning('Could not fork daemon'); + return false; + } elseif ($this->pid) { + // The parent process continues here + if (!file_put_contents($this->pidfile, $this->pid)) { + $this->logger->warning('Could not store pid file', ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + posix_kill($this->pid, SIGTERM); + return false; + } + $this->logger->notice('Child process started', ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + return true; + } + + // We now are in the child process + register_shutdown_function(function (): void { + posix_kill(posix_getpid(), SIGTERM); + posix_kill(posix_getpid(), SIGHUP); + }); + + // Make the child the main process, detach it from the terminal + if (posix_setsid() < 0) { + return true; + } + + // Closing all existing connections with the outside + fclose(STDIN); + + // And now connect the database again + $this->dba->connect(); + } + + // Just to be sure that this script really runs endlessly + set_time_limit(0); + + $daemonLogic(); + + return true; + } + + /** + * Checks, if the current daemon is running + * + * @return bool true, if the daemon is running, otherwise false (f.e no PID found, no PID file found, PID is not bound to a running process)) + */ + public function isRunning(): bool + { + $this->init(); + + if (empty($this->pid)) { + $this->logger->notice("Pid wasn't found"); + + if (is_readable($this->pidfile)) { + unlink($this->pidfile); + $this->logger->notice("Pidfile removed", ['pidfile' => $this->pidfile]); + } + return false; + } + + if (posix_kill($this->pid, 0)) { + $this->logger->notice("daemon process is running"); + return true; + } else { + unlink($this->pidfile); + $this->logger->notice("daemon process isn't running"); + return false; + } + } + + /** + * Stops the daemon, if running + * + * @return bool true, if the daemon was successfully stopped or is already stopped, otherwise false + */ + public function stop(): bool + { + $this->init(); + + if (empty($this->pid)) { + $this->logger->notice("Pidfile wasn't found", ['pidfile' => $this->pidfile]); + return true; + } + + if (!posix_kill($this->pid, SIGTERM)) { + $this->logger->warning("Cannot kill the given PID", ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + return false; + } + + if (!unlink($this->pidfile)) { + $this->logger->warning("Cannot delete the given PID file", ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + return false; + } + + $this->logger->notice('daemon process was killed', ['pid' => $this->pid, 'pidfile' => $this->pidfile]); + + return true; + } + + /** + * Sets the current daemon to sleep and checks the status afterward + * + * @param int $duration the duration of time for sleeping (in milliseconds) + * + * @return void + */ + public function sleep(int $duration) + { + usleep($duration); + + $this->pid = pcntl_waitpid(-1, $status, WNOHANG); + if ($this->pid > 0) { + $this->logger->info('Children quit via pcntl_waitpid', ['pid' => $this->pid, 'status' => $status]); + } + } +} diff --git a/src/User/Settings/Collection/UserGServers.php b/src/User/Settings/Collection/UserGServers.php index 689b801cf9..34897a1584 100644 --- a/src/User/Settings/Collection/UserGServers.php +++ b/src/User/Settings/Collection/UserGServers.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\User\Settings\Collection; diff --git a/src/User/Settings/Entity/UserGServer.php b/src/User/Settings/Entity/UserGServer.php index e5afdc51e7..f0e8a490db 100644 --- a/src/User/Settings/Entity/UserGServer.php +++ b/src/User/Settings/Entity/UserGServer.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\User\Settings\Entity; diff --git a/src/User/Settings/Factory/UserGServer.php b/src/User/Settings/Factory/UserGServer.php index 61abe28ccc..1034c27e49 100644 --- a/src/User/Settings/Factory/UserGServer.php +++ b/src/User/Settings/Factory/UserGServer.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\User\Settings\Factory; diff --git a/src/User/Settings/Repository/UserGServer.php b/src/User/Settings/Repository/UserGServer.php index baf70095ce..016f2ea70d 100644 --- a/src/User/Settings/Repository/UserGServer.php +++ b/src/User/Settings/Repository/UserGServer.php @@ -1,49 +1,34 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\User\Settings\Repository; use Exception; -use Friendica\BaseCollection; -use Friendica\BaseEntity; +use Friendica\BaseRepository; use Friendica\Content\Pager; use Friendica\Database\Database; use Friendica\Federation\Repository\GServer; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\NotFoundException; -use Friendica\User\Settings\Collection; -use Friendica\User\Settings\Entity; -use Friendica\User\Settings\Factory; +use Friendica\User\Settings\Collection\UserGServers as UserGServersCollection; +use Friendica\User\Settings\Entity\UserGServer as UserGServerEntity; +use Friendica\User\Settings\Factory\UserGServer as UserGServerFactory; use Psr\Log\LoggerInterface; -class UserGServer extends \Friendica\BaseRepository +class UserGServer extends BaseRepository { protected static $table_name = 'user-gserver'; - /** @var Factory\UserGServer */ + /** @var UserGServerFactory */ protected $factory; /** @var GServer */ protected $gserverRepository; - public function __construct(GServer $gserverRepository, Database $database, LoggerInterface $logger, Factory\UserGServer $factory) + public function __construct(GServer $gserverRepository, Database $database, LoggerInterface $logger, UserGServerFactory $factory) { parent::__construct($database, $logger, $factory); @@ -53,12 +38,9 @@ class UserGServer extends \Friendica\BaseRepository /** * Returns an existing UserGServer entity or create one on the fly * - * @param int $uid - * @param int $gsid * @param bool $hydrate Populate the related GServer entity - * @return Entity\UserGServer */ - public function getOneByUserAndServer(int $uid, int $gsid, bool $hydrate = true): Entity\UserGServer + public function getOneByUserAndServer(int $uid, int $gsid, bool $hydrate = true): UserGServerEntity { try { return $this->selectOneByUserAndServer($uid, $gsid, $hydrate); @@ -68,18 +50,15 @@ class UserGServer extends \Friendica\BaseRepository } /** - * @param int $uid - * @param int $gsid * @param bool $hydrate Populate the related GServer entity - * @return Entity\UserGServer * @throws NotFoundException */ - public function selectOneByUserAndServer(int $uid, int $gsid, bool $hydrate = true): Entity\UserGServer + public function selectOneByUserAndServer(int $uid, int $gsid, bool $hydrate = true): UserGServerEntity { return $this->_selectOne(['uid' => $uid, 'gsid' => $gsid], [], $hydrate); } - public function save(Entity\UserGServer $userGServer): Entity\UserGServer + public function save(UserGServerEntity $userGServer): UserGServerEntity { $fields = [ 'uid' => $userGServer->uid, @@ -92,7 +71,7 @@ class UserGServer extends \Friendica\BaseRepository return $userGServer; } - public function selectByUserWithPagination(int $uid, Pager $pager): Collection\UserGServers + public function selectByUserWithPagination(int $uid, Pager $pager): UserGServersCollection { return $this->_select(['uid' => $uid], ['limit' => [$pager->getStart(), $pager->getItemsPerPage()]]); } @@ -108,20 +87,18 @@ class UserGServer extends \Friendica\BaseRepository } /** - * @param Entity\UserGServer $userGServer - * @return bool * @throws InternalServerErrorException in case the underlying storage cannot delete the record */ - public function delete(Entity\UserGServer $userGServer): bool + public function delete(UserGServerEntity $userGServer): bool { try { return $this->db->delete(self::$table_name, ['uid' => $userGServer->uid, 'gsid' => $userGServer->gsid]); - } catch (\Exception $exception) { + } catch (Exception $exception) { throw new InternalServerErrorException('Cannot delete the UserGServer', $exception); } } - protected function _selectOne(array $condition, array $params = [], bool $hydrate = true): BaseEntity + protected function _selectOne(array $condition, array $params = [], bool $hydrate = true): UserGServerEntity { $fields = $this->db->selectFirst(static::$table_name, [], $condition, $params); if (!$this->db->isResult($fields)) { @@ -132,16 +109,13 @@ class UserGServer extends \Friendica\BaseRepository } /** - * @param array $condition - * @param array $params - * @return Collection\UserGServers * @throws Exception */ - protected function _select(array $condition, array $params = [], bool $hydrate = true): BaseCollection + protected function _select(array $condition, array $params = [], bool $hydrate = true): UserGServersCollection { $rows = $this->db->selectToArray(static::$table_name, [], $condition, $params); - $Entities = new Collection\UserGServers(); + $Entities = new UserGServersCollection(); foreach ($rows as $fields) { $Entities[] = $this->factory->createFromTableRow($fields, $hydrate ? $this->gserverRepository->selectOneById($fields['gsid']) : null); } @@ -149,7 +123,7 @@ class UserGServer extends \Friendica\BaseRepository return $Entities; } - public function listIgnoredByUser(int $uid): Collection\UserGServers + public function listIgnoredByUser(int $uid): UserGServersCollection { return $this->_select(['uid' => $uid, 'ignored' => 1], [], false); } diff --git a/src/Util/ACLFormatter.php b/src/Util/ACLFormatter.php index cd398fc280..4b0073dada 100644 --- a/src/Util/ACLFormatter.php +++ b/src/Util/ACLFormatter.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/Arrays.php b/src/Util/Arrays.php index 1d96be0512..03159625da 100644 --- a/src/Util/Arrays.php +++ b/src/Util/Arrays.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/BasePath.php b/src/Util/BasePath.php index 4811239295..e886eeda30 100644 --- a/src/Util/BasePath.php +++ b/src/Util/BasePath.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; @@ -33,8 +19,8 @@ class BasePath private $server; /** - * @param string|null $baseDir The default base path - * @param array $server server arguments + * @param string $baseDir The default base path + * @param array $server server arguments */ public function __construct(string $baseDir, array $server = []) { diff --git a/src/Util/Clock/FrozenClock.php b/src/Util/Clock/FrozenClock.php index 661d274850..59e1f6c8ab 100644 --- a/src/Util/Clock/FrozenClock.php +++ b/src/Util/Clock/FrozenClock.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\Clock; diff --git a/src/Util/Clock/SystemClock.php b/src/Util/Clock/SystemClock.php index a0c877e738..40339cb2f2 100644 --- a/src/Util/Clock/SystemClock.php +++ b/src/Util/Clock/SystemClock.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\Clock; diff --git a/src/Util/Crypto.php b/src/Util/Crypto.php index 20ab9bc528..084bd21808 100644 --- a/src/Util/Crypto.php +++ b/src/Util/Crypto.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\DI; use phpseclib3\Crypt\PublicKeyLoader; @@ -41,7 +26,7 @@ class Crypto public static function rsaSign($data, $key, $alg = 'sha256') { if (empty($key)) { - Logger::warning('Empty key parameter'); + DI::logger()->warning('Empty key parameter'); } openssl_sign($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg)); return $sig; @@ -57,7 +42,7 @@ class Crypto public static function rsaVerify($data, $sig, $key, $alg = 'sha256') { if (empty($key)) { - Logger::warning('Empty key parameter'); + DI::logger()->warning('Empty key parameter'); } return openssl_verify($data, $sig, $key, (($alg == 'sha1') ? OPENSSL_ALGO_SHA1 : $alg)); } @@ -94,7 +79,7 @@ class Crypto $result = openssl_pkey_new($openssl_options); if (empty($result)) { - Logger::notice('new_keypair: failed'); + DI::logger()->notice('new_keypair: failed'); return false; } @@ -104,7 +89,7 @@ class Crypto openssl_pkey_export($result, $response['prvkey']); // Get public key - $pkey = openssl_pkey_get_details($result); + $pkey = openssl_pkey_get_details($result); $response['pubkey'] = $pkey["key"]; return $response; @@ -175,19 +160,19 @@ class Crypto private static function encapsulateOther($data, $pubkey, $alg) { if (!$pubkey) { - Logger::notice('no key. data: '.$data); + DI::logger()->notice('no key. data: '.$data); } $fn = 'encrypt' . strtoupper($alg); if (method_exists(__CLASS__, $fn)) { - $result = ['encrypted' => true]; - $key = random_bytes(256); - $iv = random_bytes(256); + $result = ['encrypted' => true]; + $key = random_bytes(256); + $iv = random_bytes(256); $result['data'] = Strings::base64UrlEncode(self::$fn($data, $key, $iv), true); // log the offending call so we can track it down if (!openssl_public_encrypt($key, $k, $pubkey)) { $x = debug_backtrace(); - Logger::notice('RSA failed', ['trace' => $x[0]]); + DI::logger()->notice('RSA failed', ['trace' => $x[0]]); } $result['alg'] = $alg; @@ -217,18 +202,18 @@ class Crypto private static function encapsulateAes($data, $pubkey) { if (!$pubkey) { - Logger::notice('aes_encapsulate: no key. data: ' . $data); + DI::logger()->notice('aes_encapsulate: no key. data: ' . $data); } - $key = random_bytes(32); - $iv = random_bytes(16); - $result = ['encrypted' => true]; + $key = random_bytes(32); + $iv = random_bytes(16); + $result = ['encrypted' => true]; $result['data'] = Strings::base64UrlEncode(self::encryptAES256CBC($data, $key, $iv), true); // log the offending call so we can track it down if (!openssl_public_encrypt($key, $k, $pubkey)) { $x = debug_backtrace(); - Logger::notice('aes_encapsulate: RSA failed.', ['data' => $x[0]]); + DI::logger()->notice('aes_encapsulate: RSA failed.', ['data' => $x[0]]); } $result['alg'] = 'aes256cbc'; @@ -240,19 +225,18 @@ class Crypto } /** - * * Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php * * @param array $data ['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data] * @param string $prvkey The private key used for decryption. * - * @return string|boolean The decrypted string or false on failure. + * @return string|false The decrypted string or false on failure. * @throws \Exception */ public static function unencapsulate(array $data, $prvkey) { if (!$data) { - return; + return false; } $alg = $data['alg'] ?? 'aes256cbc'; @@ -314,11 +298,11 @@ class Crypto * Creates cryptographic secure random digits * * @param string $digits The count of digits - * @return int The random Digits + * @return string The random Digits * * @throws \Exception In case 'random_int' isn't usable */ - public static function randomDigits($digits) + public static function randomDigits($digits): string { $rn = ''; diff --git a/src/Util/DateTimeFormat.php b/src/Util/DateTimeFormat.php index e8b3482620..830b50a2fe 100644 --- a/src/Util/DateTimeFormat.php +++ b/src/Util/DateTimeFormat.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\Core\Logger; use DateTime; use DateTimeZone; use Exception; +use Friendica\DI; /** * Temporal class @@ -37,7 +23,7 @@ class DateTimeFormat const JSON = 'Y-m-d\TH:i:s.v\Z'; const API = 'D M d H:i:s +0000 Y'; - static $localTimezone = 'UTC'; + public static $localTimezone = 'UTC'; public static function setLocalTimeZone(string $timezone) { @@ -131,7 +117,7 @@ class DateTimeFormat $tz_to = 'UTC'; } - if (($s === '') || (!is_string($s))) { + if ($s === '') { $s = 'now'; } @@ -149,12 +135,13 @@ class DateTimeFormat } try { - $d = new DateTime($s, $from_obj); + $d = DateTime::createFromFormat('U', $s, $from_obj) + ?: new DateTime($s, $from_obj); } catch (Exception $e) { try { $d = new DateTime(self::fix($s), $from_obj); } catch (\Throwable $e) { - Logger::warning('DateTimeFormat::convert: exception: ' . $e->getMessage()); + DI::logger()->warning('DateTimeFormat::convert: exception: ' . $e->getMessage()); $d = new DateTime('now', $from_obj); } } @@ -190,6 +177,7 @@ class DateTimeFormat $pregPatterns = [ ['#(\w+), (\d+ \w+ \d+) (\d+:\d+:\d+) (.+)#', '$2 $3 $4'], ['#(\d+:\d+) (\w+), (\w+) (\d+), (\d+)#', '$1 $2 $3 $4 $5'], + ['#\[[^\]]*\]#', ''], // 2025-03-07T08:54:14.341+01:00[Europe/Berlin] ]; foreach ($pregPatterns as $pattern) { diff --git a/src/Util/EMailer/MailBuilder.php b/src/Util/EMailer/MailBuilder.php index 96e3a0928e..55f5d1ca67 100644 --- a/src/Util/EMailer/MailBuilder.php +++ b/src/Util/EMailer/MailBuilder.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\EMailer; diff --git a/src/Util/EMailer/NotifyMailBuilder.php b/src/Util/EMailer/NotifyMailBuilder.php index 4191dbba24..d53715a83e 100644 --- a/src/Util/EMailer/NotifyMailBuilder.php +++ b/src/Util/EMailer/NotifyMailBuilder.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\EMailer; @@ -123,9 +109,9 @@ class NotifyMailBuilder extends MailBuilder public function withPhoto(string $image, string $link, string $name) { $this->photo = [ - 'image' => $image ?? '', - 'link' => $link ?? '', - 'name' => $name ?? '', + 'image' => $image, + 'link' => $link, + 'name' => $name, ]; return $this; diff --git a/src/Util/EMailer/SystemMailBuilder.php b/src/Util/EMailer/SystemMailBuilder.php index 68e19ae83d..930587fe18 100644 --- a/src/Util/EMailer/SystemMailBuilder.php +++ b/src/Util/EMailer/SystemMailBuilder.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\EMailer; diff --git a/src/Util/Emailer.php b/src/Util/Emailer.php index 1accc574c3..3ced3e8afc 100644 --- a/src/Util/Emailer.php +++ b/src/Util/Emailer.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\App; +use Friendica\App\BaseURL; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Hook; use Friendica\Core\L10n; @@ -44,7 +30,7 @@ class Emailer private $pConfig; /** @var LoggerInterface */ private $logger; - /** @var App\BaseURL */ + /** @var BaseURL */ private $baseUrl; /** @var L10n */ private $l10n; @@ -54,14 +40,18 @@ class Emailer /** @var string */ private $siteEmailName; - public function __construct(IManageConfigValues $config, IManagePersonalConfigValues $pConfig, App\BaseURL $baseURL, LoggerInterface $logger, - L10n $defaultLang) - { - $this->config = $config; - $this->pConfig = $pConfig; - $this->logger = $logger; - $this->baseUrl = $baseURL; - $this->l10n = $defaultLang; + public function __construct( + IManageConfigValues $config, + IManagePersonalConfigValues $pConfig, + BaseURL $baseURL, + LoggerInterface $logger, + L10n $defaultLang + ) { + $this->config = $config; + $this->pConfig = $pConfig; + $this->logger = $logger; + $this->baseUrl = $baseURL; + $this->l10n = $defaultLang; $this->siteEmailAddress = $this->config->get('config', 'sender_email'); if (empty($this->siteEmailAddress)) { @@ -103,8 +93,14 @@ class Emailer */ public function newSystemMail() { - return new SystemMailBuilder($this->l10n, $this->baseUrl, $this->config, $this->logger, - $this->getSiteEmailAddress(), $this->getSiteEmailName()); + return new SystemMailBuilder( + $this->l10n, + $this->baseUrl, + $this->config, + $this->logger, + $this->getSiteEmailAddress(), + $this->getSiteEmailName() + ); } /** @@ -114,8 +110,14 @@ class Emailer */ public function newNotifyMail() { - return new NotifyMailBuilder($this->l10n, $this->baseUrl, $this->config, $this->logger, - $this->getSiteEmailAddress(), $this->getSiteEmailName()); + return new NotifyMailBuilder( + $this->l10n, + $this->baseUrl, + $this->config, + $this->logger, + $this->getSiteEmailAddress(), + $this->getSiteEmailName() + ); } /** @@ -130,7 +132,7 @@ class Emailer { Hook::callAll('emailer_send_prepare', $email); - if (empty($email)) { + if (! $email instanceof IEmail) { return true; } @@ -157,9 +159,9 @@ class Emailer // generate a mime boundary $mimeBoundary = rand(0, 9) . '-' - . rand(100000000, 999999999) . '-' - . rand(100000000, 999999999) . '=:' - . rand(10000, 99999); + . rand(100000000, 999999999) . '-' + . rand(100000000, 999999999) . '=:' + . rand(10000, 99999); $messageHeader = $email->getAdditionalMailHeaderString(); if ($countMessageId === 0) { @@ -177,9 +179,9 @@ class Emailer $textBody = chunk_split(base64_encode($email->getMessage(true))); $htmlBody = chunk_split(base64_encode($email->getMessage())); $multipartMessageBody = "--" . $mimeBoundary . "\n" . // plain text section - "Content-Type: text/plain; charset=UTF-8\n" . - "Content-Transfer-Encoding: base64\n\n" . - $textBody . "\n"; + "Content-Type: text/plain; charset=UTF-8\n" . + "Content-Transfer-Encoding: base64\n\n" . + $textBody . "\n"; if (!$email_textonly && !is_null($email->getMessage())) { $multipartMessageBody .= @@ -221,8 +223,7 @@ class Emailer $hookdata['parameters'] ); - $this->logger->debug('header ' . 'To: ' . $email->getToAddress() . '\n' . $messageHeader); - $this->logger->debug('return value ' . (($res) ? 'true' : 'false')); + $this->logger->debug('Email message header', ['To' => $email->getToAddress(), 'messageHeader' => $messageHeader, 'return' => ($res) ? 'true' : 'false']); return $res; } diff --git a/src/Util/HTTPHeaders.php b/src/Util/HTTPHeaders.php index 396e781016..dcfb620a30 100644 --- a/src/Util/HTTPHeaders.php +++ b/src/Util/HTTPHeaders.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/HTTPInputData.php b/src/Util/HTTPInputData.php index c56f6bd130..4237679931 100644 --- a/src/Util/HTTPInputData.php +++ b/src/Util/HTTPInputData.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index 9e130bc916..a779a59a86 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\Core\Logger; +use Exception; use Friendica\Core\Protocol; use Friendica\Database\Database; use Friendica\Database\DBA; @@ -29,11 +15,14 @@ use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Model\GServer; +use Friendica\Model\Item; use Friendica\Model\ItemURI; use Friendica\Model\User; use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; +use Friendica\Protocol\ActivityPub\Receiver; /** * Implements HTTP Signatures per draft-cavage-http-signatures-07. @@ -61,19 +50,21 @@ class HTTPSignature { $headers = null; $spoofable = false; - $result = [ - 'signer' => '', - 'header_signed' => false, - 'header_valid' => false + $result = [ + 'signer' => '', + 'header_signed' => false, + 'header_valid' => false ]; // Decide if $data arrived via controller submission or curl. $headers = []; - $headers['(request-target)'] = strtolower(DI::args()->getMethod()).' '.$_SERVER['REQUEST_URI']; + + $headers['(request-target)'] = strtolower(DI::args()->getMethod()) . ' ' . $_SERVER['REQUEST_URI']; foreach ($_SERVER as $k => $v) { if (strpos($k, 'HTTP_') === 0) { $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; } } @@ -83,7 +74,7 @@ class HTTPSignature $sig_block = self::parseSigheader($headers['authorization']); if (!$sig_block) { - Logger::notice('no signature provided.'); + DI::logger()->notice('no signature provided.'); return $result; } @@ -110,10 +101,11 @@ class HTTPSignature if ($key && function_exists($key)) { $result['signer'] = $sig_block['keyId']; + $key = $key($sig_block['keyId']); } - Logger::info('Got keyID ' . $sig_block['keyId']); + DI::logger()->info('Got keyID ' . $sig_block['keyId']); if (!$key) { return $result; @@ -121,7 +113,7 @@ class HTTPSignature $x = Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm); - Logger::info('verified: ' . $x); + DI::logger()->info('verified: ' . $x); if (!$x) { return $result; @@ -148,7 +140,7 @@ class HTTPSignature $return_headers = $head; } - $alg = 'sha512'; + $alg = 'sha512'; $algorithm = 'rsa-sha512'; $x = self::sign($head, $prvkey, $alg); @@ -170,7 +162,7 @@ class HTTPSignature */ private static function sign(array $head, string $prvkey, string $alg = 'sha256'): array { - $ret = []; + $ret = []; $headers = ''; $fields = ''; @@ -222,7 +214,7 @@ class HTTPSignature $headers = []; foreach ($matches as $match) { - $headers[$match[1]] = trim($match[2] ?: $match[3], '"'); + $headers[$match[1]] = trim((string) $match[2], '"'); } // if the header is encrypted, decrypt with (default) site private key and continue @@ -232,10 +224,10 @@ class HTTPSignature } $return = [ - 'keyId' => $headers['keyId'] ?? '', + 'keyId' => $headers['keyId'] ?? '', 'algorithm' => $headers['algorithm'] ?? 'rsa-sha256', - 'created' => $headers['created'] ?? null, - 'expires' => $headers['expires'] ?? null, + 'created' => $headers['created'] ?? null, + 'expires' => $headers['expires'] ?? null, 'headers' => explode(' ', $headers['headers'] ?? ''), 'signature' => base64_decode(preg_replace('/\s+/', '', $headers['signature'] ?? '')), ]; @@ -256,7 +248,7 @@ class HTTPSignature private static function decryptSigheader(array $headers, string $prvkey): string { if (!empty($headers['iv']) && !empty($headers['key']) && !empty($headers['data'])) { - return Crypto::unencapsulate($headers, $prvkey); + return (string) Crypto::unencapsulate($headers, $prvkey); } return ''; @@ -280,20 +272,20 @@ class HTTPSignature $content = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // Header data that is about to be signed. - $host = parse_url($target, PHP_URL_HOST); - $path = parse_url($target, PHP_URL_PATH); - $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); + $host = strtolower(parse_url($target, PHP_URL_HOST)); + $path = parse_url($target, PHP_URL_PATH); + $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); $content_length = strlen($content); - $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); + $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); $headers = [ - 'Date' => $date, + 'Date' => $date, 'Content-Length' => $content_length, - 'Digest' => $digest, - 'Host' => $host + 'Digest' => $digest, + 'Host' => $host ]; - $signed_data = "(request-target): post " . $path . "\ndate: ". $date . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; + $signed_data = "(request-target): post " . $path . "\ndate: " . $date . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); @@ -301,16 +293,69 @@ class HTTPSignature $headers['Content-Type'] = 'application/activity+json'; - $postResult = DI::httpClient()->post($target, $content, $headers, DI::config()->get('system', 'curl_timeout')); + $postResult = DI::httpClient()->post($target, $content, $headers, DI::config()->get('system', 'curl_timeout'), HttpClientRequest::ACTIVITYPUB); $return_code = $postResult->getReturnCode(); - Logger::info('Transmit to ' . $target . ' returned ' . $return_code); + DI::logger()->info('Transmit to ' . $target . ' returned ' . $return_code); self::setInboxStatus($target, ($return_code >= 200) && ($return_code <= 299)); + if (($return_code >= 200) && ($return_code <= 299)) { + Item::incrementOutbound(Protocol::ACTIVITYPUB); + } + return $postResult; } + /** + * Route activities locally + * + * @param array $data + * @param string $target + * @param array $owner + * @return boolean + */ + private static function routeLocal(array $data, string $target, array $owner): bool + { + $uid = self::getUserIdForInbox($target); + if (is_null($uid)) { + return false; + } + + $activity = JsonLD::compact($data); + $type = JsonLD::fetchElement($activity, '@type'); + $trust_source = true; + $object_data = Receiver::prepareObjectData($activity, $uid, true, $trust_source, $owner['url']); + if (empty($object_data)) { + return false; + } + + DI::logger()->debug('Process directly', ['uid' => $uid, 'target' => $target, 'type' => $type]); + return Receiver::routeActivities($object_data, $type, true, true, $uid); + } + + /** + * Fetch the user id for a given inbox + * + * @param string $inbox + * @return integer|null + */ + private static function getUserIdForInbox(string $inbox): ?int + { + $gsid = GServer::getID(DI::baseUrl()); + if (!$gsid) { + return null; + } + if (DBA::exists('apcontact', ['gsid' => $gsid, 'sharedinbox' => $inbox])) { + return 0; + } + $apcontact = DBA::selectFirst('apcontact', ['url'], ['gsid' => $gsid, 'inbox' => $inbox]); + if (empty($apcontact['url'])) { + return null; + } + return User::getIdForURL($apcontact['url']); + } + /** * Transmit given data to a target for a user * @@ -322,7 +367,16 @@ class HTTPSignature */ public static function transmit(array $data, string $target, array $owner): bool { - $postResult = self::post($data, $target, $owner); + if (DI::baseUrl()->isLocalUrl($target) && self::routeLocal($data, $target, $owner)) { + return true; + } + + try { + $postResult = self::post($data, $target, $owner); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return false; + } $return_code = $postResult->getReturnCode(); return ($return_code >= 200) && ($return_code <= 299); @@ -337,7 +391,7 @@ class HTTPSignature * @param int $gsid Server ID * @throws \Exception */ - static public function setInboxStatus(string $url, bool $success, bool $shared = false, int $gsid = null) + public static function setInboxStatus(string $url, bool $success, bool $shared = false, int $gsid = null) { $now = DateTimeFormat::utcNow(); @@ -351,7 +405,7 @@ class HTTPSignature $status = DBA::selectFirst('inbox-status', [], ['url' => $url]); if (empty($status)) { - Logger::warning('Unable to insert inbox-status row', $insertFields); + DI::logger()->warning('Unable to insert inbox-status row', $insertFields); return; } } @@ -384,7 +438,7 @@ class HTTPSignature $stamp1 = strtotime($status['success']); } - $stamp2 = strtotime($now); + $stamp2 = strtotime($now); $previous_stamp = strtotime($status['previous']); // Archive the inbox when there had been failures for five days. @@ -425,33 +479,50 @@ class HTTPSignature try { $curlResult = self::fetchRaw($request, $uid); } catch (\Exception $exception) { - Logger::notice('Error fetching url', ['url' => $request, 'exception' => $exception]); + DI::logger()->notice('Error fetching url', ['url' => $request, 'exception' => $exception]); return []; } - if (empty($curlResult)) { + if (!$curlResult->isSuccess() || empty($curlResult->getBodyString())) { + DI::logger()->debug('Fetching was unsuccessful', ['url' => $request, 'return-code' => $curlResult->getReturnCode(), 'error-number' => $curlResult->getErrorNumber(), 'error' => $curlResult->getError()]); return []; } - if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { - Logger::debug('Fetching was unsuccessful', ['url' => $request, 'return-code' => $curlResult->getReturnCode(), 'error-number' => $curlResult->getErrorNumber(), 'error' => $curlResult->getError()]); - return []; - } - - $content = json_decode($curlResult->getBody(), true); + $content = json_decode($curlResult->getBodyString(), true); if (empty($content) || !is_array($content)) { return []; } + if (!self::isValidContentType($curlResult->getContentType(), $request)) { + return []; + } + return $content; } + /** + * Check if the provided content type is a valid LD JSON mime type + * + * @param string $contentType + * @return boolean + */ + public static function isValidContentType(string $contentType, string $url = ''): bool + { + if (in_array(current(explode(';', $contentType)), ['application/activity+json', 'application/ld+json'])) { + return true; + } + + if (current(explode(';', $contentType)) == 'application/json') { + DI::logger()->notice('Unexpected content type, possibly from a remote system that is not standard compliant.', ['content-type' => $contentType, 'url' => $url]); + } + return false; + } + /** * Fetches raw data for a user * * @param string $request request url * @param integer $uid User id of the requester - * @param boolean $binary TRUE if asked to return binary results (file download) (default is "false") * @param array $opts (optional parameters) associative array with: * 'accept_content' => supply Accept: header with 'accept_content' as the value * 'timeout' => int Timeout in seconds, default system config value or 60 seconds @@ -467,26 +538,24 @@ class HTTPSignature if (!empty($uid)) { $owner = User::getOwnerDataById($uid); - if (!$owner) { - return; - } } else { $owner = User::getSystemAccount(); - if (!$owner) { - return; - } + } + + if (!$owner) { + throw new Exception('Could not find owner for uid ' . $uid); } if (!empty($owner['uprvkey'])) { // Header data that is about to be signed. - $host = parse_url($request, PHP_URL_HOST); + $host = strtolower(parse_url($request, PHP_URL_HOST)); $path = parse_url($request, PHP_URL_PATH); $date = DateTimeFormat::utcNow(DateTimeFormat::HTTP); $header['Date'] = $date; $header['Host'] = $host; - $signed_data = "(request-target): get " . $path . "\ndate: ". $date . "\nhost: " . $host; + $signed_data = "(request-target): get " . $path . "\ndate: " . $date . "\nhost: " . $host; $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); @@ -495,6 +564,7 @@ class HTTPSignature $curl_opts = $opts; $curl_opts[HttpClientOptions::HEADERS] = $header; + $curl_opts[HttpClientOptions::REQUEST] = HttpClientRequest::ACTIVITYPUB; if (!empty($opts['nobody'])) { $curlResult = DI::httpClient()->head($request, $curl_opts); @@ -503,7 +573,7 @@ class HTTPSignature } $return_code = $curlResult->getReturnCode(); - Logger::info('Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code); + DI::logger()->info('Fetched for user ' . $uid . ' from ' . $request . ' returned ' . $return_code); return $curlResult; } @@ -518,14 +588,14 @@ class HTTPSignature public static function getKeyIdContact(array $http_headers): array { if (empty($http_headers['HTTP_SIGNATURE'])) { - Logger::debug('No HTTP_SIGNATURE header', ['header' => $http_headers]); + DI::logger()->debug('No HTTP_SIGNATURE header', ['header' => $http_headers]); return []; } $sig_block = self::parseSigHeader($http_headers['HTTP_SIGNATURE']); if (empty($sig_block['keyId'])) { - Logger::debug('No keyId', ['sig_block' => $sig_block]); + DI::logger()->debug('No keyId', ['sig_block' => $sig_block]); return []; } @@ -536,23 +606,24 @@ class HTTPSignature /** * Gets a signer from a given HTTP request * - * @param string $content - * @param array $http_headers + * @param string $content Body of the request + * @param array $http_headers array containing the HTTP headers + * @param ?boolean $update true = always update, false = never update, null = update when not found or outdated * * @return string|null|false Signer * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function getSigner(string $content, array $http_headers) + public static function getSigner(string $content, array $http_headers, ?bool $update = null) { if (empty($http_headers['HTTP_SIGNATURE'])) { - Logger::debug('No HTTP_SIGNATURE header'); + DI::logger()->debug('No HTTP_SIGNATURE header'); return false; } if (!empty($content)) { $object = json_decode($content, true); if (empty($object)) { - Logger::info('No object'); + DI::logger()->info('No object'); return false; } @@ -562,11 +633,13 @@ class HTTPSignature } $headers = []; + $headers['(request-target)'] = strtolower(DI::args()->getMethod()) . ' ' . parse_url($http_headers['REQUEST_URI'], PHP_URL_PATH); // First take every header foreach ($http_headers as $k => $v) { $field = str_replace('_', '-', strtolower($k)); + $headers[$field] = $v; } @@ -574,6 +647,7 @@ class HTTPSignature foreach ($http_headers as $k => $v) { if (strpos($k, 'HTTP_') === 0) { $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; } } @@ -590,7 +664,7 @@ class HTTPSignature } if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) { - Logger::info('No headers or keyId'); + DI::logger()->info('No headers or keyId'); return false; } @@ -599,13 +673,13 @@ class HTTPSignature if (array_key_exists($h, $headers)) { $signed_data .= $h . ': ' . $headers[$h] . "\n"; } else { - Logger::info('Requested header field not found', ['field' => $h, 'header' => $headers]); + DI::logger()->info('Requested header field not found', ['field' => $h, 'header' => $headers]); } } $signed_data = rtrim($signed_data, "\n"); if (empty($signed_data)) { - Logger::info('Signed data is empty'); + DI::logger()->info('Signed data is empty'); return false; } @@ -628,18 +702,18 @@ class HTTPSignature } if (empty($algorithm)) { - Logger::info('No algorithm'); + DI::logger()->info('No algorithm'); return false; } - $key = self::fetchKey($sig_block['keyId'], $actor); + $key = self::fetchKey($sig_block['keyId'], $actor, $update); if (empty($key)) { - Logger::info('Empty key'); + DI::logger()->info('Empty key'); return false; } if (!empty($key['url']) && !empty($key['type']) && ($key['type'] == 'Tombstone')) { - Logger::info('Actor is a tombstone', ['key' => $key]); + DI::logger()->info('Actor is a tombstone', ['key' => $key]); if (!Contact::isLocal($key['url'])) { // We now delete everything that we possibly knew from this actor @@ -649,12 +723,12 @@ class HTTPSignature } if (empty($key['pubkey'])) { - Logger::info('Empty pubkey'); + DI::logger()->info('Empty pubkey'); return false; } if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key['pubkey'], $algorithm)) { - Logger::info('Verification failed', ['signed_data' => $signed_data, 'algorithm' => $algorithm, 'header' => $sig_block['headers'], 'http_headers' => $http_headers]); + DI::logger()->info('Verification failed', ['signed_data' => $signed_data, 'algorithm' => $algorithm, 'header' => $sig_block['headers'], 'http_headers' => $http_headers]); return false; } @@ -673,7 +747,7 @@ class HTTPSignature /// @todo add all hashes from the rfc if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { - Logger::info('Digest does not match'); + DI::logger()->info('Digest does not match'); return false; } @@ -689,9 +763,9 @@ class HTTPSignature } if (in_array('(expires)', $sig_block['headers']) && !empty($sig_block['expires'])) { - $expired = min($sig_block['expires'], $created + 300); + $expired = min($sig_block['expires'], $created + 3600); } else { - $expired = $created + 300; + $expired = $created + 3600; } // Check if the signed date field is in an acceptable range @@ -700,23 +774,23 @@ class HTTPSignature // Calculate with a grace period of 60 seconds to avoid slight time differences between the servers if (($created - 60) > $current) { - Logger::notice('Signature created in the future', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]); + DI::logger()->notice('Signature created in the future', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]); return false; } if ($current > $expired) { - Logger::notice('Signature expired', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]); + DI::logger()->notice('Signature expired', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]); return false; } - Logger::debug('Valid creation date', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]); + DI::logger()->debug('Valid creation date', ['created' => date(DateTimeFormat::MYSQL, $created), 'expired' => date(DateTimeFormat::MYSQL, $expired), 'current' => date(DateTimeFormat::MYSQL, $current)]); $hasGoodSignedContent = true; } // Check the content-length when it is part of the signed data if (in_array('content-length', $sig_block['headers'])) { if (strlen($content) != $headers['content-length']) { - Logger::info('Content length does not match'); + DI::logger()->info('Content length does not match'); return false; } } @@ -724,7 +798,7 @@ class HTTPSignature // Ensure that the authentication had been done with some content // Without this check someone could authenticate with fakeable data if (!$hasGoodSignedContent) { - Logger::info('No good signed content'); + DI::logger()->info('No good signed content'); return false; } @@ -734,29 +808,30 @@ class HTTPSignature /** * fetches a key for a given id and actor * - * @param string $id - * @param string $actor + * @param string $id keyId of the signature block + * @param string $actor Actor URI + * @param ?boolean $update true = always update, false = never update, null = update when not found or outdated * * @return array with actor url and public key * @throws \Exception */ - private static function fetchKey(string $id, string $actor): array + private static function fetchKey(string $id, string $actor, ?bool $update = null): array { $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); - $profile = APContact::getByURL($url); + $profile = APContact::getByURL($url, $update); if (!empty($profile)) { - Logger::info('Taking key from id', ['id' => $id]); + DI::logger()->info('Taking key from id', ['id' => $id]); return ['url' => $url, 'pubkey' => $profile['pubkey'], 'type' => $profile['type']]; } elseif ($url != $actor) { $profile = APContact::getByURL($actor); if (!empty($profile)) { - Logger::info('Taking key from actor', ['actor' => $actor]); + DI::logger()->info('Taking key from actor', ['actor' => $actor]); return ['url' => $actor, 'pubkey' => $profile['pubkey'], 'type' => $profile['type']]; } } - Logger::notice('Key could not be fetched', ['url' => $url, 'actor' => $actor]); + DI::logger()->notice('Key could not be fetched', ['url' => $url, 'actor' => $actor]); return []; } } diff --git a/src/Util/Images.php b/src/Util/Images.php index 1305441304..5fcdb29381 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -1,30 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\Core\Logger; +use Friendica\Core\Hook; use Friendica\DI; use Friendica\Model\Photo; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Object\Image; /** @@ -32,19 +19,107 @@ use Friendica\Object\Image; */ class Images { + // @todo add IMAGETYPE_AVIF once our minimal supported PHP version is 8.1.0 + const IMAGETYPES = [IMAGETYPE_WEBP, IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_BMP]; + /** - * Maps Mime types to Imagick formats + * Get the Imagick format for the given image type * - * @return array Format map + * @param int $imagetype + * @return string */ - public static function getFormatsMap() + public static function getImagickFormatByImageType(int $imagetype): string { - return [ - 'image/jpeg' => 'JPG', - 'image/jpg' => 'JPG', - 'image/png' => 'PNG', - 'image/gif' => 'GIF', + $formats = [ + // @todo add "IMAGETYPE_AVIF => 'AVIF'" once our minimal supported PHP version is 8.1.0 + IMAGETYPE_WEBP => 'WEBP', + IMAGETYPE_PNG => 'PNG', + IMAGETYPE_JPEG => 'JPEG', + IMAGETYPE_GIF => 'GIF', + IMAGETYPE_BMP => 'BMP', ]; + + if (empty($formats[$imagetype])) { + return ''; + } + + return $formats[$imagetype]; + } + + /** + * Sanitize the provided mime type, replace invalid mime types with valid ones. + * + * @param string $mimetype + * @return string + */ + private static function sanitizeMimeType(string $mimetype): string + { + $mimetype = current(explode(';', $mimetype)); + + if ($mimetype == 'image/jpg') { + $mimetype = image_type_to_mime_type(IMAGETYPE_JPEG); + } elseif (in_array($mimetype, ['image/vnd.mozilla.apng', 'image/apng'])) { + $mimetype = image_type_to_mime_type(IMAGETYPE_PNG); + } elseif (in_array($mimetype, ['image/x-ms-bmp', 'image/x-bmp'])) { + $mimetype = image_type_to_mime_type(IMAGETYPE_BMP); + } + + return $mimetype; + } + + /** + * Replace invalid extensions with valid ones. + * + * @param string $extension + * @return string + */ + private static function sanitizeExtensions(string $extension): string + { + if (in_array($extension, ['jpg', 'jpe', 'jfif'])) { + $extension = image_type_to_extension(IMAGETYPE_JPEG, false); + } elseif ($extension == 'apng') { + $extension = image_type_to_extension(IMAGETYPE_PNG, false); + } elseif ($extension == 'dib') { + $extension = image_type_to_extension(IMAGETYPE_BMP, false); + } + + return $extension; + } + + /** + * Get the image type for the given mime type + * + * @param string $mimetype + * @return integer + */ + public static function getImageTypeByMimeType(string $mimetype): int + { + $mimetype = self::sanitizeMimeType($mimetype); + + foreach (self::IMAGETYPES as $type) { + if ($mimetype == image_type_to_mime_type($type)) { + return $type; + } + } + + DI::logger()->debug('Undetected mimetype', ['mimetype' => $mimetype]); + return 0; + } + + /** + * Get the extension for the given image type + * + * @param integer $type + * @return string + */ + public static function getExtensionByImageType(int $type): string + { + if (empty($type)) { + DI::logger()->debug('Invalid image type', ['type' => $type]); + return ''; + } + + return image_type_to_extension($type); } /** @@ -55,104 +130,132 @@ class Images */ public static function getExtensionByMimeType(string $mimetype): string { - switch ($mimetype) { - case 'image/png': - $imagetype = IMAGETYPE_PNG; - break; - - case 'image/gif': - $imagetype = IMAGETYPE_GIF; - break; - - case 'image/jpeg': - case 'image/jpg': - $imagetype = IMAGETYPE_JPEG; - break; - - default: // Unknown type must be a blob then - return 'blob'; - break; + if (empty($mimetype)) { + return ''; } - return image_type_to_extension($imagetype); + return self::getExtensionByImageType(self::getImageTypeByMimeType($mimetype)); } /** - * Returns supported image mimetypes and corresponding file extensions + * Returns supported image mimetypes * * @return array */ - public static function supportedTypes(): array + public static function supportedMimeTypes(): array { - $types = [ - 'image/jpeg' => 'jpg', - 'image/jpg' => 'jpg', - ]; + $types = []; - if (class_exists('Imagick')) { - // Imagick::queryFormats won't help us a lot there... - // At least, not yet, other parts of friendica uses this array - $types += [ - 'image/png' => 'png', - 'image/gif' => 'gif' - ]; - } elseif (imagetypes() & IMG_PNG) { - $types += [ - 'image/png' => 'png' - ]; + // @todo enable, once our lowest supported PHP version is 8.1.0 + //if (imagetypes() & IMG_AVIF) { + // $types[] = image_type_to_mime_type(IMAGETYPE_AVIF); + //} + if (imagetypes() & IMG_WEBP) { + $types[] = image_type_to_mime_type(IMAGETYPE_WEBP); + } + if (imagetypes() & IMG_PNG) { + $types[] = image_type_to_mime_type(IMAGETYPE_PNG); + } + if (imagetypes() & IMG_JPG) { + $types[] = image_type_to_mime_type(IMAGETYPE_JPEG); + } + if (imagetypes() & IMG_GIF) { + $types[] = image_type_to_mime_type(IMAGETYPE_GIF); + } + if (imagetypes() & IMG_BMP) { + $types[] = image_type_to_mime_type(IMAGETYPE_BMP); } return $types; } /** - * Fetch image mimetype from the image data or guessing from the file name + * Checks if the provided mime type can be handled for resizing. + * Only with Imagick installed, animated GIF and WebP keep their animation after resize. * - * @param string $image_data Image data - * @param string $filename File name (for guessing the type via the extension) - * @param string $default Default MIME type - * @return string MIME type - * @throws \Exception + * @param string $mimetype + * @return boolean */ - public static function getMimeTypeByData(string $image_data, string $filename = '', string $default = ''): string + public static function canResize(string $mimetype): bool { - if (substr($default, 0, 6) == 'image/') { - Logger::info('Using default mime type', ['filename' => $filename, 'mime' => $default]); - return $default; + if (in_array(self::getImageTypeByMimeType($mimetype), [IMAGETYPE_GIF, IMAGETYPE_WEBP])) { + return class_exists('Imagick'); } - - $image = @getimagesizefromstring($image_data); - if (!empty($image['mime'])) { - Logger::info('Mime type detected via data', ['filename' => $filename, 'default' => $default, 'mime' => $image['mime']]); - return $image['mime']; - } - - return self::guessTypeByExtension($filename); + return true; } /** * Fetch image mimetype from the image data or guessing from the file name * - * @param string $sourcefile Source file of the image - * @param string $filename File name (for guessing the type via the extension) - * @param string $default default MIME type + * @param string $image_data Image data + * * @return string MIME type + * * @throws \Exception */ - public static function getMimeTypeBySource(string $sourcefile, string $filename = '', string $default = ''): string + public static function getMimeTypeByData(string $image_data): string { - if (substr($default, 0, 6) == 'image/') { - Logger::info('Using default mime type', ['filename' => $filename, 'mime' => $default]); - return $default; - } - - $image = @getimagesize($sourcefile); + $image = @getimagesizefromstring($image_data); if (!empty($image['mime'])) { - Logger::info('Mime type detected via file', ['filename' => $filename, 'default' => $default, 'image' => $image]); return $image['mime']; } - return self::guessTypeByExtension($filename); + DI::logger()->debug('Undetected mime type', ['image' => $image, 'size' => strlen($image_data)]); + + return ''; + } + + /** + * Checks if the provided mime type is supported by the system + * + * @param string $mimetype + * @return boolean + */ + public static function isSupportedMimeType(string $mimetype): bool + { + if (substr($mimetype, 0, 6) != 'image/') { + return false; + } + + return in_array(self::sanitizeMimeType($mimetype), self::supportedMimeTypes()); + } + + /** + * Checks if the provided mime type is supported. If not, it is fetched from the provided image data. + * + * @param string $mimetype + * @param string $image_data + * @return string + */ + public static function addMimeTypeByDataIfInvalid(string $mimetype, string $image_data): string + { + $mimetype = self::sanitizeMimeType($mimetype); + + if (($image_data == '') || self::isSupportedMimeType($mimetype)) { + return $mimetype; + } + + $alternative = self::getMimeTypeByData($image_data); + return $alternative ?: $mimetype; + } + + /** + * Checks if the provided mime type is supported. If not, it is fetched from the provided file name. + * + * @param string $mimetype + * @param string $filename + * @return string + */ + public static function addMimeTypeByExtensionIfInvalid(string $mimetype, string $filename): string + { + $mimetype = self::sanitizeMimeType($mimetype); + + if (($filename == '') || self::isSupportedMimeType($mimetype)) { + return $mimetype; + } + + $alternative = self::guessTypeByExtension($filename); + return $alternative ?: $mimetype; } /** @@ -164,27 +267,35 @@ class Images */ public static function guessTypeByExtension(string $filename): string { - $ext = pathinfo(parse_url($filename, PHP_URL_PATH), PATHINFO_EXTENSION); - $types = self::supportedTypes(); - $type = 'image/jpeg'; - foreach ($types as $m => $e) { - if ($ext == $e) { - $type = $m; + if (empty($filename)) { + return ''; + } + + $ext = strtolower(pathinfo(parse_url($filename, PHP_URL_PATH), PATHINFO_EXTENSION)); + $ext = self::sanitizeExtensions($ext); + if ($ext == '') { + return ''; + } + + foreach (self::IMAGETYPES as $type) { + if ($ext == image_type_to_extension($type, false)) { + return image_type_to_mime_type($type); } } - Logger::info('Mime type guessed via extension', ['filename' => $filename, 'type' => $type]); - return $type; + DI::logger()->debug('Unhandled extension', ['filename' => $filename, 'extension' => $ext]); + return ''; } /** * Gets info array from given URL, cached data has priority * * @param string $url + * @param bool $ocr * @return array Info * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function getInfoFromURLCached(string $url): array + public static function getInfoFromURLCached(string $url, bool $ocr = false): array { $data = []; @@ -192,27 +303,28 @@ class Images return $data; } - $cacheKey = 'getInfoFromURL:' . sha1($url); + $cacheKey = 'getInfoFromURL:' . sha1($url . $ocr); $data = DI::cache()->get($cacheKey); - if (empty($data) || !is_array($data)) { - $data = self::getInfoFromURL($url); + if (!is_array($data)) { + $data = self::getInfoFromURL($url, $ocr); DI::cache()->set($cacheKey, $data); } - return $data ?? []; + return $data; } /** * Gets info from URL uncached * * @param string $url + * @param bool $ocr * @return array Info array * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function getInfoFromURL(string $url): array + public static function getInfoFromURL(string $url, bool $ocr = false): array { $data = []; @@ -220,7 +332,7 @@ class Images return $data; } - if (Network::isLocalLink($url) && ($data = Photo::getResourceData($url))) { + if (DI::baseUrl()->isLocalUrl($url) && ($data = Photo::getResourceData($url))) { $photo = Photo::selectFirst([], ['resource-id' => $data['guid'], 'scale' => $data['scale']]); if (!empty($photo)) { $img_str = Photo::getImageDataForPhoto($photo); @@ -230,9 +342,9 @@ class Images if (empty($img_str)) { try { - $img_str = DI::httpClient()->fetch($url, HttpClientAccept::IMAGE, 4); + $img_str = DI::httpClient()->fetch($url, HttpClientAccept::IMAGE, 4, '', HttpClientRequest::MEDIAVERIFIER); } catch (\Exception $exception) { - Logger::notice('Image is invalid', ['url' => $url, 'exception' => $exception]); + DI::logger()->notice('Image is invalid', ['url' => $url, 'exception' => $exception]); return []; } } @@ -253,10 +365,18 @@ class Images return []; } - $image = new Image($img_str); + $image = new Image($img_str, '', $url, false); if ($image->isValid()) { - $data['blurhash'] = $image->getBlurHash(); + $data['blurhash'] = $image->getBlurHash($img_str); + + if ($ocr) { + $media = ['img_str' => $img_str]; + Hook::callAll('ocr-detection', $media); + if (!empty($media['description'])) { + $data['description'] = $media['description']; + } + } } $data['size'] = $filesize; @@ -283,34 +403,34 @@ class Images // constrain the width - let the height float. if ((($height * 9) / 16) > $width) { - $dest_width = $max; - $dest_height = intval(($height * $max) / $width); + $dest_width = $max; + $dest_height = intval(ceil(($height * $max) / $width)); } elseif ($width > $height) { // else constrain both dimensions - $dest_width = $max; - $dest_height = intval(($height * $max) / $width); + $dest_width = $max; + $dest_height = intval(ceil(($height * $max) / $width)); } else { - $dest_width = intval(($width * $max) / $height); + $dest_width = intval(ceil(($width * $max) / $height)); $dest_height = $max; } } else { if ($width > $max) { - $dest_width = $max; - $dest_height = intval(($height * $max) / $width); + $dest_width = $max; + $dest_height = intval(ceil(($height * $max) / $width)); } else { if ($height > $max) { // very tall image (greater than 16:9) // but width is OK - don't do anything if ((($height * 9) / 16) > $width) { - $dest_width = $width; + $dest_width = $width; $dest_height = $height; } else { - $dest_width = intval(($width * $max) / $height); + $dest_width = intval(ceil(($width * $max) / $height)); $dest_height = $max; } } else { - $dest_width = $width; + $dest_width = $width; $dest_height = $height; } } @@ -333,7 +453,7 @@ class Images { return self::getBBCodeByUrl( DI::baseUrl() . '/photos/' . $nickname . '/image/' . $resource_id, - DI::baseUrl() . '/photo/' . $resource_id . '-' . $preview. '.' . $ext, + DI::baseUrl() . '/photo/' . $resource_id . '-' . $preview . $ext, $description ); } diff --git a/src/Util/JsonLD.php b/src/Util/JsonLD.php index 5bf6593aed..afca8d79ba 100644 --- a/src/Util/JsonLD.php +++ b/src/Util/JsonLD.php @@ -1,31 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; use Exception; use Friendica\Core\System; use Friendica\DI; +use Friendica\Protocol\ActivityPub; +use stdClass; /** * This class contain methods to work with JsonLD data @@ -58,6 +45,9 @@ class JsonLD case 'https://www.w3.org/ns/activitystreams': $url = DI::basePath() . '/static/activitystreams.jsonld'; break; + case 'https://www.w3.org/ns/did/v1': + $url = DI::basePath() . '/static/did-v1.jsonld'; + break; case 'https://funkwhale.audio/ns': $url = DI::basePath() . '/static/funkwhale.audio.jsonld'; break; @@ -67,9 +57,12 @@ class JsonLD case 'http://joinmastodon.org/ns': $url = DI::basePath() . '/static/joinmastodon.jsonld'; break; + case 'https://purl.archive.org/socialweb/webfinger': + $url = DI::basePath() . '/static/socialweb-webfinger.jsonld'; + break; default: switch (parse_url($url, PHP_URL_PATH)) { - case '/schemas/litepub-0.1.jsonld'; + case '/schemas/litepub-0.1.jsonld': $url = DI::basePath() . '/static/litepub-0.1.jsonld'; break; case '/apschema/v1.2': @@ -78,7 +71,7 @@ class JsonLD $url = DI::basePath() . '/static/apschema.jsonld'; break; default: - Logger::info('Got url', ['url' =>$url]); + DI::logger()->info('Got url', ['url' => $url]); break; } } @@ -88,14 +81,14 @@ class JsonLD $x = debug_backtrace(); if ($x) { foreach ($x as $n) { - if ($n['function'] === __FUNCTION__) { - $recursion ++; + if ($n['function'] === __FUNCTION__) { + $recursion++; } } } if ($recursion > 5) { - Logger::error('jsonld bomb detected at: ' . $url); + DI::logger()->error('jsonld bomb detected at: ' . $url); System::exit(); } @@ -124,20 +117,18 @@ class JsonLD $jsonobj = json_decode(json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); try { - $normalized = jsonld_normalize($jsonobj, array('algorithm' => 'URDNA2015', 'format' => 'application/nquads')); - } - catch (Exception $e) { - $normalized = false; - $messages = []; + $normalized = jsonld_normalize($jsonobj, ['algorithm' => 'URDNA2015', 'format' => 'application/nquads']); + } catch (Exception $e) { + $normalized = false; + $messages = []; $currentException = $e; do { $messages[] = $currentException->getMessage(); - } while($currentException = $currentException->getPrevious()); + } while ($currentException = $currentException->getPrevious()); - Logger::warning('JsonLD normalize error'); - Logger::notice('JsonLD normalize error', ['messages' => $messages]); - Logger::info('JsonLD normalize error', ['trace' => $e->getTraceAsString()]); - Logger::debug('JsonLD normalize error', ['jsonobj' => $jsonobj]); + DI::logger()->notice('JsonLD normalize error', ['messages' => $messages]); + DI::logger()->info('JsonLD normalize error', ['trace' => $e->getTraceAsString()]); + DI::logger()->debug('JsonLD normalize error', ['jsonobj' => $jsonobj]); } return $normalized; @@ -156,26 +147,58 @@ class JsonLD { jsonld_set_document_loader('Friendica\Util\JsonLD::documentLoader'); - $context = (object)['as' => 'https://www.w3.org/ns/activitystreams#', - 'w3id' => 'https://w3id.org/security#', - 'ldp' => (object)['@id' => 'http://www.w3.org/ns/ldp#', '@type' => '@id'], - 'vcard' => (object)['@id' => 'http://www.w3.org/2006/vcard/ns#', '@type' => '@id'], - 'dfrn' => (object)['@id' => 'http://purl.org/macgirvin/dfrn/1.0/', '@type' => '@id'], - 'diaspora' => (object)['@id' => 'https://diasporafoundation.org/ns/', '@type' => '@id'], - 'ostatus' => (object)['@id' => 'http://ostatus.org#', '@type' => '@id'], - 'dc' => (object)['@id' => 'http://purl.org/dc/terms/', '@type' => '@id'], - 'toot' => (object)['@id' => 'http://joinmastodon.org/ns#', '@type' => '@id'], - 'litepub' => (object)['@id' => 'http://litepub.social/ns#', '@type' => '@id'], - 'sc' => (object)['@id' => 'http://schema.org#', '@type' => '@id'], - 'pt' => (object)['@id' => 'https://joinpeertube.org/ns#', '@type' => '@id'], + $context = (object)[ + 'as' => 'https://www.w3.org/ns/activitystreams#', + 'w3id' => 'https://w3id.org/security#', + 'ldp' => (object)['@id' => 'http://www.w3.org/ns/ldp#', '@type' => '@id'], + 'vcard' => (object)['@id' => 'http://www.w3.org/2006/vcard/ns#', '@type' => '@id'], + 'dfrn' => (object)['@id' => 'http://purl.org/macgirvin/dfrn/1.0/', '@type' => '@id'], + 'diaspora' => (object)['@id' => 'https://diasporafoundation.org/ns/', '@type' => '@id'], + 'ostatus' => (object)['@id' => 'http://ostatus.org#', '@type' => '@id'], + 'dc' => (object)['@id' => 'http://purl.org/dc/terms/', '@type' => '@id'], + 'toot' => (object)['@id' => 'http://joinmastodon.org/ns#', '@type' => '@id'], + 'litepub' => (object)['@id' => 'http://litepub.social/ns#', '@type' => '@id'], + 'sc' => (object)['@id' => 'http://schema.org#', '@type' => '@id'], + 'pt' => (object)['@id' => 'https://joinpeertube.org/ns#', '@type' => '@id'], 'mobilizon' => (object)['@id' => 'https://joinmobilizon.org/ns#', '@type' => '@id'], - 'fedibird' => (object)['@id' => 'http://fedibird.com/ns#', '@type' => '@id'], - 'misskey' => (object)['@id' => 'https://misskey-hub.net/ns#', '@type' => '@id'], - 'pixelfed' => (object)['@id' => 'http://pixelfed.org/ns#', '@type' => '@id'], + 'fedibird' => (object)['@id' => 'http://fedibird.com/ns#', '@type' => '@id'], + 'misskey' => (object)['@id' => 'https://misskey-hub.net/ns#', '@type' => '@id'], + 'pixelfed' => (object)['@id' => 'http://pixelfed.org/ns#', '@type' => '@id'], + 'lemmy' => (object)['@id' => 'https://join-lemmy.org/ns#', '@type' => '@id'], ]; $orig_json = $json; + $jsonobj = self::fixInvalidJsonLD($json); + + try { + $compacted = jsonld_compact($jsonobj, $context); + } catch (Exception $e) { + $compacted = false; + DI::logger()->notice('compacting error', ['msg' => $e->getMessage(), 'previous' => $e->getPrevious(), 'line' => $e->getLine()]); + if ($logfailed && DI::config()->get('debug', 'ap_log_failure')) { + $tempfile = tempnam(System::getTempPath(), 'failed-jsonld'); + file_put_contents($tempfile, json_encode(['json' => $orig_json, 'msg' => $e->getMessage(), 'previous' => $e->getPrevious()], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + DI::logger()->notice('Failed message stored', ['file' => $tempfile]); + } + } + + $json = json_decode(json_encode($compacted, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), true); + + if ($json === false) { + DI::logger()->notice('JSON encode->decode failed', ['orig_json' => $orig_json, 'compacted' => $compacted]); + $json = []; + } + + return $json; + } + + private static function fixInvalidJsonLD(array $json): stdClass + { + if (empty($json['@context'])) { + $json['@context'] = ActivityPub::CONTEXT; + } + // Preparation for adding possibly missing content to the context if (!empty($json['@context']) && is_string($json['@context'])) { $json['@context'] = [$json['@context']]; @@ -188,50 +211,43 @@ class JsonLD // Workaround for servers with missing context // See issue https://github.com/nextcloud/social/issues/330 if (!in_array('https://w3id.org/security/v1', $json['@context'])) { + DI::logger()->debug('Missing security context'); $json['@context'][] = 'https://w3id.org/security/v1'; } } + // Issue 14448: Peertube transmits an unexpected type and schema URL. + array_walk_recursive($json['@context'], function (&$value, $key) { + if ($key == '@type' && $value == '@json') { + DI::logger()->debug('"@json" converted to "@id"'); + $value = '@id'; + } + if ($key == 'sc' && $value == 'http://schema.org/') { + DI::logger()->debug('schema.org path fixed'); + $value = 'http://schema.org#'; + } + // Issue 14630: Wordpress Event Bridge uses a URL that cannot be retrieved + if (is_int($key) && $value == 'https://schema.org/') { + DI::logger()->debug('https schema.org path fixed'); + $value = 'https://schema.org/docs/jsonldcontext.json#'; + } + }); + // Bookwyrm transmits "id" fields with "null", which isn't allowed. array_walk_recursive($json, function (&$value, $key) { if ($key == 'id' && is_null($value)) { + DI::logger()->debug('Fixed null id'); $value = ''; } }); - $jsonobj = json_decode(json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); - - try { - $compacted = jsonld_compact($jsonobj, $context); - } - catch (Exception $e) { - $compacted = false; - Logger::notice('compacting error', ['msg' => $e->getMessage(), 'previous' => $e->getPrevious(), 'line' => $e->getLine()]); - if ($logfailed && DI::config()->get('debug', 'ap_log_failure')) { - $tempfile = tempnam(System::getTempPath(), 'failed-jsonld'); - file_put_contents($tempfile, json_encode(['json' => $orig_json, 'msg' => $e->getMessage(), 'previous' => $e->getPrevious()], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); - Logger::notice('Failed message stored', ['file' => $tempfile]); - } - } - - $json = json_decode(json_encode($compacted, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), true); - - if ($json === false) { - Logger::notice('JSON encode->decode failed', ['orig_json' => $orig_json, 'compacted' => $compacted]); - $json = []; - } - - return $json; + return json_decode(json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } /** * Fetches an element array from a JSON array * - * @param $array - * @param $element - * @param $key - * - * @return array fetched element + * @return array|null fetched element or null */ public static function fetchElementArray($array, $element, $key = null, $type = null, $type_value = null) { @@ -270,7 +286,7 @@ class JsonLD * @param $type * @param $type_value * - * @return string fetched element + * @return string|null fetched element */ public static function fetchElement($array, $element, $key = '@id', $type = null, $type_value = null) { diff --git a/src/Util/LDSignature.php b/src/Util/LDSignature.php index 8cfadb16da..f5c86d4843 100644 --- a/src/Util/LDSignature.php +++ b/src/Util/LDSignature.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\Core\Logger; +use Friendica\DI; use Friendica\Model\APContact; /** @@ -69,7 +55,7 @@ class LDSignature $dhash = self::hash(self::signableData($data)); $x = Crypto::rsaVerify($ohash . $dhash, base64_decode($data['signature']['signatureValue']), $pubkey); - Logger::info('LD-verify', ['verified' => (int)$x, 'actor' => $profile['url']]); + DI::logger()->info('LD-verify', ['verified' => (int)$x, 'actor' => $profile['url']]); if (empty($x)) { return false; @@ -88,14 +74,14 @@ class LDSignature public static function sign(array $data, array $owner): array { $options = [ - 'type' => 'RsaSignature2017', - 'nonce' => Strings::getRandomHex(64), + 'type' => 'RsaSignature2017', + 'nonce' => Strings::getRandomHex(64), 'creator' => $owner['url'] . '#main-key', 'created' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), ]; - $ohash = self::hash(self::signableOptions($options)); - $dhash = self::hash(self::signableData($data)); + $ohash = self::hash(self::signableOptions($options)); + $dhash = self::hash(self::signableData($data)); $options['signatureValue'] = base64_encode(Crypto::rsaSign($ohash . $dhash, $owner['uprvkey'])); return array_merge($data, ['signature' => $options]); @@ -133,7 +119,7 @@ class LDSignature /** * Hashes normalized object * - * @param ??? $obj + * @param array $obj * @return string SHA256 hash */ private static function hash($obj): string diff --git a/src/Util/Map.php b/src/Util/Map.php index fdc480b5e3..f0ebad3cbc 100644 --- a/src/Util/Map.php +++ b/src/Util/Map.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/Mimetype.php b/src/Util/Mimetype.php index 1a046e76f6..80cb085253 100644 --- a/src/Util/Mimetype.php +++ b/src/Util/Mimetype.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/Network.php b/src/Util/Network.php index b2f472ba77..2172c5a177 100644 --- a/src/Util/Network.php +++ b/src/Util/Network.php @@ -1,39 +1,25 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\DI; +use Friendica\Event\ArrayFilterEvent; use Friendica\Model\Contact; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Network\HTTPException\NotModifiedException; +use GuzzleHttp\Psr7\Exception\MalformedUriException; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; class Network { - /** * Return raw post data from a post request * @@ -71,14 +57,15 @@ class Network } $xrd_timeout = DI::config()->get('system', 'xrd_timeout'); - $host = parse_url($url, PHP_URL_HOST); + $host = parse_url($url, PHP_URL_HOST); if (empty($host) || !(filter_var($host, FILTER_VALIDATE_IP) || @dns_get_record($host . '.', DNS_A + DNS_AAAA))) { return false; } if (in_array(parse_url($url, PHP_URL_SCHEME), ['https', 'http'])) { - $options = [HttpClientOptions::VERIFY => true, HttpClientOptions::TIMEOUT => $xrd_timeout]; + $options = [HttpClientOptions::VERIFY => true, HttpClientOptions::TIMEOUT => $xrd_timeout, + HttpClientOptions::REQUEST => HttpClientRequest::URLVERIFIER]; try { $curlResult = DI::httpClient()->head($url, $options); } catch (\Exception $e) { @@ -90,12 +77,13 @@ class Network try { $curlResult = DI::httpClient()->get($url, HttpClientAccept::DEFAULT, $options); } catch (\Exception $e) { + DI::logger()->notice('Got exception', ['code' => $e->getCode(), 'message' => $e->getMessage()]); return false; } } if (!$curlResult->isSuccess()) { - Logger::notice('Url not reachable', ['host' => $host, 'url' => $url]); + DI::logger()->notice('Url not reachable', ['host' => $host, 'url' => $url]); return false; } elseif ($curlResult->isRedirectUrl()) { $url = $curlResult->getRedirectUrl(); @@ -194,7 +182,7 @@ class Network try { return self::isUriBlocked(new Uri($url)); } catch (\Throwable $e) { - Logger::warning('Invalid URL', ['url' => $url]); + DI::logger()->warning('Invalid URL', ['url' => $url]); return false; } } @@ -217,7 +205,7 @@ class Network } foreach ($domain_blocklist as $domain_block) { - if (fnmatch(strtolower($domain_block['domain']), strtolower($uri->getHost()))) { + if (!empty($domain_block['domain']) && fnmatch(strtolower($domain_block['domain']), strtolower($uri->getHost()))) { return true; } } @@ -271,14 +259,17 @@ class Network return false; } - $str_allowed = DI::config()->get('system', 'allowed_email', ''); - if (empty($str_allowed)) { + $allowed = DI::config()->get('system', 'allowed_email'); + if (!empty($allowed) && self::isDomainMatch($domain, explode(',', $allowed))) { return true; } - $allowed = explode(',', $str_allowed); + $disallowed = DI::config()->get('system', 'disallowed_email'); + if (!empty($disallowed) && self::isDomainMatch($domain, explode(',', $disallowed))) { + return false; + } - return self::isDomainAllowed($domain, $allowed); + return true; } /** @@ -289,7 +280,7 @@ class Network * * @return boolean */ - public static function isDomainAllowed(string $domain, array $domain_list): bool + public static function isDomainMatch(string $domain, array $domain_list): bool { $found = false; @@ -306,18 +297,22 @@ class Network public static function lookupAvatarByEmail(string $email): string { - $avatar['size'] = 300; - $avatar['email'] = $email; - $avatar['url'] = ''; + $avatar['size'] = 300; + $avatar['email'] = $email; + $avatar['url'] = ''; $avatar['success'] = false; - Hook::callAll('avatar_lookup', $avatar); + $eventDispatcher = DI::eventDispatcher(); + + $avatar = $eventDispatcher->dispatch( + new ArrayFilterEvent(ArrayFilterEvent::AVATAR_LOOKUP, $avatar), + )->getArray(); if (! $avatar['success']) { $avatar['url'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO; } - Logger::info('Avatar: ' . $avatar['email'] . ' ' . $avatar['url']); + DI::logger()->info('Avatar: ' . $avatar['email'] . ' ' . $avatar['url']); return $avatar['url']; } @@ -348,18 +343,18 @@ class Network 'fb_action_ids', 'fb_action_types', 'fb_ref', 'awesm', 'wtrid', 'woo_campaign', 'woo_source', 'woo_medium', 'woo_content', 'woo_term'] - ) + ) ) { $pair = $param . '=' . urlencode($value); - $url = str_replace($pair, '', $url); + $url = str_replace($pair, '', $url); // Second try: if the url isn't encoded completely $pair = $param . '=' . str_replace(' ', '+', $value); - $url = str_replace($pair, '', $url); + $url = str_replace($pair, '', $url); // Third try: Maybe the url isn't encoded at all $pair = $param . '=' . $value; - $url = str_replace($pair, '', $url); + $url = str_replace($pair, '', $url); $url = str_replace(['?&', '&&'], ['?', ''], $url); } @@ -391,11 +386,11 @@ class Network $base = [ 'scheme' => parse_url($basepath, PHP_URL_SCHEME), - 'host' => parse_url($basepath, PHP_URL_HOST), + 'host' => parse_url($basepath, PHP_URL_HOST), ]; $parts = array_merge($base, parse_url('/' . ltrim($url, '/'))); - return self::unparseURL($parts); + return (string)Uri::fromParts((array)$parts); } /** @@ -471,7 +466,7 @@ class Network $pathparts1 = explode('/', $parts1['path']); $pathparts2 = explode('/', $parts2['path']); - $i = 0; + $i = 0; $path = ''; do { $path1 = $pathparts1[$i] ?? ''; @@ -487,38 +482,6 @@ class Network return Strings::normaliseLink($match); } - /** - * Glue url parts together - * - * @param array $parsed URL parts - * - * @return string|null The glued URL or null on error - * @deprecated since version 2021.12, use GuzzleHttp\Psr7\Uri::fromParts($parts) instead - */ - public static function unparseURL(array $parsed): string - { - $get = function ($key) use ($parsed) { - return isset($parsed[$key]) ? $parsed[$key] : null; - }; - - $pass = $get('pass'); - $user = $get('user'); - $userinfo = $pass !== null ? "$user:$pass" : $user; - $port = $get('port'); - $scheme = $get('scheme'); - $query = $get('query'); - $fragment = $get('fragment'); - $authority = ($userinfo !== null ? $userinfo . '@' : '') . - $get('host') . - ($port ? ":$port" : ''); - - return (!empty($scheme) ? $scheme . ':' : '') . - (!empty($authority) ? '//' . $authority : '') . - $get('path') . - (!empty($query) ? '?' . $query : '') . - (!empty($fragment) ? '#' . $fragment : ''); - } - /** * Convert an URI to an IDN compatible URI * @@ -530,20 +493,29 @@ class Network { $parts = parse_url($uri); if (!empty($parts['scheme']) && !empty($parts['host'])) { - $parts['host'] = idn_to_ascii($parts['host']); - $uri = (string)Uri::fromParts($parts); + $parts['host'] = self::idnToAscii($parts['host']); + $uri = (string)Uri::fromParts($parts); } else { $parts = explode('@', $uri); if (count($parts) == 2) { - $uri = $parts[0] . '@' . idn_to_ascii($parts[1]); + $uri = $parts[0] . '@' . self::idnToAscii($parts[1]); } else { - $uri = idn_to_ascii($uri); + $uri = self::idnToAscii($uri); } } return $uri; } + private static function idnToAscii(string $uri): string + { + if (!function_exists('idn_to_ascii')) { + DI::logger()->error('IDN functions are missing.'); + return $uri; + } + return idn_to_ascii($uri); + } + /** * Switch the scheme of an url between http and https * @@ -588,7 +560,7 @@ class Network $parsed['query'] = http_build_query($params); - return self::unparseURL($parsed); + return (string)Uri::fromParts((array)$parsed); } /** @@ -634,19 +606,6 @@ class Network } } - /** - * Check if the given URL is a local link - * - * @param string $url - * - * @return bool - * @deprecated since 2023.09, please use BaseUrl->isLocalUrl or BaseUrl->isLocalUri instead. - */ - public static function isLocalLink(string $url): bool - { - return DI::baseUrl()->isLocalUrl($url); - } - /** * Check if the given URL is a valid HTTP/HTTPS URL * @@ -659,6 +618,29 @@ class Network return !empty($scheme) && in_array($scheme, ['http', 'https']) && parse_url($url, PHP_URL_HOST); } + /** + * Remove invalid parts from an URL + * + * @param string $url + * @return string sanitized URL + */ + public static function sanitizeUrl(string $url): string + { + $sanitized = $url = trim($url); + + foreach (['"', ' '] as $character) { + $pos = strpos($sanitized, $character); + if ($pos !== false) { + $sanitized = trim(substr($sanitized, 0, $pos)); + } + } + + if ($sanitized != $url) { + DI::logger()->debug('Link got sanitized', ['url' => $url, 'sanitzed' => $sanitized]); + } + return $sanitized; + } + /** * Creates an Uri object out of a given Uri string * @@ -674,8 +656,47 @@ class Network try { return new Uri($uri); } catch (\Exception $e) { - Logger::debug('Invalid URI', ['code' => $e->getCode(), 'message' => $e->getMessage(), 'uri' => $uri]); + DI::logger()->debug('Invalid URI', ['code' => $e->getCode(), 'message' => $e->getMessage(), 'uri' => $uri]); return null; } } + + /** + * Remove an Url parameter + * + * @param string $url + * @param string $parameter + * @return string + * @throws MalformedUriException + */ + public static function removeUrlParameter(string $url, string $parameter): string + { + $parts = parse_url($url); + if (empty($parts['query'])) { + return $url; + } + + parse_str($parts['query'], $data); + + unset($data[$parameter]); + + $parts['query'] = http_build_query($data); + + return (string)Uri::fromParts($parts); + } + + /** + * Get base url without a path, fragment or query + * + * @param UriInterface $uri + * @return string baseurl + */ + public static function getBaseUrl(UriInterface $uri): string + { + return $uri + ->withUserInfo('') + ->withQuery('') + ->withFragment('') + ->withPath(''); + } } diff --git a/src/Util/ParseUrl.php b/src/Util/ParseUrl.php index 79f427a654..ad50269186 100644 --- a/src/Util/ParseUrl.php +++ b/src/Util/ParseUrl.php @@ -1,39 +1,24 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; use DOMDocument; use DOMXPath; -use Friendica\Content\OEmbed; use Friendica\Content\Text\HTML; use Friendica\Protocol\HTTP\MediaType; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPException; use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; /** * Get information about a given URL @@ -65,9 +50,9 @@ class ParseUrl public static function getContentType(string $url, string $accept = HttpClientAccept::DEFAULT, int $timeout = 0): array { if (!empty($timeout)) { - $options = [HttpClientOptions::TIMEOUT => $timeout]; + $options = [HttpClientOptions::TIMEOUT => $timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTENTTYPE]; } else { - $options = []; + $options = [HttpClientOptions::REQUEST => HttpClientRequest::CONTENTTYPE]; } try { @@ -79,15 +64,20 @@ class ParseUrl // Workaround for systems that can't handle a HEAD request. Don't retry on timeouts. if (!$curlResult->isSuccess() && ($curlResult->getReturnCode() >= 400) && !in_array($curlResult->getReturnCode(), [408, 504])) { - $curlResult = DI::httpClient()->get($url, $accept, array_merge([HttpClientOptions::CONTENT_LENGTH => 1000000], $options)); + try { + $curlResult = DI::httpClient()->get($url, $accept, array_merge([HttpClientOptions::CONTENT_LENGTH => 1000000], $options)); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return []; + } } if (!$curlResult->isSuccess()) { - Logger::debug('Got HTTP Error', ['http error' => $curlResult->getReturnCode(), 'url' => $url]); + DI::logger()->debug('Got HTTP Error', ['http error' => $curlResult->getReturnCode(), 'url' => $url]); return []; } - $contenttype = $curlResult->getHeader('Content-Type')[0] ?? ''; + $contenttype = $curlResult->getContentType(); if (empty($contenttype)) { return ['application', 'octet-stream']; } @@ -98,9 +88,8 @@ class ParseUrl /** * Search for cached embeddable data of an url otherwise fetch it * - * @param string $url The url of the page which should be scraped - * @param bool $do_oembed The false option is used by the function fetch_oembed() - * to avoid endless loops + * @param string $url The url of the page which should be scraped + * @param string $mimetype Optional mimetype that had already been detected for this page * * @return array which contains needed data for embedding * string 'url' => The url of the parsed page @@ -115,26 +104,24 @@ class ParseUrl * @see ParseUrl::getSiteinfo() for more information about scraping * embeddable content */ - public static function getSiteinfoCached(string $url, bool $do_oembed = true): array + public static function getSiteinfoCached(string $url, string $mimetype = ''): array { if (empty($url)) { return [ - 'url' => '', + 'url' => '', 'type' => 'error', ]; } $urlHash = hash('sha256', $url); - $parsed_url = DBA::selectFirst('parsed_url', ['content'], - ['url_hash' => $urlHash, 'oembed' => $do_oembed] - ); + $parsed_url = DBA::selectFirst('parsed_url', ['content'], ['url_hash' => $urlHash, 'oembed' => false]); if (!empty($parsed_url['content'])) { $data = unserialize($parsed_url['content']); return $data; } - $data = self::getSiteinfo($url, $do_oembed); + $data = self::getSiteinfo($url, $mimetype); $expires = $data['expires']; @@ -144,7 +131,7 @@ class ParseUrl 'parsed_url', [ 'url_hash' => $urlHash, - 'oembed' => $do_oembed, + 'oembed' => false, 'url' => $url, 'content' => serialize($data), 'created' => DateTimeFormat::utcNow(), @@ -166,10 +153,9 @@ class ParseUrl * like \Awesome Title\ or * \ * - * @param string $url The url of the page which should be scraped - * @param bool $do_oembed The false option is used by the function fetch_oembed() - * to avoid endless loops - * @param int $count Internal counter to avoid endless loops + * @param string $url The url of the page which should be scraped + * @param string $mimetype Optional mimetype that had already been detected for this page + * @param int $count Internal counter to avoid endless loops * * @return array which contains needed data for embedding * string 'url' => The url of the parsed page @@ -194,11 +180,11 @@ class ParseUrl * * @endverbatim */ - public static function getSiteinfo(string $url, bool $do_oembed = true, int $count = 1): array + public static function getSiteinfo(string $url, string $mimetype = '', int $count = 1): array { if (empty($url)) { return [ - 'url' => '', + 'url' => '', 'type' => 'error', ]; } @@ -215,31 +201,40 @@ class ParseUrl $url = Network::stripTrackingQueryParams($url); $siteinfo = [ - 'url' => $url, - 'type' => 'link', + 'url' => $url, + 'type' => 'link', 'expires' => DateTimeFormat::utc(self::DEFAULT_EXPIRATION_FAILURE), ]; if ($count > 10) { - Logger::warning('Endless loop detected', ['url' => $url]); + DI::logger()->warning('Endless loop detected', ['url' => $url]); return $siteinfo; } - $type = self::getContentType($url); - Logger::info('Got content-type', ['content-type' => $type, 'url' => $url]); + if (!empty($mimetype)) { + $type = explode('/', current(explode(';', $mimetype))); + } else { + $type = self::getContentType($url); + } + DI::logger()->info('Got content-type', ['content-type' => $type, 'url' => $url]); if (!empty($type) && in_array($type[0], ['image', 'video', 'audio'])) { $siteinfo['type'] = $type[0]; return $siteinfo; } if ((count($type) >= 2) && (($type[0] != 'text') || ($type[1] != 'html'))) { - Logger::info('Unparseable content-type, quitting here, ', ['content-type' => $type, 'url' => $url]); + DI::logger()->info('Unparseable content-type, quitting here, ', ['content-type' => $type, 'url' => $url]); return $siteinfo; } - $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::CONTENT_LENGTH => 1000000]); - if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { - Logger::info('Empty body or error when fetching', ['url' => $url, 'success' => $curlResult->isSuccess(), 'code' => $curlResult->getReturnCode()]); + try { + $curlResult = DI::httpClient()->get($url, HttpClientAccept::HTML, [HttpClientOptions::CONTENT_LENGTH => 1000000, HttpClientOptions::REQUEST => HttpClientRequest::SITEINFO]); + } catch (\Throwable $th) { + DI::logger()->info('Exception when fetching', ['url' => $url, 'code' => $th->getCode(), 'message' => $th->getMessage()]); + return $siteinfo; + } + if (!$curlResult->isSuccess() || empty($curlResult->getBodyString())) { + DI::logger()->info('Empty body or error when fetching', ['url' => $url, 'success' => $curlResult->isSuccess(), 'code' => $curlResult->getReturnCode()]); return $siteinfo; } @@ -248,46 +243,14 @@ class ParseUrl if ($cacheControlHeader = $curlResult->getHeader('Cache-Control')[0] ?? '') { if (preg_match('/max-age=([0-9]+)/i', $cacheControlHeader, $matches)) { $maxAge = max(86400, (int)array_pop($matches)); + $siteinfo['expires'] = DateTimeFormat::utc("now + $maxAge seconds"); } } - $body = $curlResult->getBody(); + $body = $curlResult->getBodyString(); - if ($do_oembed) { - $oembed_data = OEmbed::fetchURL($url, false, false); - - if (!empty($oembed_data->type)) { - if (!in_array($oembed_data->type, ['error', 'rich', 'image', 'video', 'audio', ''])) { - $siteinfo['type'] = $oembed_data->type; - } - - // See https://github.com/friendica/friendica/pull/5763#discussion_r217913178 - if ($siteinfo['type'] != 'photo') { - if (!empty($oembed_data->title)) { - $siteinfo['title'] = trim($oembed_data->title); - } - if (!empty($oembed_data->description)) { - $siteinfo['text'] = trim($oembed_data->description); - } - if (!empty($oembed_data->author_name)) { - $siteinfo['author_name'] = trim($oembed_data->author_name); - } - if (!empty($oembed_data->author_url)) { - $siteinfo['author_url'] = trim($oembed_data->author_url); - } - if (!empty($oembed_data->provider_name)) { - $siteinfo['publisher_name'] = trim($oembed_data->provider_name); - } - if (!empty($oembed_data->provider_url)) { - $siteinfo['publisher_url'] = trim($oembed_data->provider_url); - } - if (!empty($oembed_data->thumbnail_url)) { - $siteinfo['image'] = $oembed_data->thumbnail_url; - } - } - } - } + $siteinfo['size'] = mb_strlen($body); $charset = ''; try { @@ -296,7 +259,8 @@ class ParseUrl if (isset($mediaType->parameters['charset'])) { $charset = $mediaType->parameters['charset']; } - } catch(\InvalidArgumentException $e) {} + } catch(\InvalidArgumentException $e) { + } $siteinfo['charset'] = $charset; @@ -304,7 +268,7 @@ class ParseUrl // See https://github.com/friendica/friendica/issues/5470#issuecomment-418351211 $charset = str_ireplace('latin-1', 'latin1', $charset); - Logger::info('detected charset', ['charset' => $charset]); + DI::logger()->info('detected charset', ['charset' => $charset]); $body = iconv($charset, 'UTF-8//TRANSLIT', $body); } @@ -332,6 +296,17 @@ class ParseUrl $xpath = new DOMXPath($doc); + $list = $xpath->query('//html[@lang]'); + foreach ($list as $node) { + if ($node->attributes->length) { + foreach ($node->attributes as $attribute) { + if ($attribute->name == 'lang') { + $siteinfo['language'] = $attribute->value; + } + } + } + } + $list = $xpath->query('//meta[@content]'); foreach ($list as $node) { $meta_tag = []; @@ -342,16 +317,15 @@ class ParseUrl } if (@$meta_tag['http-equiv'] == 'refresh') { - $path = $meta_tag['content']; - $pathinfo = explode(';', $path); + $path = $meta_tag['content']; $content = ''; - foreach ($pathinfo as $value) { + foreach (explode(';', $path) as $value) { if (substr(strtolower($value), 0, 4) == 'url=') { $content = substr($value, 4); } } if ($content != '') { - $siteinfo = self::getSiteinfo($content, $do_oembed, ++$count); + $siteinfo = self::getSiteinfo($content, $mimetype, ++$count); return $siteinfo; } } @@ -491,7 +465,8 @@ class ParseUrl $list = $xpath->query("//script[@type='application/ld+json']"); foreach ($list as $node) { if (!empty($node->nodeValue)) { - if ($jsonld = json_decode($node->nodeValue, true)) { + $jsonld = json_decode($node->nodeValue, true); + if (is_array($jsonld)) { $siteinfo = self::parseParts($siteinfo, $jsonld); } } @@ -524,13 +499,18 @@ class ParseUrl if (!empty($siteinfo['text']) && mb_strlen($siteinfo['text']) > self::MAX_DESC_COUNT) { $siteinfo['text'] = mb_substr($siteinfo['text'], 0, self::MAX_DESC_COUNT) . '…'; + $pos = mb_strrpos($siteinfo['text'], '.'); if ($pos > self::MIN_DESC_COUNT) { $siteinfo['text'] = mb_substr($siteinfo['text'], 0, $pos + 1); } } - Logger::info('Siteinfo fetched', ['url' => $url, 'siteinfo' => $siteinfo]); + if (!empty($siteinfo['language'])) { + $siteinfo['language'] = explode('_', str_replace('-', '_', $siteinfo['language']))[0]; + } + + DI::logger()->info('Siteinfo fetched', ['url' => $url, 'siteinfo' => $siteinfo]); Hook::callAll('getsiteinfo', $siteinfo); @@ -547,7 +527,7 @@ class ParseUrl * @param array $siteinfo * @return array */ - private static function checkMedia(string $page_url, array $siteinfo) : array + private static function checkMedia(string $page_url, array $siteinfo): array { if (!empty($siteinfo['images'])) { array_walk($siteinfo['images'], function (&$image) use ($page_url) { @@ -558,13 +538,14 @@ class ParseUrl */ if (!empty($image['url'])) { $image['url'] = self::completeUrl($image['url'], $page_url); + $photodata = Images::getInfoFromURLCached($image['url']); if (($photodata) && ($photodata[0] > 50) && ($photodata[1] > 50)) { - $image['src'] = $image['url']; - $image['width'] = $photodata[0]; - $image['height'] = $photodata[1]; + $image['src'] = $image['url']; + $image['width'] = $photodata[0]; + $image['height'] = $photodata[1]; $image['contenttype'] = $photodata['mime']; - $image['blurhash'] = $photodata['blurhash'] ?? null; + $image['blurhash'] = $photodata['blurhash'] ?? null; unset($image['url']); ksort($image); } else { @@ -581,13 +562,14 @@ class ParseUrl foreach (['audio', 'video'] as $element) { if (!empty($siteinfo[$element])) { array_walk($siteinfo[$element], function (&$media) use ($page_url, &$siteinfo) { - $url = ''; - $embed = ''; - $content = ''; + $url = ''; + $embed = ''; + $content = ''; $contenttype = ''; foreach (['embed', 'content', 'url'] as $field) { if (!empty($media[$field])) { $media[$field] = self::completeUrl($media[$field], $page_url); + $type = self::getContentType($media[$field]); if (($type[0] ?? '') == 'text') { if ($field == 'embed') { @@ -596,7 +578,7 @@ class ParseUrl $url = $media[$field]; } } elseif (!empty($type[0])) { - $content = $media[$field]; + $content = $media[$field]; $contenttype = implode('/', $type); } } @@ -743,7 +725,7 @@ class ParseUrl } elseif (!empty($jsonld['@type'])) { $siteinfo = self::parseJsonLd($siteinfo, $jsonld); } elseif (!empty($jsonld)) { - $keys = array_keys($jsonld); + $keys = array_keys($jsonld); $numeric_keys = true; foreach ($keys as $key) { if (!is_int($key)) { @@ -782,7 +764,7 @@ class ParseUrl { $type = JsonLD::fetchElement($jsonld, '@type'); if (empty($type)) { - Logger::info('Empty type', ['url' => $siteinfo['url']]); + DI::logger()->info('Empty type', ['url' => $siteinfo['url']]); return $siteinfo; } @@ -847,7 +829,7 @@ class ParseUrl case 'Person': case 'Patient': case 'PerformingGroup': - case 'DanceGroup'; + case 'DanceGroup': case 'MusicGroup': case 'TheaterGroup': return self::parseJsonLdWebPerson($siteinfo, $jsonld); @@ -859,7 +841,7 @@ class ParseUrl case 'ImageObject': return self::parseJsonLdMediaObject($siteinfo, $jsonld, 'images'); default: - Logger::info('Unknown type', ['type' => $type, 'url' => $siteinfo['url']]); + DI::logger()->info('Unknown type', ['type' => $type, 'url' => $siteinfo['url']]); return $siteinfo; } } @@ -884,7 +866,7 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'publisher', 'url'); if (!empty($content) && is_string($content)) { - $jsonldinfo['publisher_url'] = trim($content); + $jsonldinfo['publisher_url'] = Network::sanitizeUrl($content); } $brand = JsonLD::fetchElement($jsonld, 'publisher', 'brand', '@type', 'Organization'); @@ -896,7 +878,7 @@ class ParseUrl $content = JsonLD::fetchElement($brand, 'url'); if (!empty($content) && is_string($content)) { - $jsonldinfo['publisher_url'] = trim($content); + $jsonldinfo['publisher_url'] = Network::sanitizeUrl($content); } $content = JsonLD::fetchElement($brand, 'logo', 'url'); @@ -924,12 +906,12 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'author', 'sameAs'); if (!empty($content) && is_string($content)) { - $jsonldinfo['author_url'] = trim($content); + $jsonldinfo['author_url'] = Network::sanitizeUrl($content); } $content = JsonLD::fetchElement($jsonld, 'author', 'url'); if (!empty($content) && is_string($content)) { - $jsonldinfo['author_url'] = trim($content); + $jsonldinfo['author_url'] = Network::sanitizeUrl($content); } $logo = JsonLD::fetchElement($jsonld, 'author', 'logo'); @@ -943,7 +925,7 @@ class ParseUrl $jsonldinfo['author_name'] = trim($jsonld['author']); } - Logger::info('Fetched Author information', ['fetched' => $jsonldinfo]); + DI::logger()->info('Fetched Author information', ['fetched' => $jsonldinfo]); return array_merge($siteinfo, $jsonldinfo); } @@ -990,8 +972,7 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'keywords'); if (!empty($content)) { $siteinfo['keywords'] = []; - $keywords = explode(',', $content); - foreach ($keywords as $keyword) { + foreach (explode(',', $content) as $keyword) { $siteinfo['keywords'][] = trim($keyword); } } @@ -1014,7 +995,7 @@ class ParseUrl $jsonldinfo = self::parseJsonLdAuthor($jsonldinfo, $jsonld); - Logger::info('Fetched article information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); + DI::logger()->info('Fetched article information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); return array_merge($siteinfo, $jsonldinfo); } @@ -1054,7 +1035,7 @@ class ParseUrl $jsonldinfo = self::parseJsonLdAuthor($jsonldinfo, $jsonld); - Logger::info('Fetched WebPage information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); + DI::logger()->info('Fetched WebPage information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); return array_merge($siteinfo, $jsonldinfo); } @@ -1084,7 +1065,7 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'url'); if (!empty($content) && is_string($content)) { - $jsonldinfo['publisher_url'] = trim($content); + $jsonldinfo['publisher_url'] = Network::sanitizeUrl($content); } $content = JsonLD::fetchElement($jsonld, 'thumbnailUrl'); @@ -1094,7 +1075,7 @@ class ParseUrl $jsonldinfo = self::parseJsonLdAuthor($jsonldinfo, $jsonld); - Logger::info('Fetched WebSite information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); + DI::logger()->info('Fetched WebSite information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); return array_merge($siteinfo, $jsonldinfo); } @@ -1123,13 +1104,13 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'url'); if (!empty($content) && is_string($content)) { - $jsonldinfo['publisher_url'] = trim($content); + $jsonldinfo['publisher_url'] = Network::sanitizeUrl($content); } $content = JsonLD::fetchElement($jsonld, 'logo', 'url', '@type', 'ImageObject'); if (!empty($content) && is_string($content)) { $jsonldinfo['publisher_img'] = trim($content); - } elseif (!empty($content) && is_array($content)) { + } elseif (is_array($content) && array_key_exists(0, $content)) { $jsonldinfo['publisher_img'] = trim($content[0]); } @@ -1140,10 +1121,10 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'brand', 'url', '@type', 'Organization'); if (!empty($content) && is_string($content)) { - $jsonldinfo['publisher_url'] = trim($content); + $jsonldinfo['publisher_url'] = Network::sanitizeUrl($content); } - Logger::info('Fetched Organization information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); + DI::logger()->info('Fetched Organization information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); return array_merge($siteinfo, $jsonldinfo); } @@ -1172,24 +1153,24 @@ class ParseUrl $content = JsonLD::fetchElement($jsonld, 'sameAs'); if (!empty($content) && is_string($content)) { - $jsonldinfo['author_url'] = trim($content); + $jsonldinfo['author_url'] = Network::sanitizeUrl($content); } $content = JsonLD::fetchElement($jsonld, 'url'); if (!empty($content) && is_string($content)) { - $jsonldinfo['author_url'] = trim($content); + $jsonldinfo['author_url'] = Network::sanitizeUrl($content); } $content = JsonLD::fetchElement($jsonld, 'image', 'url', '@type', 'ImageObject'); if (!empty($content) && !is_string($content)) { - Logger::notice('Unexpected return value for the author image', ['content' => $content]); + DI::logger()->notice('Unexpected return value for the author image', ['content' => $content]); } if (!empty($content) && is_string($content)) { $jsonldinfo['author_img'] = trim($content); } - Logger::info('Fetched Person information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); + DI::logger()->info('Fetched Person information', ['url' => $siteinfo['url'], 'fetched' => $jsonldinfo]); return array_merge($siteinfo, $jsonldinfo); } @@ -1265,7 +1246,7 @@ class ParseUrl } } - Logger::info('Fetched Media information', ['url' => $siteinfo['url'], 'fetched' => $media]); + DI::logger()->info('Fetched Media information', ['url' => $siteinfo['url'], 'fetched' => $media]); $siteinfo[$name][] = $media; return $siteinfo; } diff --git a/src/Util/PidFile.php b/src/Util/PidFile.php index 1d9019d734..2aaf5238a7 100644 --- a/src/Util/PidFile.php +++ b/src/Util/PidFile.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/Profiler.php b/src/Util/Profiler.php index d626288993..aa2c9617d3 100644 --- a/src/Util/Profiler.php +++ b/src/Util/Profiler.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; diff --git a/src/Util/Proxy.php b/src/Util/Proxy.php index 8d79b05dd8..f6f76603e9 100644 --- a/src/Util/Proxy.php +++ b/src/Util/Proxy.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; -use Friendica\Core\Logger; -use Friendica\Core\System; +use Friendica\Content\Text\BBCode; use Friendica\DI; use GuzzleHttp\Psr7\Uri; @@ -49,81 +34,12 @@ class Proxy const PIXEL_MEDIUM = 640; const PIXEL_LARGE = 1024; - /** - * Accepted extensions - * - * @var array - * @todo Make this configurable? - */ - private static $extensions = [ - 'jpg', - 'jpeg', - 'gif', - 'png', - ]; - /** * Private constructor */ - private function __construct () { - // No instances from utilities classes - } - - /** - * Transform a remote URL into a local one. - * - * This function only performs the URL replacement on http URL and if the - * provided URL isn't local - * - * @param string $url The URL to proxify - * @param string $size One of the Proxy::SIZE_* constants - * @return string The proxified URL or relative path - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function proxifyUrl(string $url, string $size = ''): string + private function __construct() { - if (!DI::config()->get('system', 'proxify_content')) { - return $url; - } - - // Trim URL first - $url = trim($url); - - // Quit if not an HTTP/HTTPS link or if local - if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https']) || self::isLocalImage($url)) { - return $url; - } - - // Image URL may have encoded ampersands for display which aren't desirable for proxy - $url = html_entity_decode($url, ENT_NOQUOTES, 'utf-8'); - - $shortpath = hash('md5', $url); - $longpath = substr($shortpath, 0, 2); - - $longpath .= '/' . strtr(base64_encode($url), '+/', '-_'); - - // Extract the URL extension - $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); - - if (in_array($extension, self::$extensions)) { - $shortpath .= '.' . $extension; - $longpath .= '.' . $extension; - } - - $proxypath = DI::baseUrl() . '/proxy/' . $longpath; - - if ($size != '') { - $size = ':' . $size; - } - - Logger::info('Created proxy link', ['url' => $url]); - - // Too long files aren't supported by Apache - if (strlen($proxypath) > 250) { - return DI::baseUrl() . '/proxy/' . $shortpath . '?url=' . urlencode($url); - } else { - return $proxypath . $size; - } + // No instances from utilities classes } /** @@ -133,15 +49,24 @@ class Proxy * proxy storage directory. * * @param string $html Un-proxified HTML code + * @param int $uriid * * @return string Proxified HTML code * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function proxifyHtml(string $html): string + public static function proxifyHtml(string $html, int $uriid): string { $html = str_replace(Strings::normaliseLink(DI::baseUrl()) . '/', DI::baseUrl() . '/', $html); - return preg_replace_callback('/(]*src *= *["\'])([^"\']+)(["\'][^>]*>)/siU', [self::class, 'replaceUrl'], $html); + if (!preg_match_all('/(]*src *= *["\'])([^"\']+)(["\'][^>]*>)/siU', $html, $matches, PREG_SET_ORDER)) { + return $html; + } + + foreach ($matches as $match) { + $html = str_replace($match[0], self::replaceUrl($match, $uriid), $html); + } + + return $html; } /** @@ -162,7 +87,7 @@ class Proxy return true; } - return Network::isLocalLink($url); + return DI::baseUrl()->isLocalUrl($url); } /** @@ -193,7 +118,7 @@ class Proxy * @return string Proxified HTML image tag * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function replaceUrl(array $matches): string + private static function replaceUrl(array $matches, int $uriid): string { // if the picture seems to be from another picture cache then take the original source $queryvar = self::parseQuery($matches[2]); @@ -208,7 +133,7 @@ class Proxy } // Return proxified HTML - return $matches[1] . self::proxifyUrl(htmlspecialchars_decode($matches[2])) . $matches[3]; + return $matches[1] . BBCode::proxyUrl(htmlspecialchars_decode($matches[2]), BBCode::INTERNAL, $uriid, Proxy::SIZE_MEDIUM) . $matches[3]; } public static function getPixelsFromSize(string $size): int diff --git a/src/Util/ReversedFileReader.php b/src/Util/ReversedFileReader.php index 58fb2cb3fa..57800ab561 100644 --- a/src/Util/ReversedFileReader.php +++ b/src/Util/ReversedFileReader.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; @@ -40,13 +26,13 @@ class ReversedFileReader implements \Iterator /** @var int */ private $pos = -1; - /** @var array */ + /** @var array|null */ private $buffer = null; /** @var int */ private $key = -1; - /** @var string */ + /** @var string|null */ private $value = null; /** @@ -67,6 +53,7 @@ class ReversedFileReader implements \Iterator $this->buffer = null; $this->key = -1; $this->value = null; + return $this; } diff --git a/src/Util/Router/FriendicaGroupCountBased.php b/src/Util/Router/FriendicaGroupCountBased.php index 09960818de..092f696879 100644 --- a/src/Util/Router/FriendicaGroupCountBased.php +++ b/src/Util/Router/FriendicaGroupCountBased.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\Router; diff --git a/src/Util/Strings.php b/src/Util/Strings.php index 12503528fb..9dbf361761 100644 --- a/src/Util/Strings.php +++ b/src/Util/Strings.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; use Friendica\Content\ContactSelector; -use Friendica\Core\Logger; +use Friendica\DI; use ParagonIE\ConstantTime\Base64; /** @@ -169,7 +155,8 @@ class Strings { if ($network != '') { if ($url != '') { - $network_name = '' . ContactSelector::networkToName($network, $url) . ''; + $gsid = ContactSelector::getServerIdForProfile($url); + $network_name = '' . ContactSelector::networkToName($network, '', $gsid) . ''; } else { $network_name = ContactSelector::networkToName($network); } @@ -222,13 +209,13 @@ class Strings { // If this method is called for an infinite (== unlimited) amount of bytes: if ($bytes == INF) { - return INF; + return 'INF'; } $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; $bytes = max($bytes, 0); - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; @@ -461,13 +448,13 @@ class Strings if ($start < 0) { $start = max(0, $string_length + $start); - } else if ($start > $string_length) { + } elseif ($start > $string_length) { $start = $string_length; } if ($length < 0) { $length = max(0, $string_length - $start + $length); - } else if ($length > $string_length) { + } elseif ($length > $string_length) { $length = $string_length; } @@ -498,7 +485,8 @@ class Strings $blocks = []; - $return = preg_replace_callback($regex, + $return = preg_replace_callback( + $regex, function ($matches) use ($executionId, &$blocks) { $return = '«block-' . $executionId . '-' . count($blocks) . '»'; @@ -510,17 +498,18 @@ class Strings ); if (is_null($return)) { - Logger::notice('Received null value from preg_replace_callback', ['text' => $text, 'regex' => $regex, 'blocks' => $blocks, 'executionId' => $executionId]); + DI::logger()->notice('Received null value from preg_replace_callback', ['text' => $text, 'regex' => $regex, 'blocks' => $blocks, 'executionId' => $executionId]); } $text = $callback($return ?? $text) ?? ''; // Restore code blocks - $text = preg_replace_callback('/«block-' . $executionId . '-([0-9]+)»/iU', + $text = preg_replace_callback( + '/«block-' . $executionId . '-([0-9]+)»/iU', function ($matches) use ($blocks) { $return = $matches[0]; if (isset($blocks[intval($matches[1])])) { - $return = $blocks[$matches[1]]; + $return = $blocks[intval($matches[1])]; } return $return; }, @@ -541,18 +530,24 @@ class Strings { $shorthand = trim($shorthand); - if (is_numeric($shorthand)) { - return $shorthand; + if (ctype_digit($shorthand)) { + return (int) $shorthand; } - $last = strtolower($shorthand[strlen($shorthand)-1]); + if ($shorthand === '') { + return 0; + } + + $last = strtolower($shorthand[strlen($shorthand) - 1]); $shorthand = substr($shorthand, 0, -1); - switch($last) { + switch ($last) { case 'g': $shorthand *= 1024; + // no break case 'm': $shorthand *= 1024; + // no break case 'k': $shorthand *= 1024; } @@ -563,19 +558,56 @@ class Strings /** * Converts an URL in a nicer format (without the scheme and possibly shortened) * - * @param string $url URL that is about to be reformatted + * @param string $url URL that is about to be reformatted + * @param int $max_length Maximum length of an url before it is shortened * @return string reformatted link */ - public static function getStyledURL(string $url): string + public static function getStyledURL(string $url, int $max_length = 30): string { $parts = parse_url($url); - $scheme = [$parts['scheme'] . '://www.', $parts['scheme'] . '://']; + if (empty($parts['scheme'])) { + return $url; + } + + $scheme = [$parts['scheme'] . '://www.', $parts['scheme'] . '://']; $styled_url = str_replace($scheme, '', $url); - if (strlen($styled_url) > 30) { - $styled_url = substr($styled_url, 0, 30) . "…"; + if (!empty($max_length) && strlen($styled_url) > $max_length) { + $styled_url = substr($styled_url, 0, $max_length) . "…"; } return $styled_url; } + + /** + * Sort a comma separated list of hashtags, convert them to lowercase and remove duplicates + * + * @param string $tag_list + * @return string + */ + public static function cleanTags(string $tag_list): string + { + $tags = []; + + $tagitems = explode(',', str_replace([' ', ';', '#'], ',', mb_strtolower($tag_list))); + foreach ($tagitems as $tag) { + if (!empty($tag)) { + $tags[] = preg_replace('#\s#u', '', $tag); + } + } + $tags = array_unique($tags); + asort($tags); + return implode(',', $tags); + } + + /** + * Get a tag array out of a comma separated list of tags + * + * @param string $tag_list + * @return array + */ + public static function getTagArrayByString(string $tag_list): array + { + return explode(',', self::cleanTags($tag_list)); + } } diff --git a/src/Util/Temporal.php b/src/Util/Temporal.php index 525cb32c30..cc9baa53a9 100644 --- a/src/Util/Temporal.php +++ b/src/Util/Temporal.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; @@ -100,7 +86,7 @@ class Temporal $o .= ''; } } - $city = str_replace('_', ' ', DI::l10n()->t($city)); + $city = str_replace('_', ' ', DI::l10n()->t($city)); $selected = (($value == $current) ? " selected=\"selected\" " : ""); $o .= ""; } @@ -151,27 +137,29 @@ class Temporal if ($dob < '0000-01-01') { $value = ''; - $age = 0; + $age = 0; } elseif ($dob < '0001-00-00') { $value = substr($dob, 5); - $age = 0; + $age = 0; } else { $value = DateTimeFormat::utc($dob, 'Y-m-d'); - $age = self::getAgeByTimezone($value, $timezone); + $age = self::getAgeByTimezone($value, $timezone); } $tpl = Renderer::getMarkupTemplate("field_input.tpl"); - $o = Renderer::replaceMacros($tpl, + $o = Renderer::replaceMacros( + $tpl, [ - '$field' => [ - 'dob', - DI::l10n()->t('Birthday:'), - $value, - intval($age) > 0 ? DI::l10n()->t('Age: ') . DI::l10n()->tt('%d year old', '%d years old', $age) : '', - '', - 'placeholder="' . DI::l10n()->t('YYYY-MM-DD or MM-DD') . '"' + '$field' => [ + 'dob', + DI::l10n()->t('Birthday:'), + $value, + intval($age) > 0 ? DI::l10n()->t('Age: ') . DI::l10n()->tt('%d year old', '%d years old', $age) : '', + '', + 'placeholder="' . DI::l10n()->t('YYYY-MM-DD or MM-DD') . '"' + ] ] - ]); + ); return $o; } @@ -232,26 +220,28 @@ class Temporal DateTime $maxDate, DateTime $defaultDate = null, $label, - string $id = 'datetimepicker', + string $id = 'datetimepicker', bool $pickdate = true, bool $picktime = true, - string $minfrom = '', - string $maxfrom = '', - bool $required = false): string - { + string $minfrom = '', + string $maxfrom = '', + bool $required = false + ): string { // First day of the week (0 = Sunday) - $firstDay = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'calendar', 'first_day_of_week', 0); + $firstDay = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'calendar', 'first_day_of_week') ?: 0; $lang = DI::l10n()->toISO6391(DI::l10n()->getCurrentLang()); // Check if the detected language is supported by the picker - if (!in_array($lang, - ['ar', 'ro', 'id', 'bg', 'fa', 'ru', 'uk', 'en', 'el', 'de', 'nl', 'tr', 'fr', 'es', 'th', 'pl', 'pt', 'ch', 'se', 'kr', - 'it', 'da', 'no', 'ja', 'vi', 'sl', 'cs', 'hu'])) { + if (!in_array( + $lang, + ['ar', 'ro', 'id', 'bg', 'fa', 'ru', 'uk', 'en', 'el', 'de', 'nl', 'tr', 'fr', 'es', 'th', 'pl', 'pt', 'ch', 'se', 'kr', + 'it', 'da', 'no', 'ja', 'vi', 'sl', 'cs', 'hu'] + )) { $lang = 'en'; } - $o = ''; + $o = ''; $dateformat = ''; if ($pickdate) { @@ -278,21 +268,21 @@ class Temporal $input_text, DI::l10n()->t( 'Time zone: %s Change in Settings', - str_replace('_', ' ', DI::app()->getTimeZone()) . ' (GMT ' . DateTimeFormat::localNow('P') . ')', + str_replace('_', ' ', DI::appHelper()->getTimeZone()) . ' (GMT ' . DateTimeFormat::localNow('P') . ')', DI::baseUrl() . '/settings' ), $required ? '*' : '', 'placeholder="' . $readable_format . '"' ], '$datetimepicker' => [ - 'minDate' => $minDate, - 'maxDate' => $maxDate, + 'minDate' => $minDate, + 'maxDate' => $maxDate, 'defaultDate' => $defaultDate, - 'dateformat' => $dateformat, - 'firstDay' => $firstDay, - 'lang' => $lang, - 'minfrom' => $minfrom, - 'maxfrom' => $maxfrom, + 'dateformat' => $dateformat, + 'firstDay' => $firstDay, + 'lang' => $lang, + 'minfrom' => $minfrom, + 'maxfrom' => $maxfrom, ], ]); @@ -323,7 +313,7 @@ class Temporal $clock = $clock ?? new SystemClock(); $localtime = $posted_date . ' UTC'; - $abs = strtotime($localtime); + $abs = strtotime($localtime); if ($abs === false) { return DI::l10n()->t('never'); @@ -337,25 +327,25 @@ class Temporal } $isfuture = false; - $etime = $now - $abs; + $etime = $now - $abs; if ($etime >= 0 && $etime < 1) { return $compare_time ? DI::l10n()->t('less than a second ago') : DI::l10n()->t('today'); } - if ($etime < 0){ - $etime = -$etime; + if ($etime < 0) { + $etime = -$etime; $isfuture = true; } $a = [ 12 * 30 * 24 * 60 * 60 => [DI::l10n()->t('year'), DI::l10n()->t('years')], - 30 * 24 * 60 * 60 => [DI::l10n()->t('month'), DI::l10n()->t('months')], - 7 * 24 * 60 * 60 => [DI::l10n()->t('week'), DI::l10n()->t('weeks')], - 24 * 60 * 60 => [DI::l10n()->t('day'), DI::l10n()->t('days')], - 60 * 60 => [DI::l10n()->t('hour'), DI::l10n()->t('hours')], - 60 => [DI::l10n()->t('minute'), DI::l10n()->t('minutes')], - 1 => [DI::l10n()->t('second'), DI::l10n()->t('seconds')], + 30 * 24 * 60 * 60 => [DI::l10n()->t('month'), DI::l10n()->t('months')], + 7 * 24 * 60 * 60 => [DI::l10n()->t('week'), DI::l10n()->t('weeks')], + 24 * 60 * 60 => [DI::l10n()->t('day'), DI::l10n()->t('days')], + 60 * 60 => [DI::l10n()->t('hour'), DI::l10n()->t('hours')], + 60 => [DI::l10n()->t('minute'), DI::l10n()->t('minutes')], + 1 => [DI::l10n()->t('second'), DI::l10n()->t('seconds')], ]; foreach ($a as $secs => $str) { @@ -363,16 +353,17 @@ class Temporal if ($d >= 1) { $r = floor($d); // translators - e.g. 22 hours ago, 1 minute ago - if($isfuture){ + if($isfuture) { $format = DI::l10n()->t('in %1$d %2$s'); - } - else { + } else { $format = DI::l10n()->t('%1$d %2$s ago'); } return sprintf($format, $r, (($r == 1) ? $str[0] : $str[1])); } } + + return ''; } /** @@ -393,7 +384,7 @@ class Temporal return 0; } - $birthdate = new DateTime($dob . ' 00:00:00', new DateTimeZone($timezone)); + $birthdate = new DateTime($dob . ' 00:00:00', new DateTimeZone($timezone)); $currentDate = new DateTime('now', new DateTimeZone('UTC')); $interval = $birthdate->diff($currentDate); @@ -414,7 +405,7 @@ class Temporal */ public static function getDaysInMonth(int $y, int $m): int { - return date('t', mktime(0, 0, 0, $m, 1, $y)); + return (int) date('t', mktime(0, 0, 0, $m, 1, $y)); } /** @@ -463,7 +454,7 @@ class Temporal 'October', 'November', 'December' ]; - $thisyear = DateTimeFormat::localNow('Y'); + $thisyear = DateTimeFormat::localNow('Y'); $thismonth = DateTimeFormat::localNow('m'); if (!$y) { $y = $thisyear; @@ -473,11 +464,11 @@ class Temporal $m = intval($thismonth); } - $dn = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - $f = self::getFirstDayInMonth($y, $m); - $l = self::getDaysInMonth($y, $m); - $d = 1; - $dow = 0; + $dn = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + $f = self::getFirstDayInMonth($y, $m); + $l = self::getDaysInMonth($y, $m); + $d = 1; + $dow = 0; $started = false; if (($y == $thisyear) && ($m == $thismonth)) { @@ -485,9 +476,9 @@ class Temporal } $str_month = DI::l10n()->getDay($mtab[$m]); - $o = ''; + $o = '
                              '; $o .= ""; - for ($a = 0; $a < 7; $a ++) { + for ($a = 0; $a < 7; $a++) { $o .= ''; } @@ -508,13 +499,13 @@ class Temporal $o .= $day; } - $d ++; + $d++; } else { $o .= ' '; } $o .= ''; - $dow ++; + $dow++; if (($dow == 7) && ($d <= $l)) { $dow = 0; $o .= ''; @@ -522,7 +513,7 @@ class Temporal } if ($dow) { - for ($a = $dow; $a < 7; $a ++) { + for ($a = $dow; $a < 7; $a++) { $o .= ''; } } diff --git a/src/Util/Writer/DbaDefinitionSqlWriter.php b/src/Util/Writer/DbaDefinitionSqlWriter.php index 786eb86650..b5a770ab57 100644 --- a/src/Util/Writer/DbaDefinitionSqlWriter.php +++ b/src/Util/Writer/DbaDefinitionSqlWriter.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\Writer; diff --git a/src/Util/Writer/DocWriter.php b/src/Util/Writer/DocWriter.php index 4d3442a090..1968de212f 100644 --- a/src/Util/Writer/DocWriter.php +++ b/src/Util/Writer/DocWriter.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\Writer; diff --git a/src/Util/Writer/ViewDefinitionSqlWriter.php b/src/Util/Writer/ViewDefinitionSqlWriter.php index 68fc70988b..920b7f9111 100644 --- a/src/Util/Writer/ViewDefinitionSqlWriter.php +++ b/src/Util/Writer/ViewDefinitionSqlWriter.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util\Writer; diff --git a/src/Util/XML.php b/src/Util/XML.php index e1910ce47d..4c099fb78e 100644 --- a/src/Util/XML.php +++ b/src/Util/XML.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Util; @@ -25,7 +11,7 @@ use DOMDocument; use DOMElement; use DOMNode; use DOMXPath; -use Friendica\Core\Logger; +use Friendica\DI; use SimpleXMLElement; /** @@ -59,9 +45,9 @@ class XML $root = new SimpleXMLElement('<' . $key . '>' . self::escape($value ?? '') . ''); } - $dom = dom_import_simplexml($root)->ownerDocument; + $dom = dom_import_simplexml($root)->ownerDocument; $dom->formatOutput = true; - $xml = $dom; + $xml = $dom; $xml_text = $dom->saveXML(); @@ -136,9 +122,9 @@ class XML /** * Copies an XML object * - * @param object|string $source The XML source - * @param object $target The XML target - * @param string $elementname Name of the XML element of the target + * @param object $source The XML source + * @param object $target The XML target + * @param string $elementname Name of the XML element of the target * @return void */ public static function copy(&$source, &$target, $elementname) @@ -168,7 +154,7 @@ class XML $element = $doc->createElement($element, self::escape($value)); foreach ($attributes as $key => $value) { - $attribute = $doc->createAttribute($key); + $attribute = $doc->createAttribute($key); $attribute->value = self::escape($value ?? ''); $element->appendChild($attribute); } @@ -198,7 +184,7 @@ class XML * @param integer $recursion_depth recursion counter for internal use - default 0 * internal use, recursion counter * - * @return array | string The array from the xml element or the string + * @return array|string|null The array from the xml element or the string */ public static function elementToArray($xml_element, int &$recursion_depth = 0) { @@ -213,7 +199,7 @@ class XML && (get_class($xml_element) == 'SimpleXMLElement') ) { $xml_element_copy = $xml_element; - $xml_element = get_object_vars($xml_element); + $xml_element = get_object_vars($xml_element); } if (is_array($xml_element)) { @@ -224,12 +210,12 @@ class XML foreach ($xml_element as $key => $value) { $recursion_depth++; - $result_array[strtolower($key)] = self::elementToArray($value, $recursion_depth); + $result_array[strtolower($key)] = self::elementToArray($value, $recursion_depth); $recursion_depth--; } if ($recursion_depth == 0) { - $temp_array = $result_array; + $temp_array = $result_array; $result_array = [ strtolower($xml_element_copy->getName()) => $temp_array, ]; @@ -270,7 +256,7 @@ class XML } if (!function_exists('xml_parser_create')) { - Logger::error('Xml::toArray: parser function missing'); + DI::logger()->error('Xml::toArray: parser function missing'); return []; } @@ -286,7 +272,7 @@ class XML } if (!$parser) { - Logger::warning('Xml::toArray: xml_parser_create: no resource'); + DI::logger()->warning('Xml::toArray: xml_parser_create: no resource'); return []; } @@ -298,9 +284,9 @@ class XML @xml_parser_free($parser); if (! $xml_values) { - Logger::debug('Xml::toArray: libxml: parse error: ' . $contents); + DI::logger()->debug('Xml::toArray: libxml: parse error: ' . $contents); foreach (libxml_get_errors() as $err) { - Logger::debug('libxml: parse: ' . $err->code . ' at ' . $err->line . ':' . $err->column . ' : ' . $err->message); + DI::logger()->debug('libxml: parse: ' . $err->code . ' at ' . $err->line . ':' . $err->column . ' : ' . $err->message); } libxml_clear_errors(); return []; @@ -320,7 +306,7 @@ class XML $attributes = isset($data['attributes']) ? $data['attributes'] : null; $value = isset($data['value']) ? $data['value'] : null; - $result = []; + $result = []; $attributes_data = []; if (isset($value)) { @@ -344,14 +330,14 @@ class XML // See tag status and do the needed. if ($namespaces && strpos($tag, ':')) { - $namespc = substr($tag, 0, strrpos($tag, ':')); - $tag = strtolower(substr($tag, strlen($namespc)+1)); + $namespc = substr($tag, 0, strrpos($tag, ':')); + $tag = strtolower(substr($tag, strlen($namespc) + 1)); $result['@namespace'] = $namespc; } $tag = strtolower($tag); if ($type == 'open') { // The starting of the tag '' - $parent[$level-1] = &$current; + $parent[$level - 1] = &$current; if (!is_array($current) || (!in_array($tag, array_keys($current)))) { // Insert New tag $current[$tag] = $result; if ($attributes_data) { @@ -366,7 +352,7 @@ class XML $current[$tag][$repeated_tag_index[$tag . '_' . $level]] = $result; $repeated_tag_index[$tag . '_' . $level]++; } else { // This section will make the value an array if multiple tags with the same name appear together - $current[$tag] = [$current[$tag], $result]; // This will combine the existing item and the new item together to make an array + $current[$tag] = [$current[$tag], $result]; // This will combine the existing item and the new item together to make an array $repeated_tag_index[$tag . '_' . $level] = 2; if (isset($current[$tag.'_attr'])) { // The attribute of the last(0th) tag must be moved as well @@ -374,13 +360,13 @@ class XML unset($current[$tag.'_attr']); } } - $last_item_index = $repeated_tag_index[$tag . '_' . $level]-1; - $current = &$current[$tag][$last_item_index]; + $last_item_index = $repeated_tag_index[$tag . '_' . $level] - 1; + $current = &$current[$tag][$last_item_index]; } } elseif ($type == 'complete') { // Tags that ends in 1 line '' //See if the key is already taken. if (!isset($current[$tag])) { //New Key - $current[$tag] = $result; + $current[$tag] = $result; $repeated_tag_index[$tag . '_' . $level] = 1; if ($priority == 'tag' and $attributes_data) { $current[$tag. '_attr'] = $attributes_data; @@ -396,7 +382,7 @@ class XML } $repeated_tag_index[$tag . '_' . $level]++; } else { // If it is not an array... - $current[$tag] = [$current[$tag], $result]; //...Make it an array using the existing value and the new value + $current[$tag] = [$current[$tag], $result]; //...Make it an array using the existing value and the new value $repeated_tag_index[$tag . '_' . $level] = 1; if ($priority == 'tag' and $get_attributes) { if (isset($current[$tag.'_attr'])) { // The attribute of the last(0th) tag must be moved as well @@ -413,7 +399,7 @@ class XML } } } elseif ($type == 'close') { // End of tag '' - $current = &$parent[$level-1]; + $current = &$parent[$level - 1]; } } @@ -430,7 +416,7 @@ class XML public static function deleteNode(DOMDocument $doc, string $node) { $xpath = new DOMXPath($doc); - $list = $xpath->query('//' . $node); + $list = $xpath->query('//' . $node); foreach ($list as $child) { $child->parentNode->removeChild($child); } @@ -450,11 +436,11 @@ class XML $x = @simplexml_load_string($s); if (!$x) { if (!$suppress_log) { - Logger::error('Error(s) while parsing XML string.'); + DI::logger()->error('Error(s) while parsing XML string.'); foreach (libxml_get_errors() as $err) { - Logger::info('libxml error', ['code' => $err->code, 'position' => $err->line . ':' . $err->column, 'message' => $err->message]); + DI::logger()->info('libxml error', ['code' => $err->code, 'position' => $err->line . ':' . $err->column, 'message' => $err->message]); } - Logger::debug('Erroring XML string', ['xml' => $s]); + DI::logger()->debug('Erroring XML string', ['xml' => $s]); } libxml_clear_errors(); } @@ -490,7 +476,7 @@ class XML * @param DOMXPath $xpath XPath object * @param string $element Element name * @param DOMNode $context Context object or NULL - * @return ???|bool First element's attributes field or false on failure + * @return mixed|bool First element's attributes field or false on failure */ public static function getFirstAttributes(DOMXPath $xpath, string $element, DOMNode $context = null) { diff --git a/src/Worker/APDelivery.php b/src/Worker/APDelivery.php index b16ad7705f..0c986d3bab 100644 --- a/src/Worker/APDelivery.php +++ b/src/Worker/APDelivery.php @@ -1,31 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; +use Friendica\DI; use Friendica\Model\Post; use Friendica\Model\User; use Friendica\Protocol\ActivityPub; + class APDelivery { /** @@ -43,9 +30,9 @@ class APDelivery public static function execute(string $cmd, int $item_id, string $inbox, int $uid, array $receivers = [], int $uri_id = 0) { if (ActivityPub\Transmitter::archivedInbox($inbox)) { - Logger::info('Inbox is archived', ['cmd' => $cmd, 'inbox' => $inbox, 'id' => $item_id, 'uri-id' => $uri_id, 'uid' => $uid]); + DI::logger()->info('Inbox is archived', ['cmd' => $cmd, 'inbox' => $inbox, 'id' => $item_id, 'uri-id' => $uri_id, 'uid' => $uid]); if (empty($uri_id) && !empty($item_id)) { - $item = Post::selectFirst(['uri-id'], ['id' => $item_id]); + $item = Post::selectFirst(['uri-id'], ['id' => $item_id]); $uri_id = $item['uri-id'] ?? 0; } if (empty($uri_id)) { @@ -62,7 +49,7 @@ class APDelivery return; } - Logger::debug('Invoked', ['cmd' => $cmd, 'inbox' => $inbox, 'id' => $item_id, 'uri-id' => $uri_id, 'uid' => $uid]); + DI::logger()->debug('Invoked', ['cmd' => $cmd, 'inbox' => $inbox, 'id' => $item_id, 'uri-id' => $uri_id, 'uid' => $uid]); if (empty($uri_id)) { $result = ActivityPub\Delivery::deliver($inbox); diff --git a/src/Worker/AddContact.php b/src/Worker/AddContact.php index b27c54ed76..658a1ebcd1 100644 --- a/src/Worker/AddContact.php +++ b/src/Worker/AddContact.php @@ -1,30 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; +use Friendica\Core\Worker; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Util\Network; class AddContact { @@ -53,4 +41,20 @@ class AddContact DI::logger()->notice('Imagick not found.', ['exception' => $e, 'uid' => $uid, 'url' => $url]); } } + + /** + * @param array|int $run_parameters Priority constant or array of options described in Worker::add + * @param int $uid User ID + * @param string $url Contact link + * @return int + */ + public static function add($run_parameters, int $uid, string $url): int + { + if (Network::isUrlBlocked($url)) { + return 0; + } + + DI::logger()->debug('Add contact', ['uid' => $uid, 'url' => $url]); + return Worker::add($run_parameters, 'AddContact', 0, $url); + } } diff --git a/src/Worker/BulkDelivery.php b/src/Worker/BulkDelivery.php index 4ac77b2137..3d42424a82 100644 --- a/src/Worker/BulkDelivery.php +++ b/src/Worker/BulkDelivery.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\DI; use Friendica\Model\GServer; @@ -38,7 +23,7 @@ class BulkDelivery foreach ($deliveryQueueItems as $deliveryQueueItem) { if (!$server_failure && ProtocolDelivery::deliver($deliveryQueueItem->command, $deliveryQueueItem->postUriId, $deliveryQueueItem->targetContactId, $deliveryQueueItem->senderUserId)) { DI::deliveryQueueItemRepo()->remove($deliveryQueueItem); - Logger::debug('Delivery successful', $deliveryQueueItem->toArray()); + DI::logger()->debug('Delivery successful', $deliveryQueueItem->toArray()); } else { DI::deliveryQueueItemRepo()->incrementFailed($deliveryQueueItem); $delivery_failure = true; @@ -46,7 +31,7 @@ class BulkDelivery if (!$server_failure) { $server_failure = !GServer::isReachableById($gsid); } - Logger::debug('Delivery failed', ['server_failure' => $server_failure, 'post' => $deliveryQueueItem]); + DI::logger()->debug('Delivery failed', ['server_failure' => $server_failure, 'post' => $deliveryQueueItem]); } } diff --git a/src/Worker/CheckDeletedContacts.php b/src/Worker/CheckDeletedContacts.php index b6f80f4339..c7fba0a6c1 100644 --- a/src/Worker/CheckDeletedContacts.php +++ b/src/Worker/CheckDeletedContacts.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/CheckRelMeProfileLink.php b/src/Worker/CheckRelMeProfileLink.php index d55ebf3c45..3b0f1800e3 100644 --- a/src/Worker/CheckRelMeProfileLink.php +++ b/src/Worker/CheckRelMeProfileLink.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; use DOMDocument; use Friendica\Content\Text\HTML; -use Friendica\Core\Logger; use Friendica\DI; use Friendica\Model\Profile; use Friendica\Model\User; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use GuzzleHttp\Psr7\Uri; /* This class is used to verify the homepage link of a user profile. @@ -54,39 +40,44 @@ class CheckRelMeProfileLink */ public static function execute(int $uid) { - Logger::notice('Verifying the homepage', ['uid' => $uid]); + DI::logger()->notice('Verifying the homepage', ['uid' => $uid]); Profile::update(['homepage_verified' => false], $uid); $owner = User::getOwnerDataById($uid); if (empty($owner['homepage'])) { - Logger::notice('The user has no homepage link.', ['uid' => $uid]); + DI::logger()->notice('The user has no homepage link.', ['uid' => $uid]); return; } $xrd_timeout = DI::config()->get('system', 'xrd_timeout'); - $curlResult = DI::httpClient()->get($owner['homepage'], HttpClientAccept::HTML, [HttpClientOptions::TIMEOUT => $xrd_timeout]); + try { + $curlResult = DI::httpClient()->get($owner['homepage'], HttpClientAccept::HTML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::CONTACTVERIFIER]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return; + } if (!$curlResult->isSuccess()) { - Logger::notice('Could not cURL the homepage URL', ['owner homepage' => $owner['homepage']]); + DI::logger()->notice('Could not cURL the homepage URL', ['owner homepage' => $owner['homepage']]); return; } - $content = $curlResult->getBody(); + $content = $curlResult->getBodyString(); if (!$content) { - Logger::notice('Empty body of the fetched homepage link). Cannot verify the relation to profile of UID %s.', ['uid' => $uid, 'owner homepage' => $owner['homepage']]); + DI::logger()->notice('Empty body of the fetched homepage link). Cannot verify the relation to profile of UID %s.', ['uid' => $uid, 'owner homepage' => $owner['homepage']]); return; } $doc = new DOMDocument(); if (!@$doc->loadHTML($content)) { - Logger::notice('Could not parse the content'); + DI::logger()->notice('Could not parse the content'); return; } if (HTML::checkRelMeLink($doc, new Uri($owner['url']))) { Profile::update(['homepage_verified' => true], $uid); - Logger::notice('Homepage URL verified', ['uid' => $uid, 'owner homepage' => $owner['homepage']]); + DI::logger()->notice('Homepage URL verified', ['uid' => $uid, 'owner homepage' => $owner['homepage']]); } else { - Logger::notice('Homepage URL could not be verified', ['uid' => $uid, 'owner homepage' => $owner['homepage']]); + DI::logger()->notice('Homepage URL could not be verified', ['uid' => $uid, 'owner homepage' => $owner['homepage']]); } } } diff --git a/src/Worker/CheckVersion.php b/src/Worker/CheckVersion.php index f4c45fe33d..2a8f7264ae 100644 --- a/src/Worker/CheckVersion.php +++ b/src/Worker/CheckVersion.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Network\HTTPClient\Client\HttpClientAccept; @@ -36,7 +21,7 @@ class CheckVersion { public static function execute() { - Logger::notice('checkversion: start'); + DI::logger()->notice('checkversion: start'); $checkurl = DI::config()->get('system', 'check_new_version_url', 'none'); @@ -52,15 +37,15 @@ class CheckVersion // don't check return; } - Logger::info("Checking VERSION from: ".$checked_url); + DI::logger()->info("Checking VERSION from: ".$checked_url); // fetch the VERSION file $gitversion = DBA::escape(trim(DI::httpClient()->fetch($checked_url, HttpClientAccept::TEXT))); - Logger::notice("Upstream VERSION is: ".$gitversion); + DI::logger()->notice("Upstream VERSION is: ".$gitversion); DI::keyValue()->set('git_friendica_version', $gitversion); - Logger::notice('checkversion: end'); + DI::logger()->notice('checkversion: end'); return; } diff --git a/src/Worker/ClearCache.php b/src/Worker/ClearCache.php index 593395ad09..8a6640dbf1 100644 --- a/src/Worker/ClearCache.php +++ b/src/Worker/ClearCache.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/Contact/Block.php b/src/Worker/Contact/Block.php new file mode 100644 index 0000000000..9c1c2cb43c --- /dev/null +++ b/src/Worker/Contact/Block.php @@ -0,0 +1,38 @@ +. - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker\Contact; -use Friendica\Core\Logger; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Model\Contact; /** @@ -43,7 +29,7 @@ class Remove extends RemoveContent } $ret = Contact::deleteById($id); - Logger::info('Deleted contact', ['id' => $id, 'result' => $ret]); + DI::logger()->info('Deleted contact', ['id' => $id, 'result' => $ret]); return true; } diff --git a/src/Worker/Contact/RemoveContent.php b/src/Worker/Contact/RemoveContent.php index c537900e4c..762ff7a6d4 100644 --- a/src/Worker/Contact/RemoveContent.php +++ b/src/Worker/Contact/RemoveContent.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker\Contact; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; @@ -39,7 +24,7 @@ class RemoveContent return false; } - Logger::info('Start deleting contact content', ['cid' => $id]); + DI::logger()->info('Start deleting contact content', ['cid' => $id]); // Now we delete the contact and all depending tables DBA::delete('post-tag', ['cid' => $id]); diff --git a/src/Worker/Contact/RevokeFollow.php b/src/Worker/Contact/RevokeFollow.php index 714866f425..702a65b579 100644 --- a/src/Worker/Contact/RevokeFollow.php +++ b/src/Worker/Contact/RevokeFollow.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker\Contact; @@ -42,7 +28,12 @@ class RevokeFollow */ public static function execute(int $cid, int $uid) { - $contact = Contact::getById($cid); + $ucid = Contact::getUserContactId($cid, $uid); + if (!$ucid) { + return; + } + + $contact = Contact::getById($ucid); if (empty($contact)) { return; } @@ -53,7 +44,11 @@ class RevokeFollow } if (!Protocol::revokeFollow($contact, $owner)) { - Worker::defer(self::WORKER_DEFER_LIMIT); + if (!Worker::defer(self::WORKER_DEFER_LIMIT)) { + Contact::removeFollower($contact); + } + } else { + Contact::removeFollower($contact); } } } diff --git a/src/Worker/Contact/Unblock.php b/src/Worker/Contact/Unblock.php new file mode 100644 index 0000000000..a79b6ad7aa --- /dev/null +++ b/src/Worker/Contact/Unblock.php @@ -0,0 +1,38 @@ +. - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker\Contact; @@ -41,7 +27,12 @@ class Unfollow */ public static function execute(int $cid, int $uid) { - $contact = Contact::getById($cid); + $ucid = Contact::getUserContactId($cid, $uid); + if (!$ucid) { + return; + } + + $contact = Contact::getById($ucid); if (empty($contact)) { return; } @@ -53,7 +44,11 @@ class Unfollow $result = Protocol::unfollow($contact, $owner); if ($result === false) { - Worker::defer(self::WORKER_DEFER_LIMIT); + if (!Worker::defer(self::WORKER_DEFER_LIMIT)) { + Contact::removeSharer($contact); + } + } else { + Contact::removeSharer($contact); } } } diff --git a/src/Worker/ContactDiscovery.php b/src/Worker/ContactDiscovery.php index 64ed07fd01..82925e4021 100644 --- a/src/Worker/ContactDiscovery.php +++ b/src/Worker/ContactDiscovery.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/ContactDiscoveryForUser.php b/src/Worker/ContactDiscoveryForUser.php index 1df144a5c1..22eb053df6 100644 --- a/src/Worker/ContactDiscoveryForUser.php +++ b/src/Worker/ContactDiscoveryForUser.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; @@ -27,7 +13,6 @@ class ContactDiscoveryForUser { /** * Discover contact relations - * @param string $url */ public static function execute(int $uid) { diff --git a/src/Worker/Cron.php b/src/Worker/Cron.php index 66244af893..cf0529c8b4 100644 --- a/src/Worker/Cron.php +++ b/src/Worker/Cron.php @@ -1,34 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Addon; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Tag; -use Friendica\Protocol\ActivityPub\Queue; use Friendica\Protocol\Relay; use Friendica\Util\DateTimeFormat; @@ -36,7 +19,7 @@ class Cron { public static function execute() { - $a = DI::app(); + $basepath = DI::appHelper()->getBasePath(); $last = DI::keyValue()->get('last_cron'); @@ -45,16 +28,15 @@ class Cron if ($last) { $next = $last + ($poll_interval * 60); if ($next > time()) { - Logger::notice('cron interval not reached'); + DI::logger()->notice('cron interval not reached'); return; } } - Logger::notice('start'); + DI::logger()->notice('start'); // Ensure to have a .htaccess file. // this is a precaution for systems that update automatically - $basepath = $a->getBasePath(); if (!file_exists($basepath . '/.htaccess') && is_writable($basepath)) { copy($basepath . '/.htaccess-dist', $basepath . '/.htaccess'); } @@ -92,11 +74,8 @@ class Cron Tag::setLocalTrendingHashtags(24, 20); Tag::setGlobalTrendingHashtags(24, 20); - // Remove old pending posts from the queue - Queue::clear(); - // Process all unprocessed entries - Queue::processAll(); + Worker::add(Worker::PRIORITY_LOW, 'ProcessUnprocessedEntries'); // Search for new contacts in the directory if (DI::config()->get('system', 'synchronize_directory')) { @@ -123,12 +102,18 @@ class Cron Worker::add(Worker::PRIORITY_LOW, 'ExpireActivities'); + Worker::add(Worker::PRIORITY_LOW, 'ExpireSearchIndex'); + + Worker::add(Worker::PRIORITY_LOW, 'Expire'); + Worker::add(Worker::PRIORITY_LOW, 'RemoveUnusedTags'); Worker::add(Worker::PRIORITY_LOW, 'RemoveUnusedContacts'); Worker::add(Worker::PRIORITY_LOW, 'RemoveUnusedAvatars'); + Worker::add(Worker::PRIORITY_LOW, 'NodeInfo'); + // check upstream version? Worker::add(Worker::PRIORITY_LOW, 'CheckVersion'); @@ -136,6 +121,9 @@ class Cron Worker::add(Worker::PRIORITY_LOW, 'UpdateAllSuggestions'); + // add missing public contacts and account-user entries + Worker::add(Worker::PRIORITY_LOW, 'FixContacts'); + if (DI::config()->get('system', 'optimize_tables')) { Worker::add(Worker::PRIORITY_LOW, 'OptimizeTables'); } @@ -159,12 +147,12 @@ class Cron // Update "blocked" status of servers Worker::add(Worker::PRIORITY_LOW, 'UpdateBlockedServers'); - Addon::reload(); + DI::addonHelper()->reloadAddons(); DI::keyValue()->set('last_cron_daily', time()); } - Logger::notice('end'); + DI::logger()->notice('end'); DI::keyValue()->set('last_cron', time()); } @@ -176,7 +164,7 @@ class Cron */ private static function deleteSleepingProcesses() { - Logger::info('Looking for sleeping processes'); + DI::logger()->info('Looking for sleeping processes'); DBA::deleteSleepingProcesses(); } diff --git a/src/Worker/DBUpdate.php b/src/Worker/DBUpdate.php index 7b7c3b8c92..4e01e62898 100644 --- a/src/Worker/DBUpdate.php +++ b/src/Worker/DBUpdate.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; @@ -33,7 +19,7 @@ class DBUpdate { // Just in case the last update wasn't failed if (DI::config()->get('system', 'update', Update::SUCCESS) != Update::FAILED) { - Update::run(DI::app()->getBasePath()); + Update::run(DI::appHelper()->getBasePath()); } } } diff --git a/src/Worker/DelayedPublish.php b/src/Worker/DelayedPublish.php index 18ab74395e..ef2d69277c 100644 --- a/src/Worker/DelayedPublish.php +++ b/src/Worker/DelayedPublish.php @@ -1,45 +1,31 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; +use Friendica\DI; use Friendica\Model\Post; class DelayedPublish { - /** + /** * Publish a post, used for delayed postings - * - * @param array $item - * @param int $notify - * @param array $taglist - * @param array $attachments - * @param int $preparation_mode - * @param string $uri - * @return void - */ + * + * @param array $item + * @param int $notify + * @param array $taglist + * @param array $attachments + * @param int $preparation_mode + * @param string $uri + * @return void + */ public static function execute(array $item, int $notify = 0, array $taglist = [], array $attachments = [], int $preparation_mode = Post\Delayed::PREPARED, string $uri = '') { $id = Post\Delayed::publish($item, $notify, $taglist, $attachments, $preparation_mode, $uri); - Logger::notice('Post published', ['id' => $id, 'uid' => $item['uid'], 'notify' => $notify, 'unprepared' => $preparation_mode]); + DI::logger()->notice('Post published', ['id' => $id, 'uid' => $item['uid'], 'notify' => $notify, 'unprepared' => $preparation_mode]); } } diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index a441006306..b7b9a02c4d 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/Directory.php b/src/Worker/Directory.php index 2d56cb3c08..1490fc076f 100644 --- a/src/Worker/Directory.php +++ b/src/Worker/Directory.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Search; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; /** * Sends updated profile data to the directory @@ -53,15 +39,16 @@ class Directory Hook::callAll('globaldir_update', $arr); - Logger::info('Updating directory: ' . $arr['url']); + DI::logger()->info('Updating directory: ' . $arr['url']); if (strlen($arr['url'])) { - DI::httpClient()->fetch($dir . '?url=' . bin2hex($arr['url']), HttpClientAccept::HTML); + DI::httpClient()->fetch($dir . '?url=' . bin2hex($arr['url']), HttpClientAccept::HTML, 0, '', HttpClientRequest::CONTACTDISCOVER); } return; } - private static function updateAll() { + private static function updateAll() + { $users = DBA::select('owner-view', ['url'], ['net-publish' => true, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]); while ($user = DBA::fetch($users)) { Worker::add(Worker::PRIORITY_LOW, 'Directory', $user['url']); diff --git a/src/Worker/Expire.php b/src/Worker/Expire.php index 15a9355342..3387fe0973 100644 --- a/src/Worker/Expire.php +++ b/src/Worker/Expire.php @@ -1,28 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; @@ -35,46 +20,53 @@ class Expire { public static function execute($param = '', $hook_function = '') { - $a = DI::app(); + $appHelper = DI::appHelper(); Hook::loadHooks(); if (intval($param) > 0) { $user = DBA::selectFirst('user', ['uid', 'username', 'expire'], ['uid' => $param]); if (DBA::isResult($user)) { - Logger::info('Expire items', ['user' => $user['uid'], 'username' => $user['username'], 'interval' => $user['expire']]); - Item::expire($user['uid'], $user['expire']); - Logger::info('Expire items done', ['user' => $user['uid'], 'username' => $user['username'], 'interval' => $user['expire']]); + DI::logger()->info('Expire items', ['user' => $user['uid'], 'username' => $user['username'], 'interval' => $user['expire']]); + $expired = Item::expire($user['uid'], $user['expire']); + DI::logger()->info('Expire items done', ['user' => $user['uid'], 'username' => $user['username'], 'interval' => $user['expire'], 'expired' => $expired]); } return; } elseif ($param == 'hook' && !empty($hook_function)) { foreach (Hook::getByName('expire') as $hook) { if ($hook[1] == $hook_function) { - Logger::info('Calling expire hook', ['hook' => $hook[1]]); + DI::logger()->info('Calling expire hook', ['hook' => $hook[1]]); Hook::callSingle('expire', $hook, $data); } } return; } - Logger::notice('start expiry'); + DI::logger()->notice('start expiry'); $r = DBA::select('user', ['uid', 'username'], ["`expire` != ?", 0]); while ($row = DBA::fetch($r)) { - Logger::info('Calling expiry', ['user' => $row['uid'], 'username' => $row['username']]); - Worker::add(['priority' => $a->getQueueValue('priority'), 'created' => $a->getQueueValue('created'), 'dont_fork' => true], - 'Expire', (int)$row['uid']); + DI::logger()->info('Calling expiry', ['user' => $row['uid'], 'username' => $row['username']]); + Worker::add( + ['priority' => $appHelper->getQueueValue('priority'), 'created' => $appHelper->getQueueValue('created'), 'dont_fork' => true], + 'Expire', + (int)$row['uid'] + ); } DBA::close($r); - Logger::notice('calling hooks'); + DI::logger()->notice('calling hooks'); foreach (Hook::getByName('expire') as $hook) { - Logger::info('Calling expire', ['hook' => $hook[1]]); - Worker::add(['priority' => $a->getQueueValue('priority'), 'created' => $a->getQueueValue('created'), 'dont_fork' => true], - 'Expire', 'hook', $hook[1]); + DI::logger()->info('Calling expire', ['hook' => $hook[1]]); + Worker::add( + ['priority' => $appHelper->getQueueValue('priority'), 'created' => $appHelper->getQueueValue('created'), 'dont_fork' => true], + 'Expire', + 'hook', + $hook[1] + ); } - Logger::notice('calling hooks done'); + DI::logger()->notice('calling hooks done'); return; } diff --git a/src/Worker/ExpireActivities.php b/src/Worker/ExpireActivities.php index cd8e67f84f..4b6092a4cb 100644 --- a/src/Worker/ExpireActivities.php +++ b/src/Worker/ExpireActivities.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/ExpireAndRemoveUsers.php b/src/Worker/ExpireAndRemoveUsers.php index 47c3e9a60f..6fbb6a14ca 100644 --- a/src/Worker/ExpireAndRemoveUsers.php +++ b/src/Worker/ExpireAndRemoveUsers.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\Database\DBStructure; +use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Photo; use Friendica\Model\User; @@ -43,7 +29,7 @@ class ExpireAndRemoveUsers // Ensure to never remove the user with uid=0 DBA::update('user', ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false, - 'account_expires_on' => DBA::NULL_DATETIME], ['uid' => 0]); + 'account_expires_on' => DBA::NULL_DATETIME], ['uid' => 0]); // Remove any freshly expired account $users = DBA::select('user', ['uid'], ['account_expired' => true, 'account_removed' => false]); @@ -59,21 +45,21 @@ class ExpireAndRemoveUsers while ($user = DBA::fetch($users)) { $pcid = Contact::getPublicIdByUserId($user['uid']); - Logger::info('Removing user - start', ['uid' => $user['uid'], 'pcid' => $pcid]); + DI::logger()->info('Removing user - start', ['uid' => $user['uid'], 'pcid' => $pcid]); // We have to delete photo entries by hand because otherwise the photo data won't be deleted $result = Photo::delete(['uid' => $user['uid']]); if ($result) { - Logger::debug('Deleted user photos', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted user photos', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting user photos', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting user photos', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } if (!empty($pcid)) { $result = DBA::delete('post-tag', ['cid' => $pcid]); if ($result) { - Logger::debug('Deleted post-tag entries', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted post-tag entries', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting post-tag entries', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting post-tag entries', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } $tables = ['post', 'post-user', 'post-thread', 'post-thread-user']; @@ -87,9 +73,9 @@ class ExpireAndRemoveUsers foreach (['owner-id', 'author-id', 'causer-id'] as $field) { $result = DBA::delete($table, [$field => $pcid]); if ($result) { - Logger::debug('Deleted entries', ['table' => $table, 'field' => $field, 'result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted entries', ['table' => $table, 'field' => $field, 'result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting entries', ['table' => $table, 'field' => $field, 'errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting entries', ['table' => $table, 'field' => $field, 'errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } } } @@ -100,18 +86,18 @@ class ExpireAndRemoveUsers if (DBA::isResult($self)) { $result = DBA::delete('contact', ['nurl' => $self['nurl'], 'self' => false]); if ($result) { - Logger::debug('Deleted the user contact for other users', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted the user contact for other users', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting the user contact for other users', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting the user contact for other users', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } } // Delete all contacts of this user $result = DBA::delete('contact', ['uid' => $user['uid']]); if ($result) { - Logger::debug('Deleted user contacts', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted user contacts', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting user contacts', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting user contacts', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } // These tables contain the permissionset which will also be deleted when a user is deleted. @@ -121,33 +107,33 @@ class ExpireAndRemoveUsers if (DBStructure::existsTable('item')) { $result = DBA::delete('item', ['uid' => $user['uid']]); if ($result) { - Logger::debug('Deleted user items', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted user items', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting user items', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting user items', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } } $result = DBA::delete('post-user', ['uid' => $user['uid']]); if ($result) { - Logger::debug('Deleted post-user entries', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted post-user entries', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting post-user entries', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting post-user entries', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } $result = DBA::delete('profile_field', ['uid' => $user['uid']]); if ($result) { - Logger::debug('Deleted profile_field entries', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted profile_field entries', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting profile_field entries', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting profile_field entries', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } $result = DBA::delete('user', ['uid' => $user['uid']]); if ($result) { - Logger::debug('Deleted user record', ['result' => $result, 'rows' => DBA::affectedRows()]); + DI::logger()->debug('Deleted user record', ['result' => $result, 'rows' => DBA::affectedRows()]); } else { - Logger::warning('Error deleting user record', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); + DI::logger()->warning('Error deleting user record', ['errno' => DBA::errorNo(), 'errmsg' => DBA::errorMessage()]); } - Logger::info('Removing user - done', ['uid' => $user['uid']]); + DI::logger()->info('Removing user - done', ['uid' => $user['uid']]); } DBA::close($users); } diff --git a/src/Worker/ExpirePosts.php b/src/Worker/ExpirePosts.php index 2ca9e4f4e8..e4c1b231d6 100644 --- a/src/Worker/ExpirePosts.php +++ b/src/Worker/ExpirePosts.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; +use Friendica\Model\Attach; use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Util\DateTimeFormat; @@ -40,23 +26,36 @@ class ExpirePosts */ public static function execute() { + DI::logger()->notice('Expire posts - start'); + + if (!DBA::acquireOptimizeLock()) { + DI::logger()->warning('Lock could not be acquired'); + Worker::defer(); + return; + } + + DI::logger()->notice('Expire posts - Delete expired origin posts'); self::deleteExpiredOriginPosts(); + DI::logger()->notice('Expire posts - Delete orphaned entries'); self::deleteOrphanedEntries(); - self::deleteUnusedItemUri(); - + DI::logger()->notice('Expire posts - delete external posts'); self::deleteExpiredExternalPosts(); if (DI::config()->get('system', 'add_missing_posts')) { + DI::logger()->notice('Expire posts - add missing posts'); self::addMissingEntries(); } - // Set the expiry for origin posts - Worker::add(Worker::PRIORITY_LOW, 'Expire'); + DI::logger()->notice('Expire posts - delete unused attachments'); + self::deleteUnusedAttachments(); - // update nodeinfo data after everything is cleaned up - Worker::add(Worker::PRIORITY_LOW, 'NodeInfo'); + DI::logger()->notice('Expire posts - delete unused item-uri entries'); + self::deleteUnusedItemUri(); + + DBA::releaseOptimizeLock(); + DI::logger()->notice('Expire posts - done'); } /** @@ -66,17 +65,30 @@ class ExpirePosts */ private static function deleteExpiredOriginPosts() { - Logger::notice('Delete expired posts'); - // physically remove anything that has been deleted for more than two months - $condition = ["`gravity` = ? AND `deleted` AND `changed` < ?", Item::GRAVITY_PARENT, DateTimeFormat::utc('now - 60 days')]; - $rows = Post::select(['guid', 'uri-id', 'uid'], $condition); - while ($row = Post::fetch($rows)) { - Logger::info('Delete expired item', ['uri-id' => $row['uri-id'], 'guid' => $row['guid']]); - Post\User::delete(['parent-uri-id' => $row['uri-id'], 'uid' => $row['uid']]); + $limit = DI::config()->get('system', 'dbclean-expire-limit'); + if (empty($limit)) { + return; } - DBA::close($rows); - Logger::notice('Delete expired posts - done'); + DI::logger()->notice('Delete expired posts'); + // physically remove anything that has been deleted for more than two months + $condition = ["`gravity` = ? AND `deleted` AND `edited` < ?", Item::GRAVITY_PARENT, DateTimeFormat::utc('now - 60 days')]; + $pass = 0; + do { + ++$pass; + $rows = DBA::select('post-user', ['uri-id', 'uid'], $condition, ['limit' => $limit]); + $affected_count = 0; + while ($row = Post::fetch($rows)) { + DI::logger()->info('Delete expired item', ['pass' => $pass, 'uri-id' => $row['uri-id']]); + Post\User::delete(['parent-uri-id' => $row['uri-id'], 'uid' => $row['uid']]); + $affected_count += DBA::affectedRows(); + Post\Origin::delete(['parent-uri-id' => $row['uri-id'], 'uid' => $row['uid']]); + $affected_count += DBA::affectedRows(); + } + DBA::close($rows); + DBA::commit(); + DI::logger()->notice('Delete expired posts - done', ['pass' => $pass, 'rows' => $affected_count]); + } while ($affected_count); } /** @@ -86,7 +98,7 @@ class ExpirePosts */ private static function deleteOrphanedEntries() { - Logger::notice('Delete orphaned entries'); + DI::logger()->notice('Delete orphaned entries'); // "post-user" is the leading table. So we delete every entry that isn't found there $tables = ['item', 'post', 'post-content', 'post-thread', 'post-thread-user']; @@ -95,19 +107,20 @@ class ExpirePosts continue; } - Logger::notice('Start collecting orphaned entries', ['table' => $table]); - $uris = DBA::select($table, ['uri-id'], ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user`)"]); + DI::logger()->notice('Start collecting orphaned entries', ['table' => $table]); + $uris = DBA::select($table, ['uri-id'], ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user`)"]); $affected_count = 0; - Logger::notice('Deleting orphaned entries - start', ['table' => $table]); + DI::logger()->notice('Deleting orphaned entries - start', ['table' => $table]); while ($rows = DBA::toArray($uris, false, 100)) { $ids = array_column($rows, 'uri-id'); DBA::delete($table, ['uri-id' => $ids]); $affected_count += DBA::affectedRows(); } DBA::close($uris); - Logger::notice('Orphaned entries deleted', ['table' => $table, 'rows' => $affected_count]); + DBA::commit(); + DI::logger()->notice('Orphaned entries deleted', ['table' => $table, 'rows' => $affected_count]); } - Logger::notice('Delete orphaned entries - done'); + DI::logger()->notice('Delete orphaned entries - done'); } /** @@ -117,9 +130,9 @@ class ExpirePosts */ private static function addMissingEntries() { - Logger::notice('Adding missing entries'); + DI::logger()->notice('Adding missing entries'); - $rows = 0; + $rows = 0; $userposts = DBA::select('post-user', [], ["`uri-id` not in (select `uri-id` from `post`)"]); while ($fields = DBA::fetch($userposts)) { $post_fields = DI::dbaDefinition()->truncateFieldsForTable('post', $fields); @@ -128,39 +141,39 @@ class ExpirePosts } DBA::close($userposts); if ($rows > 0) { - Logger::notice('Added post entries', ['rows' => $rows]); + DI::logger()->notice('Added post entries', ['rows' => $rows]); } else { - Logger::notice('No post entries added'); + DI::logger()->notice('No post entries added'); } - $rows = 0; + $rows = 0; $userposts = DBA::select('post-user', [], ["`gravity` = ? AND `uri-id` not in (select `uri-id` from `post-thread`)", Item::GRAVITY_PARENT]); while ($fields = DBA::fetch($userposts)) { - $post_fields = DI::dbaDefinition()->truncateFieldsForTable('post-thread', $fields); + $post_fields = DI::dbaDefinition()->truncateFieldsForTable('post-thread', $fields); $post_fields['commented'] = $post_fields['changed'] = $post_fields['created']; DBA::insert('post-thread', $post_fields, Database::INSERT_IGNORE); $rows++; } DBA::close($userposts); if ($rows > 0) { - Logger::notice('Added post-thread entries', ['rows' => $rows]); + DI::logger()->notice('Added post-thread entries', ['rows' => $rows]); } else { - Logger::notice('No post-thread entries added'); + DI::logger()->notice('No post-thread entries added'); } - $rows = 0; + $rows = 0; $userposts = DBA::select('post-user', [], ["`gravity` = ? AND `id` not in (select `post-user-id` from `post-thread-user`)", Item::GRAVITY_PARENT]); while ($fields = DBA::fetch($userposts)) { - $post_fields = DI::dbaDefinition()->truncateFieldsForTable('post-thread-user', $fields); + $post_fields = DI::dbaDefinition()->truncateFieldsForTable('post-thread-user', $fields); $post_fields['commented'] = $post_fields['changed'] = $post_fields['created']; DBA::insert('post-thread-user', $post_fields, Database::INSERT_IGNORE); $rows++; } DBA::close($userposts); if ($rows > 0) { - Logger::notice('Added post-thread-user entries', ['rows' => $rows]); + DI::logger()->notice('Added post-thread-user entries', ['rows' => $rows]); } else { - Logger::notice('No post-thread-user entries added'); + DI::logger()->notice('No post-thread-user entries added'); } } @@ -169,43 +182,86 @@ class ExpirePosts */ private static function deleteUnusedItemUri() { - // We have to avoid deleting newly created "item-uri" entries. - // So we fetch a post that had been stored yesterday and only delete older ones. - $item = Post::selectFirstThread(['uri-id'], ["`uid` = ? AND `received` < ?", 0, DateTimeFormat::utc('now - 1 day')], - ['order' => ['received' => true]]); - if (empty($item['uri-id'])) { - Logger::warning('No item with uri-id found - we better quit here'); + $limit = DI::config()->get('system', 'dbclean-expire-limit'); + if (empty($limit)) { return; } - Logger::notice('Start collecting orphaned URI-ID', ['last-id' => $item['uri-id']]); - $uris = DBA::select('item-uri', ['id'], ["`id` < ? - AND NOT EXISTS(SELECT `uri-id` FROM `post-user` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `parent-uri-id` FROM `post-user` WHERE `parent-uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `thr-parent-id` FROM `post-user` WHERE `thr-parent-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `external-id` FROM `post-user` WHERE `external-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `conversation-id` FROM `post-thread` WHERE `conversation-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `mail` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `event` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `user-contact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `contact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `apcontact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `diaspora-contact` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `inbox-status` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `uri-id` FROM `post-delivery` WHERE `inbox-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `parent-uri-id` FROM `mail` WHERE `parent-uri-id` = `item-uri`.`id`) - AND NOT EXISTS(SELECT `thr-parent-id` FROM `mail` WHERE `thr-parent-id` = `item-uri`.`id`)", $item['uri-id']]); - Logger::notice('Start deleting orphaned URI-ID', ['last-id' => $item['uri-id']]); - $affected_count = 0; - while ($rows = DBA::toArray($uris, false, 100)) { - $ids = array_column($rows, 'id'); - DBA::delete('item-uri', ['id' => $ids]); - $affected_count += DBA::affectedRows(); - Logger::info('Deleted', ['rows' => $affected_count]); + // We have to avoid deleting newly created "item-uri" entries. + // So we fetch a post that had been stored yesterday and only delete older ones. + $item = Post::selectFirstThread( + ['uri-id'], + ["`uid` = ? AND `received` < ?", 0, DateTimeFormat::utc('now - 1 day')], + ['order' => ['received' => true]] + ); + if (empty($item['uri-id'])) { + DI::logger()->warning('No item with uri-id found - we better quit here'); + return; } - DBA::close($uris); - Logger::notice('Orphaned URI-ID entries removed', ['rows' => $affected_count]); + DI::logger()->notice('Start collecting orphaned URI-ID', ['last-id' => $item['uri-id']]); + + $sql = [ + 'SELECT i.id + FROM `item-uri` i + LEFT JOIN `post-user` pu1 ON i.id = pu1.`uri-id` + LEFT JOIN `post-user` pu2 ON i.id = pu2.`parent-uri-id` + LEFT JOIN `post-user` pu3 ON i.id = pu3.`thr-parent-id` + LEFT JOIN `post-user` pu4 ON i.id = pu4.`external-id` + LEFT JOIN `post-user` pu5 ON i.id = pu5.`replies-id` + LEFT JOIN `post-thread` pt1 ON i.id = pt1.`context-id` + LEFT JOIN `post-thread` pt2 ON i.id = pt2.`conversation-id` + LEFT JOIN `mail` m1 ON i.id = m1.`uri-id` + LEFT JOIN `event` e ON i.id = e.`uri-id` + LEFT JOIN `user-contact` uc ON i.id = uc.`uri-id` + LEFT JOIN `contact` c ON i.id = c.`uri-id` + LEFT JOIN `apcontact` ac ON i.id = ac.`uri-id` + LEFT JOIN `diaspora-contact` dc ON i.id = dc.`uri-id` + LEFT JOIN `inbox-status` ins ON i.id = ins.`uri-id` + LEFT JOIN `post-delivery` pd1 ON i.id = pd1.`uri-id` + LEFT JOIN `post-delivery` pd2 ON i.id = pd2.`inbox-id` + LEFT JOIN `mail` m2 ON i.id = m2.`parent-uri-id` + LEFT JOIN `mail` m3 ON i.id = m3.`thr-parent-id` + WHERE + i.id < ? AND + pu1.`uri-id` IS NULL AND + pu2.`parent-uri-id` IS NULL AND + pu3.`thr-parent-id` IS NULL AND + pu4.`external-id` IS NULL AND + pu5.`replies-id` IS NULL AND + pt1.`context-id` IS NULL AND + pt2.`conversation-id` IS NULL AND + m1.`uri-id` IS NULL AND + e.`uri-id` IS NULL AND + uc.`uri-id` IS NULL AND + c.`uri-id` IS NULL AND + ac.`uri-id` IS NULL AND + dc.`uri-id` IS NULL AND + ins.`uri-id` IS NULL AND + pd1.`uri-id` IS NULL AND + pd2.`inbox-id` IS NULL AND + m2.`parent-uri-id` IS NULL AND + m3.`thr-parent-id` IS NULL + LIMIT ?', + $item['uri-id'], + $limit + ]; + $pass = 0; + do { + ++$pass; + $uris = DBA::p(...$sql); + $total = DBA::numRows($uris); + DI::logger()->notice('Start deleting orphaned URI-ID', ['pass' => $pass, 'last-id' => $item['uri-id']]); + $affected_count = 0; + while ($rows = DBA::toArray($uris, false, 100)) { + $ids = array_column($rows, 'id'); + DBA::delete('item-uri', ['id' => $ids]); + $affected_count += DBA::affectedRows(); + DI::logger()->debug('Deleted', ['pass' => $pass, 'affected_count' => $affected_count, 'total' => $total]); + } + DBA::close($uris); + DBA::commit(); + DI::logger()->notice('Orphaned URI-ID entries removed', ['pass' => $pass, 'rows' => $affected_count]); + } while ($affected_count); } /** @@ -213,68 +269,95 @@ class ExpirePosts */ private static function deleteExpiredExternalPosts() { - $expire_days = DI::config()->get('system', 'dbclean-expire-days'); - $expire_days_unclaimed = DI::config()->get('system', 'dbclean-expire-unclaimed'); - if (empty($expire_days_unclaimed)) { - $expire_days_unclaimed = $expire_days; - } - $limit = DI::config()->get('system', 'dbclean-expire-limit'); if (empty($limit)) { return; } + $expire_days = DI::config()->get('system', 'dbclean-expire-days'); + $expire_days_unclaimed = DI::config()->get('system', 'dbclean-expire-unclaimed'); + if (empty($expire_days_unclaimed)) { + $expire_days_unclaimed = $expire_days; + } + if (!empty($expire_days)) { - Logger::notice('Start collecting expired threads', ['expiry_days' => $expire_days]); - $uris = DBA::select('item-uri', ['id'], ["`id` IN - (SELECT `uri-id` FROM `post-thread` WHERE `received` < ? - AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-thread-user` - WHERE (`mention` OR `starred` OR `wall`) AND `uri-id` = `post-thread`.`uri-id`) - AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-category` - WHERE `uri-id` = `post-thread`.`uri-id`) - AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-collection` - WHERE `uri-id` = `post-thread`.`uri-id`) - AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-media` - WHERE `uri-id` = `post-thread`.`uri-id`) - AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` INNER JOIN `contact` ON `contact`.`id` = `contact-id` AND `notify_new_posts` - WHERE `parent-uri-id` = `post-thread`.`uri-id`) - AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` - WHERE (`origin` OR `event-id` != 0 OR `post-type` = ?) AND `parent-uri-id` = `post-thread`.`uri-id`) - AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-content` - WHERE `resource-id` != 0 AND `uri-id` = `post-thread`.`uri-id`))", - DateTimeFormat::utc('now - ' . (int)$expire_days . ' days'), Item::PT_PERSONAL_NOTE]); + DI::logger()->notice('Start collecting expired threads', ['expiry_days' => $expire_days]); + $condition = [ + "`received` < ? + AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-thread-user` + WHERE (`mention` OR `starred` OR `wall`) AND `uri-id` = `post-thread`.`uri-id`) + AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-category` + WHERE `uri-id` = `post-thread`.`uri-id`) + AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-collection` + WHERE `uri-id` = `post-thread`.`uri-id`) + AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-media` + WHERE `uri-id` = `post-thread`.`uri-id`) + AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` INNER JOIN `contact` ON `contact`.`id` = `contact-id` AND `notify_new_posts` + WHERE `parent-uri-id` = `post-thread`.`uri-id`) + AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` + WHERE (`origin` OR `event-id` != 0 OR `post-type` = ?) AND `parent-uri-id` = `post-thread`.`uri-id`) + AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-content` + WHERE `resource-id` != 0 AND `uri-id` = `post-thread`.`uri-id`)", + DateTimeFormat::utc('now - ' . (int)$expire_days . ' days'), Item::PT_PERSONAL_NOTE + ]; + $pass = 0; + do { + ++$pass; + $uris = DBA::select('post-thread', ['uri-id'], $condition, ['limit' => $limit]); - Logger::notice('Start deleting expired threads'); - $affected_count = 0; - while ($rows = DBA::toArray($uris, false, 100)) { - $ids = array_column($rows, 'id'); - DBA::delete('item-uri', ['id' => $ids]); - $affected_count += DBA::affectedRows(); - } - DBA::close($uris); - - Logger::notice('Deleted expired threads', ['rows' => $affected_count]); + DI::logger()->notice('Start deleting expired threads', ['pass' => $pass]); + $affected_count = 0; + while ($rows = DBA::toArray($uris, false, 100)) { + $ids = array_column($rows, 'uri-id'); + DBA::delete('item-uri', ['id' => $ids]); + $affected_count += DBA::affectedRows(); + } + DBA::close($uris); + DBA::commit(); + DI::logger()->notice('Deleted expired threads', ['pass' => $pass, 'rows' => $affected_count]); + } while ($affected_count); } if (!empty($expire_days_unclaimed)) { - Logger::notice('Start collecting unclaimed public items', ['expiry_days' => $expire_days_unclaimed]); - $uris = DBA::select('item-uri', ['id'], ["`id` IN - (SELECT `uri-id` FROM `post-user` WHERE `gravity` = ? AND `uid` = ? AND `received` < ? - AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` AS `i` WHERE `i`.`uid` != ? - AND `i`.`parent-uri-id` = `post-user`.`uri-id`) - AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` AS `i` WHERE `i`.`uid` = ? - AND `i`.`parent-uri-id` = `post-user`.`uri-id` AND `i`.`received` > ?))", - Item::GRAVITY_PARENT, 0, DateTimeFormat::utc('now - ' . (int)$expire_days_unclaimed . ' days'), 0, 0, DateTimeFormat::utc('now - ' . (int)$expire_days_unclaimed . ' days')]); + DI::logger()->notice('Start collecting unclaimed public items', ['expiry_days' => $expire_days_unclaimed]); + $condition = [ + "`gravity` = ? AND `uid` = ? AND `received` < ? + AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` AS `i` WHERE `i`.`uid` != ? + AND `i`.`parent-uri-id` = `post-user`.`uri-id`) + AND NOT `uri-id` IN (SELECT `parent-uri-id` FROM `post-user` AS `i` WHERE `i`.`uid` = ? + AND `i`.`parent-uri-id` = `post-user`.`uri-id` AND `i`.`received` > ?)", + Item::GRAVITY_PARENT, 0, DateTimeFormat::utc('now - ' . (int)$expire_days_unclaimed . ' days'), 0, 0, DateTimeFormat::utc('now - ' . (int)$expire_days_unclaimed . ' days') + ]; + $pass = 0; + do { + ++$pass; + $uris = DBA::select('post-user', ['uri-id'], $condition, ['limit' => $limit]); + $total = DBA::numRows($uris); + DI::logger()->notice('Start deleting unclaimed public items', ['pass' => $pass]); + $affected_count = 0; + while ($rows = DBA::toArray($uris, false, 100)) { + $ids = array_column($rows, 'uri-id'); + DBA::delete('item-uri', ['id' => $ids]); + $affected_count += DBA::affectedRows(); + DI::logger()->debug('Deleted', ['pass' => $pass, 'affected_count' => $affected_count, 'total' => $total]); + } + DBA::close($uris); + DBA::commit(); + DI::logger()->notice('Deleted unclaimed public items', ['pass' => $pass, 'rows' => $affected_count]); + } while ($affected_count); + } + } - Logger::notice('Start deleting unclaimed public items'); - $affected_count = 0; - while ($rows = DBA::toArray($uris, false, 100)) { - $ids = array_column($rows, 'id'); - DBA::delete('item-uri', ['id' => $ids]); - $affected_count += DBA::affectedRows(); - } - DBA::close($uris); - Logger::notice('Deleted unclaimed public items', ['rows' => $affected_count]); + /** + * Delete media attachments (excluding photos) that aren't linked to any post + * + * @return void + */ + private static function deleteUnusedAttachments() + { + $postmedia = DBA::select('attach', ['id'], ["`id` NOT IN (SELECT `attach-id` FROM `post-media`)"]); + while ($media = DBA::fetch($postmedia)) { + Attach::delete(['id' => $media['id']]); } } } diff --git a/src/Worker/ExpireSearchIndex.php b/src/Worker/ExpireSearchIndex.php new file mode 100644 index 0000000000..5484a48392 --- /dev/null +++ b/src/Worker/ExpireSearchIndex.php @@ -0,0 +1,21 @@ +. - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; +use Friendica\DI; use Friendica\Protocol\ActivityPub; class FetchFeaturedPosts @@ -32,8 +18,8 @@ class FetchFeaturedPosts */ public static function execute(string $url) { - Logger::info('Start fetching featured posts', ['url' => $url]); + DI::logger()->info('Start fetching featured posts', ['url' => $url]); ActivityPub\Processor::fetchFeaturedPosts($url); - Logger::info('Finished fetching featured posts', ['url' => $url]); + DI::logger()->info('Finished fetching featured posts', ['url' => $url]); } } diff --git a/src/Worker/FetchMissingActivity.php b/src/Worker/FetchMissingActivity.php index d2ebc34f96..4cbc47976d 100644 --- a/src/Worker/FetchMissingActivity.php +++ b/src/Worker/FetchMissingActivity.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\DI; use Friendica\Protocol\ActivityPub; @@ -40,22 +25,26 @@ class FetchMissingActivity */ public static function execute(string $url, array $child = [], string $relay_actor = '', int $completion = Receiver::COMPLETION_MANUAL) { - Logger::info('Start fetching missing activity', ['url' => $url]); + DI::logger()->info('Start fetching missing activity', ['url' => $url]); + if (ActivityPub\Processor::alreadyKnown($url, $child['id'] ?? '')) { + DI::logger()->info('Activity is already known.', ['url' => $url]); + return; + } $result = ActivityPub\Processor::fetchMissingActivity($url, $child, $relay_actor, $completion); if ($result) { - Logger::info('Successfully fetched missing activity', ['url' => $url]); + DI::logger()->info('Successfully fetched missing activity', ['url' => $url]); } elseif (is_null($result)) { - Logger::info('Permament error, activity could not be fetched', ['url' => $url]); + DI::logger()->info('Permament error, activity could not be fetched', ['url' => $url]); } elseif (!Worker::defer(self::WORKER_DEFER_LIMIT)) { - Logger::info('Defer limit reached, activity could not be fetched', ['url' => $url]); + DI::logger()->info('Defer limit reached, activity could not be fetched', ['url' => $url]); // recursively delete all entries that belong to this worker task - $queue = DI::app()->getQueue(); + $queue = DI::appHelper()->getQueue(); if (!empty($queue['id'])) { Queue::deleteByWorkerId($queue['id']); } } else { - Logger::info('Fetching deferred', ['url' => $url]); + DI::logger()->info('Fetching deferred', ['url' => $url]); } } } diff --git a/src/Worker/FixContacts.php b/src/Worker/FixContacts.php new file mode 100644 index 0000000000..2961962bc8 --- /dev/null +++ b/src/Worker/FixContacts.php @@ -0,0 +1,51 @@ +info('Add missing public contacts'); + $contacts = DBA::p("SELECT `contact`.`id` FROM `contact` LEFT JOIN `contact` AS `pcontact` ON `contact`.`uri-id` = `pcontact`.`uri-id` WHERE `pcontact`.`id` IS NULL"); + while ($contact = DBA::fetch($contacts)) { + Contact::selectAccountUserById($contact['id'], ['id']); + $added++; + } + DBA::close($contacts); + + if ($added == 0) { + DI::logger()->info('No public contacts have been added'); + } else { + DI::logger()->info('Missing public contacts have been added', ['added' => $added]); + } + + $added = 0; + DI::logger()->info('Add missing account-user entries'); + $contacts = DBA::p("SELECT `contact`.`id`, `contact`.`uid`, `contact`.`uri-id`, `contact`.`url` FROM `contact` LEFT JOIN `account-user` ON `contact`.`id` = `account-user`.`id` WHERE `contact`.`id` > ? AND `account-user`.`id` IS NULL", 0); + while ($contact = DBA::fetch($contacts)) { + Contact::setAccountUser($contact['id'], $contact['uid'], $contact['uri-id'], $contact['url']); + $added++; + } + DBA::close($contacts); + + if ($added == 0) { + DI::logger()->info('No account-user entries have been added'); + } else { + DI::logger()->info('Missing account-user entries have been added', ['added' => $added]); + } + } +} diff --git a/src/Worker/ForkHook.php b/src/Worker/ForkHook.php index 406dcd171c..d2c06c4259 100644 --- a/src/Worker/ForkHook.php +++ b/src/Worker/ForkHook.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/MergeContact.php b/src/Worker/MergeContact.php index 0fcab79ca6..cdb464211b 100644 --- a/src/Worker/MergeContact.php +++ b/src/Worker/MergeContact.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\Database\DBStructure; +use Friendica\DI; use Friendica\Model\Contact; class MergeContact @@ -42,7 +28,7 @@ class MergeContact return; } - Logger::info('Handling duplicate', ['search' => $old_cid, 'replace' => $new_cid]); + DI::logger()->info('Handling duplicate', ['search' => $old_cid, 'replace' => $new_cid]); foreach (['item', 'thread', 'post-user', 'post-thread-user'] as $table) { if (DBStructure::existsTable($table)) { @@ -85,7 +71,7 @@ class MergeContact */ private static function mergePersonalContacts(int $first, int $duplicate) { - $fields = ['self', 'remote_self', 'rel', 'prvkey', 'subhub', 'hub-verify', 'priority', 'writable', 'archive', 'pending', + $fields = ['self', 'remote_self', 'rel', 'prvkey', 'hub-verify', 'priority', 'writable', 'archive', 'pending', 'rating', 'notify_new_posts', 'fetch_further_information', 'ffi_keyword_denylist', 'block_reason']; $c1 = Contact::getById($first, $fields); $c2 = Contact::getById($duplicate, $fields); @@ -101,7 +87,7 @@ class MergeContact $ctarget[$field] = $c1[$field] ?: $c2[$field]; } - foreach (['remote_self', 'subhub', 'writable', 'notify_new_posts'] as $field) { + foreach (['remote_self', 'writable', 'notify_new_posts'] as $field) { $ctarget[$field] = $c1[$field] || $c2[$field]; } diff --git a/src/Worker/MoveStorage.php b/src/Worker/MoveStorage.php index b66cdc017c..078f98c518 100644 --- a/src/Worker/MoveStorage.php +++ b/src/Worker/MoveStorage.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/NodeInfo.php b/src/Worker/NodeInfo.php index 81007b2372..363d2ce1f9 100644 --- a/src/Worker/NodeInfo.php +++ b/src/Worker/NodeInfo.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\DI; use Friendica\Model\Nodeinfo as ModelNodeInfo; use Friendica\Network\HTTPClient\Client\HttpClientAccept; @@ -30,13 +15,13 @@ class NodeInfo { public static function execute() { - Logger::info('start'); + DI::logger()->info('start'); ModelNodeInfo::update(); // Now trying to register $url = 'http://the-federation.info/register/' . DI::baseUrl()->getHost(); - Logger::debug('Check registering url', ['url' => $url]); + DI::logger()->debug('Check registering url', ['url' => $url]); $ret = DI::httpClient()->fetch($url, HttpClientAccept::HTML); - Logger::debug('Check registering answer', ['answer' => $ret]); - Logger::info('end'); + DI::logger()->debug('Check registering answer', ['answer' => $ret]); + DI::logger()->info('end'); } } diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 20981b7346..d0938bfcad 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -1,28 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; +use Exception; use Friendica\Core\Hook; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Worker; use Friendica\Database\DBA; @@ -33,17 +19,15 @@ use Friendica\Model\Circle; use Friendica\Model\GServer; use Friendica\Model\Item; use Friendica\Model\Post; -use Friendica\Model\PushSubscriber; use Friendica\Model\Tag; use Friendica\Model\User; +use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; +use Friendica\Protocol\ActivityPub\Transmitter; use Friendica\Protocol\Diaspora; use Friendica\Protocol\Delivery; -use Friendica\Protocol\OStatus; -use Friendica\Protocol\Salmon; use Friendica\Util\LDSignature; -use Friendica\Util\Network; use Friendica\Util\Strings; /* @@ -59,64 +43,70 @@ class Notifier { public static function execute(string $cmd, int $post_uriid, int $sender_uid = 0) { - $a = DI::app(); + $appHelper = DI::appHelper(); - Logger::info('Invoked', ['cmd' => $cmd, 'target' => $post_uriid, 'sender_uid' => $sender_uid]); + DI::logger()->info('Invoked', ['cmd' => $cmd, 'target' => $post_uriid, 'sender_uid' => $sender_uid]); - $target_id = $post_uriid; - $top_level = false; + $target_id = $post_uriid; $recipients = []; - $url_recipients = []; $delivery_contacts_stmt = null; - $target_item = []; - $parent = []; - $thr_parent = []; - $items = []; - $delivery_queue_count = 0; - $ap_contacts = []; + $target_item = []; + $parent = []; + $thr_parent = []; + $items = []; + $delivery_queue_count = 0; + $ap_contacts = []; if ($cmd == Delivery::MAIL) { $message = DBA::selectFirst('mail', ['uid', 'contact-id'], ['id' => $target_id]); if (!DBA::isResult($message)) { return; } - $uid = $message['uid']; + $uid = $message['uid']; $recipients[] = $message['contact-id']; $inboxes = ActivityPub\Transmitter::fetchTargetInboxesFromMail($target_id); foreach ($inboxes as $inbox => $receivers) { $ap_contacts = array_merge($ap_contacts, $receivers); - Logger::info('Delivery via ActivityPub', ['cmd' => $cmd, 'target' => $target_id, 'inbox' => $inbox]); - Worker::add(['priority' => Worker::PRIORITY_HIGH, 'created' => $a->getQueueValue('created'), 'dont_fork' => true], - 'APDelivery', $cmd, $target_id, $inbox, $uid, $receivers, $post_uriid); + DI::logger()->info('Delivery via ActivityPub', ['cmd' => $cmd, 'target' => $target_id, 'inbox' => $inbox]); + Worker::add( + ['priority' => Worker::PRIORITY_HIGH, 'created' => $appHelper->getQueueValue('created'), 'dont_fork' => true], + 'APDelivery', + $cmd, + $target_id, + $inbox, + $uid, + $receivers, + $post_uriid + ); } } elseif ($cmd == Delivery::SUGGESTION) { - $suggest = DI::fsuggest()->selectOneById($target_id); - $uid = $suggest->uid; + $suggest = DI::fsuggest()->selectOneById($target_id); + $uid = $suggest->uid; $recipients[] = $suggest->cid; } elseif ($cmd == Delivery::REMOVAL) { - return self::notifySelfRemoval($target_id, $a->getQueueValue('priority'), $a->getQueueValue('created')); + return self::notifySelfRemoval($target_id, $appHelper->getQueueValue('priority'), $appHelper->getQueueValue('created')); } elseif ($cmd == Delivery::RELOCATION) { $uid = $target_id; - $condition = ['uid' => $target_id, 'self' => false, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; + $condition = ['uid' => $target_id, 'self' => false, 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; $delivery_contacts_stmt = DBA::select('contact', ['id', 'uri-id', 'url', 'addr', 'network', 'protocol', 'baseurl', 'gsid', 'batch'], $condition); } else { $post = Post::selectFirst(['id'], ['uri-id' => $post_uriid, 'uid' => $sender_uid]); if (!DBA::isResult($post)) { - Logger::warning('Post not found', ['uri-id' => $post_uriid, 'uid' => $sender_uid]); + DI::logger()->warning('Post not found', ['uri-id' => $post_uriid, 'uid' => $sender_uid]); return; } $target_id = $post['id']; // find ancestors - $condition = ['id' => $target_id, 'visible' => true]; + $condition = ['id' => $target_id, 'visible' => true]; $target_item = Post::selectFirst(Item::DELIVER_FIELDLIST, $condition); $target_item = Post\Media::addHTMLAttachmentToItem($target_item); if (!DBA::isResult($target_item) || !intval($target_item['parent'])) { - Logger::info('No target item', ['cmd' => $cmd, 'target' => $target_id]); + DI::logger()->info('No target item', ['cmd' => $cmd, 'target' => $target_id]); return; } @@ -125,15 +115,15 @@ class Notifier } elseif (!empty($target_item['uid'])) { $uid = $target_item['uid']; } else { - Logger::info('Only public users, quitting', ['target' => $target_id]); + DI::logger()->info('Only public users, quitting', ['target' => $target_id]); return; } - $condition = ['parent' => $target_item['parent'], 'visible' => true]; - $params = ['order' => ['id']]; + $condition = ['parent' => $target_item['parent'], 'visible' => true]; + $params = ['order' => ['id']]; $items_stmt = Post::select(Item::DELIVER_FIELDLIST, $condition, $params); if (!DBA::isResult($items_stmt)) { - Logger::info('No item found', ['cmd' => $cmd, 'target' => $target_id]); + DI::logger()->info('No item found', ['cmd' => $cmd, 'target' => $target_id]); return; } @@ -145,13 +135,11 @@ class Notifier $item['deleted'] = 1; } } - - $top_level = $target_item['gravity'] == Item::GRAVITY_PARENT; } $owner = User::getOwnerDataById($uid); if (!$owner) { - Logger::info('Owner not found', ['cmd' => $cmd, 'target' => $target_id]); + DI::logger()->info('Owner not found', ['cmd' => $cmd, 'target' => $target_id]); return; } @@ -163,39 +151,34 @@ class Notifier $unlisted = false; - // Do a PuSH - $push_notify = false; - - // Deliver directly to a group, don't PuSH - $direct_group_delivery = false; - $only_ap_delivery = false; - $followup = false; + $followup = false; $recipients_followup = []; if (!empty($target_item) && !empty($items)) { $parent = $items[0]; - $fields = ['network', 'private', 'author-id', 'author-link', 'author-network', 'owner-id']; - $condition = ['uri' => $target_item['thr-parent'], 'uid' => $target_item['uid']]; + $fields = ['network', 'private', 'author-id', 'author-link', 'author-network', 'owner-id']; + $condition = ['uri' => $target_item['thr-parent'], 'uid' => $target_item['uid']]; $thr_parent = Post::selectFirst($fields, $condition); if (empty($thr_parent)) { $thr_parent = $parent; } - Logger::info('Got post', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'network' => $target_item['network'], 'parent-network' => $parent['network'], 'thread-parent-network' => $thr_parent['network']]); + DI::logger()->info('Got post', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'network' => $target_item['network'], 'parent-network' => $parent['network'], 'thread-parent-network' => $thr_parent['network']]); - if (!self::isRemovalActivity($cmd, $owner, Protocol::ACTIVITYPUB)) { - $apdelivery = self::activityPubDelivery($cmd, $target_item, $parent, $thr_parent, $a->getQueueValue('priority'), $a->getQueueValue('created'), $owner); - $ap_contacts = $apdelivery['contacts']; - $delivery_queue_count += $apdelivery['count']; - // Restrict distribution to AP, when there are no permissions. - if (($target_item['private'] == Item::PRIVATE) && empty($target_item['allow_cid']) && empty($target_item['allow_gid']) && empty($target_item['deny_cid']) && empty($target_item['deny_gid'])) { - $only_ap_delivery = true; - $public_message = false; - $diaspora_delivery = false; - } + // Restrict distribution to AP, when there are no permissions. + if (!self::isRemovalActivity($cmd, $owner, Protocol::ACTIVITYPUB) && ($target_item['private'] == Item::PRIVATE) && empty($target_item['allow_cid']) && empty($target_item['allow_gid']) && empty($target_item['deny_cid']) && empty($target_item['deny_gid'])) { + $only_ap_delivery = true; + $public_message = false; + $diaspora_delivery = false; + } + + if (!$target_item['origin'] && $target_item['network'] == Protocol::ACTIVITYPUB) { + $only_ap_delivery = true; + $diaspora_delivery = false; + DI::logger()->debug('Remote post arrived via AP', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'network' => $target_item['network'], 'parent-network' => $parent['network'], 'thread-parent-network' => $thr_parent['network']]); } // Only deliver threaded replies (comment to a comment) to Diaspora @@ -203,10 +186,10 @@ class Notifier if ($thr_parent['author-link'] && $target_item['parent-uri'] != $target_item['thr-parent']) { $diaspora_delivery = Diaspora::isSupportedByContactUrl($thr_parent['author-link']); if ($diaspora_delivery && empty($target_item['signed_text'])) { - Logger::debug('Post has got no Diaspora signature, so there will be no Diaspora delivery', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id']]); + DI::logger()->debug('Post has got no Diaspora signature, so there will be no Diaspora delivery', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id']]); $diaspora_delivery = false; } - Logger::info('Threaded comment', ['diaspora_delivery' => (int)$diaspora_delivery]); + DI::logger()->info('Threaded comment', ['diaspora_delivery' => (int)$diaspora_delivery]); } $unlisted = $target_item['private'] == Item::UNLISTED; @@ -219,45 +202,19 @@ class Notifier // if $parent['wall'] == 1 we will already have the parent message in our array // and we will relay the whole lot. - - $localhost = str_replace('www.','', DI::baseUrl()->getHost()); - if (strpos($localhost,':')) { - $localhost = substr($localhost,0,strpos($localhost,':')); - } - /** - * - * Be VERY CAREFUL if you make any changes to the following several lines. Seemingly innocuous changes - * have been known to cause runaway conditions which affected several servers, along with - * permissions issues. - * - */ - $relay_to_owner = false; - if (!$top_level && ($parent['wall'] == 0) && (stristr($target_item['uri'],$localhost))) { + if (($target_item['gravity'] != Item::GRAVITY_PARENT) && !$parent['wall'] && $target_item['origin']) { $relay_to_owner = true; } - // until the 'origin' flag has been in use for several months - // we will just use it as a fallback test - // later we will be able to use it as the primary test of whether or not to relay. - - if (!$target_item['origin']) { - $relay_to_owner = false; - } - if ($parent['origin']) { + if (!$target_item['origin'] || $parent['origin']) { $relay_to_owner = false; } // Special treatment for group posts if (Item::isGroupPost($target_item['uri-id'])) { $relay_to_owner = true; - $direct_group_delivery = true; - } - - // Avoid that comments in a group thread are sent to OStatus - if (Item::isGroupPost($parent['uri-id'])) { - $direct_group_delivery = true; } $exclusive_delivery = false; @@ -265,49 +222,23 @@ class Notifier $exclusive_targets = Tag::getByURIId($parent['uri-id'], [Tag::EXCLUSIVE_MENTION]); if (!empty($exclusive_targets)) { $exclusive_delivery = true; - Logger::info('Possible Exclusively delivering', ['uid' => $target_item['uid'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id']]); + DI::logger()->info('Possible Exclusively delivering', ['uid' => $target_item['uid'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id']]); foreach ($exclusive_targets as $target) { if (Strings::compareLink($owner['url'], $target['url'])) { $exclusive_delivery = false; - Logger::info('False Exclusively delivering', ['uid' => $target_item['uid'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'url' => $target['url']]); + DI::logger()->info('False Exclusively delivering', ['uid' => $target_item['uid'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'url' => $target['url']]); } } } if ($relay_to_owner) { // local followup to remote post - $followup = true; - $public_message = false; // not public - $recipients = [$parent['contact-id']]; - $recipients_followup = [$parent['contact-id']]; + $followup = true; + $public_message = false; // not public + $recipients = [$parent['contact-id']]; + $recipients_followup = [$parent['contact-id']]; - Logger::info('Followup', ['target' => $target_id, 'guid' => $target_item['guid'], 'to' => $parent['contact-id']]); - - if (($target_item['private'] != Item::PRIVATE) && - (strlen($target_item['allow_cid'].$target_item['allow_gid']. - $target_item['deny_cid'].$target_item['deny_gid']) == 0)) - $push_notify = true; - - if (($thr_parent && ($thr_parent['network'] == Protocol::OSTATUS)) || ($parent['network'] == Protocol::OSTATUS)) { - $push_notify = true; - - if ($parent["network"] == Protocol::OSTATUS) { - // Distribute the message to the DFRN contacts as if this wasn't a followup since OStatus can't relay comments - // Currently it is work at progress - $condition = ['uid' => $uid, 'network' => Protocol::DFRN, 'blocked' => false, 'pending' => false, 'archive' => false]; - $followup_contacts_stmt = DBA::select('contact', ['id'], $condition); - while($followup_contact = DBA::fetch($followup_contacts_stmt)) { - $recipients_followup[] = $followup_contact['id']; - } - DBA::close($followup_contacts_stmt); - } - } - - if ($direct_group_delivery) { - $push_notify = false; - } - - Logger::info('Notify ' . $target_item["guid"] .' via PuSH: ' . ($push_notify ? "Yes":"No")); + DI::logger()->info('Followup', ['target' => $target_id, 'guid' => $target_item['guid'], 'to' => $parent['contact-id']]); } elseif ($exclusive_delivery) { $followup = true; @@ -315,62 +246,58 @@ class Notifier $cid = Contact::getIdForURL($target['url'], $uid, false); if ($cid) { $recipients_followup[] = $cid; - Logger::info('Exclusively delivering', ['uid' => $target_item['uid'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'url' => $target['url']]); + DI::logger()->info('Exclusively delivering', ['uid' => $target_item['uid'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'url' => $target['url']]); } } } else { $followup = false; - Logger::info('Distributing directly', ['target' => $target_id, 'guid' => $target_item['guid']]); + DI::logger()->info('Distributing directly', ['target' => $target_id, 'guid' => $target_item['guid']]); // don't send deletions onward for other people's stuff if ($target_item['deleted'] && !intval($target_item['wall'])) { - Logger::notice('Ignoring delete notification for non-wall item'); + DI::logger()->notice('Ignoring delete notification for non-wall item'); return; } - if (strlen($parent['allow_cid']) + if ( + strlen($parent['allow_cid']) || strlen($parent['allow_gid']) || strlen($parent['deny_cid']) - || strlen($parent['deny_gid'])) { + || strlen($parent['deny_gid']) + ) { $public_message = false; // private recipients, not public } $aclFormatter = DI::aclFormatter(); - $allow_people = $aclFormatter->expand($parent['allow_cid']); - $allow_circles = Circle::expand($uid, $aclFormatter->expand($parent['allow_gid']),true); - $deny_people = $aclFormatter->expand($parent['deny_cid']); + $allow_people = $aclFormatter->expand($parent['allow_cid']); + $allow_circles = Circle::expand($uid, $aclFormatter->expand($parent['allow_gid']), true); + $deny_people = $aclFormatter->expand($parent['deny_cid']); $deny_circles = Circle::expand($uid, $aclFormatter->expand($parent['deny_gid'])); foreach ($items as $item) { $recipients[] = $item['contact-id']; // pull out additional tagged people to notify (if public message) if ($public_message && $item['inform']) { - $people = explode(',',$item['inform']); + $people = explode(',', $item['inform']); foreach ($people as $person) { - if (substr($person,0,4) === 'cid:') { - $recipients[] = intval(substr($person,4)); - } else { - $url_recipients[] = substr($person,4); + if (substr($person, 0, 4) === 'cid:') { + $recipients[] = intval(substr($person, 4)); } } } } - if (count($url_recipients)) { - Logger::notice('Deliver', ['target' => $target_id, 'guid' => $target_item['guid'], 'recipients' => $url_recipients]); - } - $recipients = array_unique(array_merge($recipients, $allow_people, $allow_circles)); - $deny = array_unique(array_merge($deny_people, $deny_circles)); + $deny = array_unique(array_merge($deny_people, $deny_circles)); $recipients = array_diff($recipients, $deny); // If this is a public message and pubmail is set on the parent, include all your email contacts if ( function_exists('imap_open') - && !DI::config()->get('system','imap_disabled') + && !DI::config()->get('system', 'imap_disabled') && $public_message && intval($target_item['pubmail']) ) { @@ -382,42 +309,10 @@ class Notifier } } - // If the thread parent is OStatus then do some magic to distribute the messages. - // We have not only to look at the parent, since it could be a Friendica thread. - if (($thr_parent && ($thr_parent['network'] == Protocol::OSTATUS)) || ($parent['network'] == Protocol::OSTATUS)) { - $diaspora_delivery = false; - - Logger::info('Some parent is OStatus for ' . $target_item['guid'] . ' - Author: ' . $thr_parent['author-id'] . ' - Owner: ' . $thr_parent['owner-id']); - - // Send a salmon to the parent author - $probed_contact = DBA::selectFirst('contact', ['url', 'notify'], ['id' => $thr_parent['author-id']]); - if (DBA::isResult($probed_contact) && !empty($probed_contact['notify'])) { - Logger::notice('Notify parent author', ['url' => $probed_contact['url'], 'notify' => $probed_contact['notify']]); - $url_recipients[$probed_contact['notify']] = $probed_contact['notify']; - } - - // Send a salmon to the parent owner - $probed_contact = DBA::selectFirst('contact', ['url', 'notify'], ['id' => $thr_parent['owner-id']]); - if (DBA::isResult($probed_contact) && !empty($probed_contact['notify'])) { - Logger::notice('Notify parent owner', ['url' => $probed_contact['url'], 'notify' => $probed_contact['notify']]); - $url_recipients[$probed_contact['notify']] = $probed_contact['notify']; - } - - // Send a salmon notification to every person we mentioned in the post - foreach (Tag::getByURIId($target_item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION]) as $tag) { - $probed_contact = Contact::getByURL($tag['url']); - if (!empty($probed_contact['notify'])) { - Logger::notice('Notify mentioned user', ['url' => $probed_contact['url'], 'notify' => $probed_contact['notify']]); - $url_recipients[$probed_contact['notify']] = $probed_contact['notify']; - } - } - - // It only makes sense to distribute answers to OStatus messages to Friendica and OStatus - but not Diaspora - $networks = [Protocol::DFRN]; - } elseif ($diaspora_delivery) { + if ($diaspora_delivery) { $networks = [Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; if (($parent['network'] == Protocol::DIASPORA) || ($thr_parent['network'] == Protocol::DIASPORA)) { - Logger::info('Add AP contacts', ['target' => $target_id, 'guid' => $target_item['guid']]); + DI::logger()->info('Add AP contacts', ['target' => $target_id, 'guid' => $target_item['guid']]); $networks[] = Protocol::ACTIVITYPUB; } } else { @@ -427,59 +322,72 @@ class Notifier $public_message = false; } - if (empty($delivery_contacts_stmt)) { - if ($only_ap_delivery) { - $recipients = $ap_contacts; - } elseif ($followup) { - $recipients = $recipients_followup; - } - $condition = ['id' => $recipients, 'self' => false, 'uid' => [0, $uid], - 'blocked' => false, 'pending' => false, 'archive' => false]; - if (!empty($networks)) { - $condition['network'] = $networks; - } - $delivery_contacts_stmt = DBA::select('contact', ['id', 'uri-id', 'addr', 'url', 'network', 'protocol', 'baseurl', 'gsid', 'batch'], $condition); + if ($only_ap_delivery) { + $recipients = []; + } elseif ($followup) { + $recipients = $recipients_followup; } - $conversants = []; - $batch_delivery = false; + $apdelivery = self::activityPubDelivery($cmd, $target_item, $parent, $thr_parent, $appHelper->getQueueValue('priority'), $appHelper->getQueueValue('created'), $recipients); + $ap_contacts = $apdelivery['contacts']; + $delivery_queue_count += $apdelivery['count']; - if ($public_message && !in_array($cmd, [Delivery::MAIL, Delivery::SUGGESTION]) && !$followup) { - $participants = []; - - if ($diaspora_delivery && !$unlisted) { - $batch_delivery = true; - - $participants = DBA::selectToArray('contact', ['batch', 'network', 'protocol', 'baseurl', 'gsid', 'id', 'url', 'name'], - ["`network` = ? AND `batch` != '' AND `uid` = ? AND `rel` != ? AND NOT `blocked` AND NOT `pending` AND NOT `archive`", Protocol::DIASPORA, $owner['uid'], Contact::SHARING], - ['group_by' => ['batch', 'network', 'protocol']]); - - // Fetch the participation list - // The function will ensure that there are no duplicates - $participants = Diaspora::participantsForThread($target_item, $participants); + if (!$only_ap_delivery) { + if (empty($delivery_contacts_stmt)) { + $condition = ['id' => $recipients, 'self' => false, 'uid' => [0, $uid], + 'blocked' => false, 'pending' => false, 'archive' => false]; + if (!empty($networks)) { + $condition['network'] = $networks; + } + $delivery_contacts_stmt = DBA::select('contact', ['id', 'uri-id', 'addr', 'url', 'network', 'protocol', 'baseurl', 'gsid', 'batch'], $condition); } - $condition = ['network' => Protocol::DFRN, 'uid' => $owner['uid'], 'blocked' => false, - 'pending' => false, 'archive' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]; + $conversants = []; + $batch_delivery = false; - $contacts = DBA::selectToArray('contact', ['id', 'uri-id', 'url', 'addr', 'name', 'network', 'protocol', 'baseurl', 'gsid'], $condition); + if ($public_message && !in_array($cmd, [Delivery::MAIL, Delivery::SUGGESTION]) && !$followup) { + $participants = []; - $conversants = array_merge($contacts, $participants); + if ($diaspora_delivery && !$unlisted) { + $batch_delivery = true; - $delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $thr_parent, $owner, $batch_delivery, true, $conversants, $ap_contacts, []); + $participants = DBA::selectToArray( + 'contact', + ['batch', 'network', 'protocol', 'baseurl', 'gsid', 'id', 'url', 'name'], + ["`network` = ? AND `batch` != '' AND `uid` = ? AND `rel` != ? AND NOT `blocked` AND NOT `pending` AND NOT `archive`", Protocol::DIASPORA, $owner['uid'], Contact::SHARING], + ['group_by' => ['batch', 'network', 'protocol']] + ); - $push_notify = true; + // Fetch the participation list + // The function will ensure that there are no duplicates + $participants = Diaspora::participantsForThread($target_item, $participants); + } + + $condition = [ + 'network' => Protocol::DFRN, + 'uid' => $owner['uid'], + 'self' => false, + 'blocked' => false, + 'pending' => false, + 'archive' => false, + 'rel' => [Contact::FOLLOWER, Contact::FRIEND] + ]; + + $contacts = DBA::selectToArray('contact', ['id', 'uri-id', 'url', 'addr', 'name', 'network', 'protocol', 'baseurl', 'gsid'], $condition); + + $conversants = array_merge($contacts, $participants); + + $delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $parent, $thr_parent, $owner, $batch_delivery, true, $conversants, $ap_contacts, []); + } + + $contacts = DBA::toArray($delivery_contacts_stmt); + $delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $parent, $thr_parent, $owner, $batch_delivery, false, $contacts, $ap_contacts, $conversants); } - $contacts = DBA::toArray($delivery_contacts_stmt); - $delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $thr_parent, $owner, $batch_delivery, false, $contacts, $ap_contacts, $conversants); - - $delivery_queue_count += self::deliverOStatus($target_id, $target_item, $owner, $url_recipients, $public_message, $push_notify); - if (!empty($target_item)) { - Logger::info('Calling hooks for ' . $cmd . ' ' . $target_id); + DI::logger()->info('Calling hooks for ' . $cmd . ' ' . $target_id); - Hook::fork($a->getQueueValue('priority'), 'notifier_normal', $target_item); + Hook::fork($appHelper->getQueueValue('priority'), 'notifier_normal', $target_item); Hook::callAll('notifier_end', $target_item); @@ -504,6 +412,7 @@ class Notifier * @param int $post_uriid * @param int $sender_uid * @param array $target_item + * @param array $parent * @param array $thr_parent * @param array $owner * @param bool $batch_delivery @@ -515,68 +424,84 @@ class Notifier * @throws InternalServerErrorException * @throws Exception */ - private static function delivery(string $cmd, int $post_uriid, int $sender_uid, array $target_item, array $thr_parent, array $owner, bool $batch_delivery, bool $in_batch, array $contacts, array $ap_contacts, array $conversants = []): int + private static function delivery(string $cmd, int $post_uriid, int $sender_uid, array $target_item, array $parent, array $thr_parent, array $owner, bool $batch_delivery, bool $in_batch, array $contacts, array $ap_contacts, array $conversants = []): int { - $a = DI::app(); + $appHelper = DI::appHelper(); $delivery_queue_count = 0; if (!empty($target_item['verb']) && ($target_item['verb'] == Activity::ANNOUNCE)) { - Logger::notice('Announces are only delivery via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->notice('Announces are only delivery via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); return 0; } foreach ($contacts as $contact) { + // Transmit via Diaspora if the thread had started as Diaspora post. + // Also transmit via Diaspora if this is a direct answer to a Diaspora comment. + if (($contact['network'] != Protocol::DIASPORA) && in_array(Protocol::DIASPORA, [$parent['network'] ?? '', $thr_parent['network'] ?? '', $target_item['network'] ?? ''])) { + DI::logger()->info('Enforcing the Diaspora protocol', ['id' => $contact['id'], 'network' => $contact['network'], 'parent' => $parent['network'], 'thread-parent' => $thr_parent['network'], 'post' => $target_item['network']]); + $contact['network'] = Protocol::DIASPORA; + } + // Direct delivery of local contacts if (!in_array($cmd, [Delivery::RELOCATION, Delivery::SUGGESTION, Delivery::MAIL]) && $target_uid = User::getIdForURL($contact['url'])) { if ($cmd == Delivery::DELETION) { - Logger::info('No need to deliver deletions internally', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->info('No need to deliver deletions internally', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); continue; } if ($target_item['origin'] || ($target_item['network'] != Protocol::ACTIVITYPUB)) { if ($target_uid != $target_item['uid']) { $fields = ['protocol' => Conversation::PARCEL_LOCAL_DFRN, 'direction' => Conversation::PUSH, 'post-reason' => Item::PR_DIRECT]; Item::storeForUserByUriId($target_item['uri-id'], $target_uid, $fields, $target_item['uid']); - Logger::info('Delivered locally', ['cmd' => $cmd, 'id' => $target_item['id'], 'target' => $target_uid]); + DI::logger()->info('Delivered locally', ['cmd' => $cmd, 'id' => $target_item['id'], 'target' => $target_uid]); } else { - Logger::info('No need to deliver to myself', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->info('No need to deliver to myself', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); } } else { - Logger::info('Remote item does not need to be delivered locally', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->info('Remote item does not need to be delivered locally', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); } continue; } - // Deletions are always sent via DFRN as well. - // This is done until we can perform deletions of foreign comments on our own threads via AP. - if (($cmd != Delivery::DELETION) && in_array($contact['id'], $ap_contacts)) { - Logger::info('Contact is already delivered via AP, so skip delivery via legacy DFRN/Diaspora', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); + $cdata = Contact::getPublicAndUserContactID($contact['id'], $sender_uid); + if (empty($cdata)) { + DI::logger()->info('No contact entry found', ['id' => $contact['id'], 'uid' => $sender_uid]); + continue; + } + if (in_array($cdata['public'] ?: $contact['id'], $ap_contacts)) { + DI::logger()->info('The public contact is already delivered via AP, so skip delivery via legacy DFRN/Diaspora', ['batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); + continue; + } elseif (in_array($cdata['user'] ?: $contact['id'], $ap_contacts)) { + DI::logger()->info('The user contact is already delivered via AP, so skip delivery via legacy DFRN/Diaspora', ['batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); continue; } if (!empty($contact['id']) && Contact::isArchived($contact['id'])) { - Logger::info('Contact is archived, so skip delivery', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); + // We mark the contact here, since we could have only got here, when the "archived" value on this + // specific contact hadn't been set. + Contact::markForArchival($contact); + DI::logger()->info('Contact is archived, so skip delivery', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); continue; } if (self::isRemovalActivity($cmd, $owner, $contact['network'])) { - Logger::info('Contact does no supports account removal commands, so skip delivery', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); + DI::logger()->info('Contact does no supports account removal commands, so skip delivery', ['target' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact['url']]); continue; } if (self::skipActivityPubForDiaspora($contact, $target_item, $thr_parent)) { - Logger::info('Contact is from Diaspora, but the replied author is from ActivityPub, so skip delivery via Diaspora', ['id' => $post_uriid, 'uid' => $sender_uid, 'url' => $contact['url']]); + DI::logger()->info('Contact is from Diaspora, but the replied author is from ActivityPub, so skip delivery via Diaspora', ['id' => $post_uriid, 'uid' => $sender_uid, 'url' => $contact['url']]); continue; } // Don't deliver to Diaspora if it already had been done as batch delivery if (!$in_batch && $batch_delivery && ($contact['network'] == Protocol::DIASPORA)) { - Logger::info('Diaspora contact is already delivered via batch', ['id' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact]); + DI::logger()->info('Diaspora contact is already delivered via batch', ['id' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact]); continue; } // Don't deliver to folks who have already been delivered to if (in_array($contact['id'], $conversants)) { - Logger::info('Already delivery', ['id' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact]); + DI::logger()->info('Already delivery', ['id' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact]); continue; } @@ -589,25 +514,25 @@ class Notifier } if (!$reachable) { - Logger::info('Server is not reachable', ['id' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact]); + DI::logger()->info('Server is not reachable', ['id' => $post_uriid, 'uid' => $sender_uid, 'contact' => $contact]); continue; } if (($contact['network'] == Protocol::ACTIVITYPUB) && !DI::dsprContact()->existsByUriId($contact['uri-id'])) { - Logger::info('The ActivityPub contact does not support Diaspora, so skip delivery via Diaspora', ['id' => $post_uriid, 'uid' => $sender_uid, 'url' => $contact['url']]); + DI::logger()->info('The ActivityPub contact does not support Diaspora, so skip delivery via Diaspora', ['id' => $post_uriid, 'uid' => $sender_uid, 'url' => $contact['url']]); continue; } - Logger::info('Delivery', ['batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'guid' => $target_item['guid'] ?? '', 'to' => $contact]); + DI::logger()->info('Delivery', ['cmd' => $cmd, 'batch' => $in_batch, 'target' => $post_uriid, 'uid' => $sender_uid, 'guid' => $target_item['guid'] ?? '', 'to' => $contact]); // Ensure that posts with our own protocol arrives before Diaspora posts arrive. // Situation is that sometimes Friendica servers receive Friendica posts over the Diaspora protocol first. // The conversion in Markdown reduces the formatting, so these posts should arrive after the Friendica posts. // This is only important for high and medium priority tasks and not for Low priority jobs like deletions. - if (($contact['network'] == Protocol::DIASPORA) && in_array($a->getQueueValue('priority'), [Worker::PRIORITY_HIGH, Worker::PRIORITY_MEDIUM])) { - $deliver_options = ['priority' => $a->getQueueValue('priority'), 'dont_fork' => true]; + if (($contact['network'] == Protocol::DIASPORA) && in_array($appHelper->getQueueValue('priority'), [Worker::PRIORITY_HIGH, Worker::PRIORITY_MEDIUM])) { + $deliver_options = ['priority' => $appHelper->getQueueValue('priority'), 'dont_fork' => true]; } else { - $deliver_options = ['priority' => $a->getQueueValue('priority'), 'created' => $a->getQueueValue('created'), 'dont_fork' => true]; + $deliver_options = ['priority' => $appHelper->getQueueValue('priority'), 'created' => $appHelper->getQueueValue('created'), 'dont_fork' => true]; } if (!empty($contact['gsid']) && DI::config()->get('system', 'bulk_delivery')) { @@ -626,49 +551,6 @@ class Notifier return $delivery_queue_count; } - /** - * Deliver the message via OStatus - * - * @param int $target_id - * @param array $target_item - * @param array $owner - * @param array $url_recipients - * @param bool $public_message - * @param bool $push_notify - * - * @return int Count of sent Salmon notifications - * @throws InternalServerErrorException - * @throws Exception - */ - private static function deliverOStatus(int $target_id, array $target_item, array $owner, array $url_recipients, bool $public_message, bool $push_notify): int - { - $a = DI::app(); - $delivery_queue_count = 0; - - $url_recipients = array_filter($url_recipients); - // send salmon slaps to mentioned remote tags (@foo@example.com) in OStatus posts - // They are especially used for notifications to OStatus users that don't follow us. - if (count($url_recipients) && ($public_message || $push_notify) && !empty($target_item)) { - $slap = OStatus::salmon($target_item, $owner); - foreach ($url_recipients as $url) { - Logger::info('Salmon delivery', ['item' => $target_id, 'to' => $url]); - - $delivery_queue_count++; - Salmon::slapper($owner, $url, $slap); - Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Post\DeliveryData::OSTATUS); - } - } - - // Notify PuSH subscribers (Used for OStatus distribution of regular posts) - if ($push_notify) { - Logger::info('Activating internal PuSH', ['uid' => $owner['uid']]); - - // Handling the pubsubhubbub requests - PushSubscriber::publishFeed($owner['uid'], $a->getQueueValue('priority')); - } - return $delivery_queue_count; - } - /** * Checks if the current delivery shouldn't be transported to Diaspora. * This is done for posts from AP authors or posts that are comments to AP authors. @@ -714,7 +596,7 @@ class Notifier */ private static function isRemovalActivity(string $cmd, array $owner, string $network): bool { - return ($cmd == Delivery::DELETION) && $owner['account_removed'] && in_array($network, [Protocol::ACTIVITYPUB, Protocol::DIASPORA]); + return ($cmd == Delivery::REMOVAL) && $owner['account_removed'] && in_array($network, [Protocol::ACTIVITYPUB, Protocol::DIASPORA]); } /** @@ -738,16 +620,23 @@ class Notifier return false; } - while($contact = DBA::fetch($contacts_stmt)) { + while ($contact = DBA::fetch($contacts_stmt)) { Contact::terminateFriendship($contact); } DBA::close($contacts_stmt); $inboxes = ActivityPub\Transmitter::fetchTargetInboxesforUser($self_user_id); foreach ($inboxes as $inbox => $receivers) { - Logger::info('Account removal via ActivityPub', ['uid' => $self_user_id, 'inbox' => $inbox]); - Worker::add(['priority' => Worker::PRIORITY_NEGLIGIBLE, 'created' => $created, 'dont_fork' => true], - 'APDelivery', Delivery::REMOVAL, 0, $inbox, $self_user_id, $receivers); + DI::logger()->info('Account removal via ActivityPub', ['uid' => $self_user_id, 'inbox' => $inbox]); + Worker::add( + ['priority' => Worker::PRIORITY_NEGLIGIBLE, 'created' => $created, 'dont_fork' => true], + 'APDelivery', + Delivery::REMOVAL, + 0, + $inbox, + $self_user_id, + $receivers + ); Worker::coolDown(); } @@ -759,41 +648,42 @@ class Notifier * @param array $target_item * @param array $parent * @param array $thr_parent - * @param int $priority The priority the Notifier queue item was created with - * @param string $created The date the Notifier queue item was created on + * @param int $priority The priority the Notifier queue item was created with + * @param string $created The date the Notifier queue item was created on + * @param array $recipients Array of receivers * * @return array 'count' => The number of delivery tasks created, 'contacts' => their contact ids * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException * @todo Unused parameter $owner */ - private static function activityPubDelivery($cmd, array $target_item, array $parent, array $thr_parent, int $priority, string $created, $owner): array + private static function activityPubDelivery($cmd, array $target_item, array $parent, array $thr_parent, int $priority, string $created, array $recipients): array { // Don't deliver via AP when the starting post isn't from a federated network - if (!in_array($parent['network'], Protocol::FEDERATED)) { - Logger::info('Parent network is no federated network, so no AP delivery', ['network' => $parent['network']]); + if (!in_array($parent['network'] ?? '', Protocol::FEDERATED)) { + DI::logger()->info('Parent network is no federated network, so no AP delivery', ['network' => $parent['network'] ?? '']); return ['count' => 0, 'contacts' => []]; } // Don't deliver via AP when the starting post is delivered via Diaspora if ($parent['network'] == Protocol::DIASPORA) { - Logger::info('Parent network is Diaspora, so no AP delivery'); + DI::logger()->info('Parent network is Diaspora, so no AP delivery'); return ['count' => 0, 'contacts' => []]; } // Also don't deliver when the direct thread parent was delivered via Diaspora if ($thr_parent['network'] == Protocol::DIASPORA) { - Logger::info('Thread parent network is Diaspora, so no AP delivery'); + DI::logger()->info('Thread parent network is Diaspora, so no AP delivery'); return ['count' => 0, 'contacts' => []]; } // Posts from Diaspora contacts are transmitted via Diaspora if ($target_item['network'] == Protocol::DIASPORA) { - Logger::info('Post network is Diaspora, so no AP delivery'); + DI::logger()->info('Post network is Diaspora, so no AP delivery'); return ['count' => 0, 'contacts' => []]; } - $inboxes = []; + $inboxes = []; $relay_inboxes = []; $uid = $target_item['contact-uid'] ?: $target_item['uid']; @@ -810,85 +700,97 @@ class Notifier $inboxes = ActivityPub\Transmitter::fetchTargetInboxes($target_item, $uid); if (in_array($target_item['private'], [Item::PUBLIC])) { - $inboxes = ActivityPub\Transmitter::addRelayServerInboxesForItem($target_item['id'], $inboxes); + $inboxes = ActivityPub\Transmitter::addRelayServerInboxesForItem($target_item['id'], $inboxes); $relay_inboxes = ActivityPub\Transmitter::addRelayServerInboxes(); } - Logger::info('Origin item will be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); + DI::logger()->info('Origin item will be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); $check_signature = false; - } elseif (!Post\Activity::exists($target_item['uri-id'])) { - Logger::info('Remote item is no AP post. It will not be distributed.', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); + } elseif (!$target_item['deleted'] && !Post\Activity::exists($target_item['uri-id'])) { + DI::logger()->info('Remote activity not found. It will not be distributed.', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); return ['count' => 0, 'contacts' => []]; - } elseif ($parent['origin'] && (($target_item['gravity'] != Item::GRAVITY_ACTIVITY) || DI::config()->get('system', 'redistribute_activities'))) { + } elseif ($parent['origin'] && ($target_item['private'] != Item::PRIVATE) && (($target_item['gravity'] != Item::GRAVITY_ACTIVITY) || DI::config()->get('system', 'redistribute_activities'))) { $inboxes = ActivityPub\Transmitter::fetchTargetInboxes($parent, $uid); if (in_array($target_item['private'], [Item::PUBLIC])) { $inboxes = ActivityPub\Transmitter::addRelayServerInboxesForItem($parent['id'], $inboxes); } - Logger::info('Remote item will be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); + DI::logger()->info('Remote item will be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); $check_signature = ($target_item['gravity'] == Item::GRAVITY_ACTIVITY); } else { - Logger::info('Remote activity will not be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); + DI::logger()->info('Remote activity will not be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); return ['count' => 0, 'contacts' => []]; } + if ($target_item['private'] != Item::PRIVATE) { + $inboxes = Transmitter::addInboxesForRecipients($recipients, $inboxes); + } + if (empty($inboxes) && empty($relay_inboxes)) { - Logger::info('No inboxes found for item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . '. It will not be distributed.'); + DI::logger()->info('No inboxes found for item ' . $target_item['id'] . ' with URL ' . $target_item['uri'] . '. It will not be distributed.'); return ['count' => 0, 'contacts' => []]; } // Fill the item cache $activity = ActivityPub\Transmitter::createCachedActivityFromItem($target_item['id'], true); if (empty($activity)) { - Logger::info('Item cache was not created. The post will not be distributed.', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); + DI::logger()->info('Item cache was not created. The post will not be distributed.', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); return ['count' => 0, 'contacts' => []]; } if ($check_signature && !LDSignature::isSigned($activity)) { - Logger::info('Unsigned remote activity will not be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); + DI::logger()->info('Unsigned remote activity will not be distributed', ['id' => $target_item['id'], 'url' => $target_item['uri'], 'verb' => $target_item['verb']]); return ['count' => 0, 'contacts' => []]; } $delivery_queue_count = 0; - $contacts = []; + $contacts = []; foreach ($inboxes as $inbox => $receivers) { $contacts = array_merge($contacts, $receivers); - if ((count($receivers) == 1) && Network::isLocalLink($inbox)) { + if ((count($receivers) == 1) && DI::baseUrl()->isLocalUrl($inbox)) { $contact = Contact::getById($receivers[0], ['url']); if (!in_array($cmd, [Delivery::RELOCATION, Delivery::SUGGESTION, Delivery::MAIL]) && ($target_uid = User::getIdForURL($contact['url']))) { if ($cmd == Delivery::DELETION) { - Logger::info('No need to deliver deletions internally', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->info('No need to deliver deletions internally', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); continue; } if ($target_item['origin'] || ($target_item['network'] != Protocol::ACTIVITYPUB)) { if ($target_uid != $target_item['uid']) { $fields = ['protocol' => Conversation::PARCEL_LOCAL_DFRN, 'direction' => Conversation::PUSH, 'post-reason' => Item::PR_BCC]; Item::storeForUserByUriId($target_item['uri-id'], $target_uid, $fields, $target_item['uid']); - Logger::info('Delivered locally', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]); + DI::logger()->info('Delivered locally', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]); } else { - Logger::info('No need to deliver to myself', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->info('No need to deliver to myself', ['uid' => $target_uid, 'guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); } } else { - Logger::info('Remote item does not need to be delivered locally', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + DI::logger()->info('Remote item does not need to be delivered locally', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); } continue; } - } elseif ((count($receivers) >= 1) && Network::isLocalLink($inbox)) { - Logger::info('Is this a thing?', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); + } elseif ((count($receivers) >= 1) && DI::baseUrl()->isLocalUrl($inbox)) { + DI::logger()->info('Is this a thing?', ['guid' => $target_item['guid'], 'uri-id' => $target_item['uri-id'], 'uri' => $target_item['uri']]); } - Logger::info('Delivery via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]); + DI::logger()->info('Delivery via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]); if (DI::config()->get('system', 'bulk_delivery')) { $delivery_queue_count++; Post\Delivery::add($target_item['uri-id'], $uid, $inbox, $target_item['created'], $cmd, $receivers); Worker::add([Worker::PRIORITY_HIGH, 'dont_fork' => true], 'APDelivery', '', 0, $inbox, 0); } else { - if (Worker::add(['priority' => $priority, 'created' => $created, 'dont_fork' => true], - 'APDelivery', $cmd, $target_item['id'], $inbox, $uid, $receivers, $target_item['uri-id'])) { + if (Worker::add( + ['priority' => $priority, 'created' => $created, 'dont_fork' => true], + 'APDelivery', + $cmd, + $target_item['id'], + $inbox, + $uid, + $receivers, + $target_item['uri-id'] + )) { $delivery_queue_count++; } } @@ -897,7 +799,7 @@ class Notifier // We deliver posts to relay servers slightly delayed to prioritize the direct delivery foreach ($relay_inboxes as $inbox) { - Logger::info('Delivery to relay servers via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]); + DI::logger()->info('Delivery to relay servers via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]); if (DI::config()->get('system', 'bulk_delivery')) { $delivery_queue_count++; diff --git a/src/Worker/OnePoll.php b/src/Worker/OnePoll.php index 15a9c856da..6137539612 100644 --- a/src/Worker/OnePoll.php +++ b/src/Worker/OnePoll.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Database\DBA; @@ -33,8 +18,8 @@ use Friendica\Model\Post; use Friendica\Model\User; use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Protocol\Activity; -use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Email; use Friendica\Protocol\Feed; use Friendica\Util\DateTimeFormat; @@ -45,18 +30,18 @@ class OnePoll { public static function execute(int $contact_id = 0, string $command = '') { - Logger::notice('Start polling/probing contact', ['id' => $contact_id, 'command' => $command]); + DI::logger()->notice('Start polling/probing contact', ['id' => $contact_id, 'command' => $command]); $force = ($command == 'force'); if (empty($contact_id)) { - Logger::notice('no contact provided'); + DI::logger()->notice('no contact provided'); return; } $contact = DBA::selectFirst('contact', [], ['id' => $contact_id]); if (!DBA::isResult($contact)) { - Logger::warning('Contact not found', ['id' => $contact_id]); + DI::logger()->warning('Contact not found', ['id' => $contact_id]); return; } @@ -72,25 +57,20 @@ class OnePoll $updated = DateTimeFormat::utcNow(); - // Possibly switch the remote contact to AP - if ($success && ($contact['network'] === Protocol::OSTATUS)) { - ActivityPub\Receiver::switchContact($contact['id'], $importer_uid, $contact['url']); - } - $contact = DBA::selectFirst('contact', [], ['id' => $contact_id]); if ($success && ($importer_uid != 0) && in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]) - && in_array($contact['network'], [Protocol::FEED, Protocol::MAIL, Protocol::OSTATUS])) { + && in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { $importer = User::getOwnerDataById($importer_uid); if (empty($importer)) { - Logger::warning('No self contact for user', ['uid' => $importer_uid]); + DI::logger()->warning('No self contact for user', ['uid' => $importer_uid]); // set the last-update so we don't keep polling Contact::update(['last-update' => $updated], ['id' => $contact['id']]); return; } - Logger::info('Start polling/subscribing', ['protocol' => $contact['network'], 'id' => $contact['id']]); + DI::logger()->info('Start polling/subscribing', ['protocol' => $contact['network'], 'id' => $contact['id']]); if ($contact['network'] === Protocol::FEED) { $success = self::pollFeed($contact, $importer); } elseif ($contact['network'] === Protocol::MAIL) { @@ -99,7 +79,7 @@ class OnePoll $success = self::subscribeToHub($contact['url'], $importer, $contact, $contact['blocked'] ? 'unsubscribe' : 'subscribe'); } if (!$success) { - Logger::notice('Probing had been successful, polling/subscribing failed', ['protocol' => $contact['network'], 'id' => $contact['id'], 'url' => $contact['url']]); + DI::logger()->notice('Probing had been successful, polling/subscribing failed', ['protocol' => $contact['network'], 'id' => $contact['id'], 'url' => $contact['url']]); } } @@ -111,7 +91,7 @@ class OnePoll Contact::markForArchival($contact); } - Logger::notice('End'); + DI::logger()->notice('End'); return; } @@ -126,7 +106,7 @@ class OnePoll */ private static function updateContact(array $contact, array $fields) { - if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL, Protocol::OSTATUS])) { + if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { // Update the user's contact Contact::update($fields, ['id' => $contact['id']]); @@ -154,36 +134,64 @@ class OnePoll { // Are we allowed to import from this person? if ($contact['rel'] == Contact::FOLLOWER || $contact['blocked']) { - Logger::notice('Contact is blocked or only a follower'); + DI::logger()->notice('Contact is blocked or only a follower'); return false; } if (!Network::isValidHttpUrl($contact['poll'])) { - Logger::warning('Poll address is not valid', ['id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url'], 'poll' => $contact['poll']]); + DI::logger()->warning('Poll address is not valid', ['id' => $contact['id'], 'uid' => $contact['uid'], 'url' => $contact['url'], 'poll' => $contact['poll']]); return false; } $cookiejar = tempnam(System::getTempPath(), 'cookiejar-onepoll-'); - $curlResult = DI::httpClient()->get($contact['poll'], HttpClientAccept::FEED_XML, [HttpClientOptions::COOKIEJAR => $cookiejar]); + Item::incrementInbound(Protocol::FEED); + try { + $curlResult = DI::httpClient()->get($contact['poll'], HttpClientAccept::FEED_XML, [HttpClientOptions::COOKIEJAR => $cookiejar, HttpClientOptions::REQUEST => HttpClientRequest::FEEDFETCHER]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return false; + } unlink($cookiejar); + DI::logger()->debug('Polled feed', ['url' => $contact['poll'], 'http-code' => $curlResult->getReturnCode(), 'redirect-url' => $curlResult->getRedirectUrl()]); if ($curlResult->isTimeout()) { - Logger::notice('Polling timed out', ['id' => $contact['id'], 'url' => $contact['poll']]); + DI::logger()->notice('Polling timed out', ['id' => $contact['id'], 'url' => $contact['poll']]); return false; } - $xml = $curlResult->getBody(); + if ($curlResult->isGone()) { + DI::logger()->notice('URL is permanently gone', ['id' => $contact['id'], 'url' => $contact['poll']]); + Contact::remove($contact['id']); + return false; + } + + if ($curlResult->redirectIsPermanent()) { + DI::logger()->notice('Poll address permanently changed', [ + 'id' => $contact['id'], + 'uid' => $contact['uid'], + 'old' => $contact['poll'], + 'new' => $curlResult->getRedirectUrl(), + ]); + $success = Contact::updatePollUrl($contact['id'], $curlResult->getRedirectUrl()); + } + + $xml = $curlResult->getBodyString(); if (empty($xml)) { - Logger::notice('Empty content', ['id' => $contact['id'], 'url' => $contact['poll']]); + DI::logger()->notice('Empty content', ['id' => $contact['id'], 'url' => $contact['poll']]); + return false; + } + + if (strpos($curlResult->getContentType(), 'xml') === false) { + DI::logger()->notice('Unexpected content type.', ['id' => $contact['id'], 'url' => $contact['poll'], 'content-type' => $curlResult->getContentType()]); return false; } if (!strstr($xml, '<')) { - Logger::notice('response did not contain XML.', ['id' => $contact['id'], 'url' => $contact['poll']]); + DI::logger()->notice('response did not contain XML.', ['id' => $contact['id'], 'url' => $contact['poll']]); return false; } - Logger::notice('Consume feed of contact', ['id' => $contact['id'], 'url' => $contact['poll'], 'Content-Type' => $curlResult->getHeader('Content-Type')]); + DI::logger()->notice('Consume feed of contact', ['id' => $contact['id'], 'url' => $contact['poll'], 'Content-Type' => $curlResult->getHeader('Content-Type')]); return !empty(Feed::import($xml, $importer, $contact)); } @@ -200,56 +208,61 @@ class OnePoll */ private static function pollMail(array $contact, int $importer_uid, string $updated): bool { - Logger::info('Fetching mails', ['addr' => $contact['addr']]); + DI::logger()->info('Fetching mails', ['addr' => $contact['addr']]); $mail_disabled = ((function_exists('imap_open') && !DI::config()->get('system', 'imap_disabled')) ? 0 : 1); if ($mail_disabled) { - Logger::notice('Mail is disabled'); + DI::logger()->notice('Mail is disabled'); return false; } - Logger::info('Mail is enabled'); + DI::logger()->info('Mail is enabled'); - $mbox = null; + $mbox = false; $user = DBA::selectFirst('user', ['prvkey'], ['uid' => $importer_uid]); - $condition = ["`server` != '' AND `uid` = ?", $importer_uid]; - $mailconf = DBA::selectFirst('mailacct', [], $condition); + $condition = ["`server` != ? AND `user` != ? AND `port` != ? AND `uid` = ?", '', '', 0, $importer_uid]; + $mailconf = DBA::selectFirst('mailacct', [], $condition); if (DBA::isResult($user) && DBA::isResult($mailconf)) { - $mailbox = Email::constructMailboxName($mailconf); + $mailbox = Email::constructMailboxName($mailconf); $password = ''; openssl_private_decrypt(hex2bin($mailconf['pass']), $password, $user['prvkey']); $mbox = Email::connect($mailbox, $mailconf['user'], $password); unset($password); - Logger::notice('Connect', ['user' => $mailconf['user']]); - if ($mbox) { - $fields = ['last_check' => $updated]; - DBA::update('mailacct', $fields, ['id' => $mailconf['id']]); - Logger::notice('Connected', ['user' => $mailconf['user']]); - } else { - Logger::notice('Connection error', ['user' => $mailconf['user'], 'error' => imap_errors()]); + DI::logger()->notice('Connect', ['user' => $mailconf['user']]); + + if ($mbox === false) { + DI::logger()->notice('Connection error', ['user' => $mailconf['user'], 'error' => imap_errors()]); return false; } + + $fields = ['last_check' => $updated]; + DBA::update('mailacct', $fields, ['id' => $mailconf['id']]); + DI::logger()->notice('Connected', ['user' => $mailconf['user']]); } - if (empty($mbox)) { + if ($mbox === false) { return false; } $msgs = Email::poll($mbox, $contact['addr']); if (count($msgs)) { - Logger::info('Parsing mails', ['count' => count($msgs), 'addr' => $contact['addr'], 'user' => $mailconf['user']]); + DI::logger()->info('Parsing mails', ['count' => count($msgs), 'addr' => $contact['addr'], 'user' => $mailconf['user']]); $metas = Email::messageMeta($mbox, implode(',', $msgs)); if (count($metas) != count($msgs)) { - Logger::info("for " . $mailconf['user'] . " there are ". count($msgs) . " messages but received " . count($metas) . " metas"); + DI::logger()->info("for " . $mailconf['user'] . " there are ". count($msgs) . " messages but received " . count($metas) . " metas"); } else { $msgs = array_combine($msgs, $metas); foreach ($msgs as $msg_uid => $meta) { - Logger::info('Parsing mail', ['message-uid' => $msg_uid]); + if (empty($meta->message_id)) { + continue; + } + + DI::logger()->info('Parsing mail', ['message-uid' => $msg_uid]); $datarray = [ 'uid' => $importer_uid, @@ -265,36 +278,37 @@ class OnePoll // $meta = Email::messageMeta($mbox, $msg_uid); // Have we seen it before? - $fields = ['deleted', 'id']; + $fields = ['deleted', 'id']; $condition = ['uid' => $importer_uid, 'uri' => $datarray['uri']]; - $item = Post::selectFirst($fields, $condition); + $item = Post::selectFirst($fields, $condition); if (DBA::isResult($item)) { - Logger::info('Mail: Seen before ' . $msg_uid . ' for ' . $mailconf['user'] . ' UID: ' . $importer_uid . ' URI: ' . $datarray['uri']); + DI::logger()->info('Mail: Seen before ' . $msg_uid . ' for ' . $mailconf['user'] . ' UID: ' . $importer_uid . ' URI: ' . $datarray['uri']); // Only delete when mails aren't automatically moved or deleted - if (($mailconf['action'] != 1) && ($mailconf['action'] != 3)) + if (($mailconf['action'] != 1) && ($mailconf['action'] != 3)) { if ($meta->deleted && ! $item['deleted']) { $fields = ['deleted' => true, 'changed' => $updated]; Item::update($fields, ['id' => $item['id']]); } + } switch ($mailconf['action']) { case 0: - Logger::info('Mail: Seen before ' . $msg_uid . ' for ' . $mailconf['user'] . '. Doing nothing.'); + DI::logger()->info('Mail: Seen before ' . $msg_uid . ' for ' . $mailconf['user'] . '. Doing nothing.'); break; case 1: - Logger::notice('Mail: Deleting ' . $msg_uid . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Deleting ' . $msg_uid . ' for ' . $mailconf['user']); imap_delete($mbox, $msg_uid, FT_UID); break; case 2: - Logger::notice('Mail: Mark as seen ' . $msg_uid . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Mark as seen ' . $msg_uid . ' for ' . $mailconf['user']); imap_setflag_full($mbox, $msg_uid, "\\Seen", ST_UID); break; case 3: - Logger::notice('Mail: Moving ' . $msg_uid . ' to ' . $mailconf['movetofolder'] . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Moving ' . $msg_uid . ' to ' . $mailconf['movetofolder'] . ' for ' . $mailconf['user']); imap_setflag_full($mbox, $msg_uid, "\\Seen", ST_UID); if ($mailconf['movetofolder'] != '') { imap_mail_move($mbox, $msg_uid, $mailconf['movetofolder'], FT_UID); @@ -314,19 +328,19 @@ class OnePoll if ($raw_refs) { $refs_arr = explode(' ', $raw_refs); if (count($refs_arr)) { - for ($x = 0; $x < count($refs_arr); $x ++) { - $refs_arr[$x] = Email::msgid2iri(str_replace(['<', '>', ' '],['', '', ''], $refs_arr[$x])); + for ($x = 0; $x < count($refs_arr); $x++) { + $refs_arr[$x] = Email::msgid2iri(str_replace(['<', '>', ' '], ['', '', ''], $refs_arr[$x])); } } $condition = ['uri' => $refs_arr, 'uid' => $importer_uid]; - $parent = Post::selectFirst(['uri'], $condition); + $parent = Post::selectFirst(['uri'], $condition); if (DBA::isResult($parent)) { $datarray['thr-parent'] = $parent['uri']; } } // Decoding the header - $subject = imap_mime_header_decode($meta->subject ?? ''); + $subject = imap_mime_header_decode($meta->subject ?? ''); $datarray['title'] = ""; foreach ($subject as $subpart) { if ($subpart->charset != "default") { @@ -351,8 +365,8 @@ class OnePoll // If it seems to be a reply but a header couldn't be found take the last message with matching subject if (empty($datarray['thr-parent']) && $reply) { $condition = ['title' => $datarray['title'], 'uid' => $importer_uid, 'network' => Protocol::MAIL]; - $params = ['order' => ['created' => true]]; - $parent = Post::selectFirst(['uri'], $condition, $params); + $params = ['order' => ['created' => true]]; + $parent = Post::selectFirst(['uri'], $condition, $params); if (DBA::isResult($parent)) { $datarray['thr-parent'] = $parent['uri']; } @@ -387,12 +401,12 @@ class OnePoll $fromname = $headers->from[0]->personal; } - $datarray['author-name'] = $fromname; - $datarray['author-link'] = 'mailto:' . $frommail; + $datarray['author-name'] = $fromname; + $datarray['author-link'] = 'mailto:' . $frommail; $datarray['author-avatar'] = $contact['photo']; - $datarray['owner-name'] = $contact['name']; - $datarray['owner-link'] = 'mailto:' . $contact['addr']; + $datarray['owner-name'] = $contact['name']; + $datarray['owner-link'] = 'mailto:' . $contact['addr']; $datarray['owner-avatar'] = $contact['photo']; if (empty($datarray['thr-parent']) || ($datarray['thr-parent'] === $datarray['uri'])) { @@ -400,37 +414,37 @@ class OnePoll } if (!DI::pConfig()->get($importer_uid, 'system', 'allow_public_email_replies')) { - $datarray['private'] = Item::PRIVATE; + $datarray['private'] = Item::PRIVATE; $datarray['allow_cid'] = '<' . $contact['id'] . '>'; } $datarray = Email::getMessage($mbox, $msg_uid, $reply, $datarray); if (empty($datarray['body'])) { - Logger::warning('Cannot fetch mail', ['msg-id' => $msg_uid, 'uid' => $mailconf['user']]); + DI::logger()->warning('Cannot fetch mail', ['msg-id' => $msg_uid, 'uid' => $mailconf['user']]); continue; } - Logger::notice('Mail: Importing ' . $msg_uid . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Importing ' . $msg_uid . ' for ' . $mailconf['user']); Item::insert($datarray); switch ($mailconf['action']) { case 0: - Logger::info('Mail: Seen before ' . $msg_uid . ' for ' . $mailconf['user'] . '. Doing nothing.'); + DI::logger()->info('Mail: Seen before ' . $msg_uid . ' for ' . $mailconf['user'] . '. Doing nothing.'); break; case 1: - Logger::notice('Mail: Deleting ' . $msg_uid . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Deleting ' . $msg_uid . ' for ' . $mailconf['user']); imap_delete($mbox, $msg_uid, FT_UID); break; case 2: - Logger::notice('Mail: Mark as seen ' . $msg_uid . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Mark as seen ' . $msg_uid . ' for ' . $mailconf['user']); imap_setflag_full($mbox, $msg_uid, "\\Seen", ST_UID); break; case 3: - Logger::notice('Mail: Moving ' . $msg_uid . ' to ' . $mailconf['movetofolder'] . ' for ' . $mailconf['user']); + DI::logger()->notice('Mail: Moving ' . $msg_uid . ' to ' . $mailconf['movetofolder'] . ' for ' . $mailconf['user']); imap_setflag_full($mbox, $msg_uid, "\\Seen", ST_UID); if ($mailconf['movetofolder'] != '') { imap_mail_move($mbox, $msg_uid, $mailconf['movetofolder'], FT_UID); @@ -440,11 +454,11 @@ class OnePoll } } } else { - Logger::notice('No mails', ['user' => $mailconf['user']]); + DI::logger()->notice('No mails', ['user' => $mailconf['user']]); } - Logger::info('Closing connection', ['user' => $mailconf['user']]); + DI::logger()->info('Closing connection', ['user' => $mailconf['user']]); imap_close($mbox); return true; @@ -477,15 +491,20 @@ class OnePoll $params = 'hub.mode=' . $hubmode . '&hub.callback=' . urlencode($push_url) . '&hub.topic=' . urlencode($contact['poll']) . '&hub.verify=async&hub.verify_token=' . $verify_token; - Logger::info('Hub subscription start', ['mode' => $hubmode, 'name' => $contact['name'], 'hub' => $url, 'endpoint' => $push_url, 'verifier' => $verify_token]); + DI::logger()->info('Hub subscription start', ['mode' => $hubmode, 'name' => $contact['name'], 'hub' => $url, 'endpoint' => $push_url, 'verifier' => $verify_token]); if (!strlen($contact['hub-verify']) || ($contact['hub-verify'] != $verify_token)) { Contact::update(['hub-verify' => $verify_token], ['id' => $contact['id']]); } - $postResult = DI::httpClient()->post($url, $params); + try { + $postResult = DI::httpClient()->post($url, $params, [], 0, HttpClientRequest::PUBSUB); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return false; + } - Logger::info('Hub subscription done', ['result' => $postResult->getReturnCode()]); + DI::logger()->info('Hub subscription done', ['result' => $postResult->getReturnCode()]); return $postResult->isSuccess(); } diff --git a/src/Worker/OptimizeTables.php b/src/Worker/OptimizeTables.php index 50f9341d72..85231a0466 100644 --- a/src/Worker/OptimizeTables.php +++ b/src/Worker/OptimizeTables.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\DI; @@ -34,11 +19,11 @@ class OptimizeTables { if (!DI::lock()->acquire('optimize_tables', 0)) { - Logger::warning('Lock could not be acquired'); + DI::logger()->warning('Lock could not be acquired'); return; } - Logger::info('Optimize start'); + DI::logger()->info('Optimize start'); DBA::optimizeTable('cache'); DBA::optimizeTable('locks'); @@ -74,7 +59,7 @@ class OptimizeTables DBA::optimizeTable('tag'); } - Logger::info('Optimize end'); + DI::logger()->info('Optimize end'); DI::lock()->release('optimize_tables'); } diff --git a/src/Worker/PollContacts.php b/src/Worker/PollContacts.php index 80b7ae4456..5bb7987ee6 100644 --- a/src/Worker/PollContacts.php +++ b/src/Worker/PollContacts.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Worker; use Friendica\Database\DBA; @@ -41,14 +26,18 @@ class PollContacts $abandon_days = 0; } - $condition = ['network' => [Protocol::FEED, Protocol::MAIL, Protocol::OSTATUS], 'self' => false, 'blocked' => false, 'archive' => false]; + $condition = ['network' => [Protocol::FEED, Protocol::MAIL], 'self' => false, 'blocked' => false, 'archive' => false]; if (!empty($abandon_days)) { - $condition = DBA::mergeConditions($condition, - ["`uid` != ? AND `uid` IN (SELECT `uid` FROM `user` WHERE `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` AND `last-activity` > ?)", 0, DateTimeFormat::utc('now - ' . $abandon_days . ' days')]); - } else { - $condition = DBA::mergeConditions($condition, - ["`uid` != ? AND `uid` IN (SELECT `uid` FROM `user` WHERE `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`)", 0]); + $condition = DBA::mergeConditions( + $condition, + ["`uid` != ? AND `uid` IN (SELECT `uid` FROM `user` WHERE `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` AND `last-activity` > ?)", 0, DateTimeFormat::utc('now - ' . $abandon_days . ' days')] + ); + } else { + $condition = DBA::mergeConditions( + $condition, + ["`uid` != ? AND `uid` IN (SELECT `uid` FROM `user` WHERE `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`)", 0] + ); } $contacts = DBA::select('contact', ['id', 'nick', 'name', 'network', 'archive', 'last-update', 'priority', 'rating'], $condition); @@ -62,11 +51,11 @@ class PollContacts continue; } - $now = DateTimeFormat::utcNow(); + $now = DateTimeFormat::utcNow(); $next_update = DateTimeFormat::utc($contact['last-update'] . ' + ' . $interval . ' minute'); - if ($now < $next_update) { - Logger::debug('No update', ['cid' => $contact['id'], 'interval' => $interval, 'next' => $next_update, 'now' => $now]); + if ($now < $next_update) { + DI::logger()->debug('No update', ['cid' => $contact['id'], 'interval' => $interval, 'next' => $next_update, 'now' => $now]); continue; } @@ -78,7 +67,7 @@ class PollContacts $priority = Worker::PRIORITY_LOW; } - Logger::notice("Polling " . $contact["network"] . " " . $contact["id"] . " " . $contact['priority'] . " " . $contact["nick"] . " " . $contact["name"]); + DI::logger()->notice("Polling " . $contact["network"] . " " . $contact["id"] . " " . $contact['priority'] . " " . $contact["nick"] . " " . $contact["name"]); Worker::add(['priority' => $priority, 'dont_fork' => true, 'force_priority' => true], 'OnePoll', (int)$contact['id']); Worker::coolDown(); diff --git a/src/Worker/PostUpdate.php b/src/Worker/PostUpdate.php index 8d941f9032..df3d843dd3 100644 --- a/src/Worker/PostUpdate.php +++ b/src/Worker/PostUpdate.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/ProcessQueue.php b/src/Worker/ProcessQueue.php index abafaee19a..bf8b8df1a6 100644 --- a/src/Worker/ProcessQueue.php +++ b/src/Worker/ProcessQueue.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; +use Friendica\DI; use Friendica\Protocol\ActivityPub\Queue; class ProcessQueue @@ -35,8 +21,8 @@ class ProcessQueue */ public static function execute(int $id) { - Logger::info('Start processing queue entry', ['id' => $id]); + DI::logger()->info('Start processing queue entry', ['id' => $id]); $result = Queue::process($id); - Logger::info('Successfully processed queue entry', ['result' => $result, 'id' => $id]); + DI::logger()->info('Successfully processed queue entry', ['result' => $result, 'id' => $id]); } } diff --git a/src/Worker/ProcessReplyByUri.php b/src/Worker/ProcessReplyByUri.php index 9f2c109309..88b00bc1a2 100644 --- a/src/Worker/ProcessReplyByUri.php +++ b/src/Worker/ProcessReplyByUri.php @@ -1,27 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; +use Friendica\DI; use Friendica\Protocol\ActivityPub\Queue; class ProcessReplyByUri @@ -35,8 +21,8 @@ class ProcessReplyByUri */ public static function execute(string $uri) { - Logger::info('Start processing queued replies', ['url' => $uri]); + DI::logger()->info('Start processing queued replies', ['url' => $uri]); $count = Queue::processReplyByUri($uri); - Logger::info('Successfully processed queued replies', ['count' => $count, 'url' => $uri]); + DI::logger()->info('Successfully processed queued replies', ['count' => $count, 'url' => $uri]); } } diff --git a/src/Worker/ProcessUnprocessedEntries.php b/src/Worker/ProcessUnprocessedEntries.php new file mode 100644 index 0000000000..afde8bb26b --- /dev/null +++ b/src/Worker/ProcessUnprocessedEntries.php @@ -0,0 +1,26 @@ +info('Start processing unprocessed entries'); + Queue::processAll(); + DI::logger()->info('Successfully processed unprocessed entries'); + } +} diff --git a/src/Worker/ProfileUpdate.php b/src/Worker/ProfileUpdate.php index 23fb6d27c1..fe88b387e6 100644 --- a/src/Worker/ProfileUpdate.php +++ b/src/Worker/ProfileUpdate.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\DI; use Friendica\Protocol\Delivery; @@ -31,7 +16,8 @@ use Friendica\Protocol\ActivityPub; /** * Send updated profile data to Diaspora and ActivityPub */ -class ProfileUpdate { +class ProfileUpdate +{ /** * Sends updated profile data to Diaspora and ActivityPub * @@ -44,13 +30,14 @@ class ProfileUpdate { return; } - $a = DI::app(); + $appHelper = DI::appHelper(); $inboxes = ActivityPub\Transmitter::fetchTargetInboxesforUser($uid); foreach ($inboxes as $inbox => $receivers) { - Logger::info('Profile update for user ' . $uid . ' to ' . $inbox .' via ActivityPub'); - Worker::add(['priority' => $a->getQueueValue('priority'), 'created' => $a->getQueueValue('created'), 'dont_fork' => true], + DI::logger()->info('Profile update for user ' . $uid . ' to ' . $inbox .' via ActivityPub'); + Worker::add( + ['priority' => $appHelper->getQueueValue('priority'), 'created' => $appHelper->getQueueValue('created'), 'dont_fork' => true], 'APDelivery', Delivery::PROFILEUPDATE, 0, diff --git a/src/Worker/PubSubPublish.php b/src/Worker/PubSubPublish.php deleted file mode 100644 index bb10361c58..0000000000 --- a/src/Worker/PubSubPublish.php +++ /dev/null @@ -1,96 +0,0 @@ -. - * - */ - -namespace Friendica\Worker; - -use Friendica\Core\Logger; -use Friendica\Database\DBA; -use Friendica\DI; -use Friendica\Model\PushSubscriber; -use Friendica\Protocol\OStatus; - -class PubSubPublish -{ - /** - * Publishes subscriber id - * - * @param int $pubsubpublish_id Push subscriber id - * @return void - */ - public static function execute(int $pubsubpublish_id = 0) - { - if ($pubsubpublish_id == 0) { - return; - } - - self::publish($pubsubpublish_id); - } - - /** - * Publishes push subscriber - * - * @param int $id Push subscriber id - * @return void - */ - private static function publish(int $id) - { - $subscriber = DBA::selectFirst('push_subscriber', [], ['id' => $id]); - if (!DBA::isResult($subscriber)) { - return; - } - - /// @todo Check server status with GServer::check() - // Before this can be done we need a way to safely detect the server url. - - Logger::info('Generate feed of user ' . $subscriber['nickname'] . ' to ' . $subscriber['callback_url'] . ' - last updated ' . $subscriber['last_update']); - - $last_update = $subscriber['last_update']; - $params = OStatus::feed($subscriber['nickname'], $last_update); - - if (!$params) { - return; - } - - $hmac_sig = hash_hmac('sha1', $params, $subscriber['secret']); - - $headers = [ - 'Content-type' => 'application/atom+xml', - 'Link' => sprintf('<%s>;rel=hub,<%s>;rel=self', - DI::baseUrl() . '/pubsubhubbub/' . $subscriber['nickname'], - $subscriber['topic']), - 'X-Hub-Signature' => 'sha1=' . $hmac_sig]; - - Logger::debug('POST', ['headers' => $headers, 'params' => $params]); - - $postResult = DI::httpClient()->post($subscriber['callback_url'], $params, $headers); - $ret = $postResult->getReturnCode(); - - if ($ret >= 200 && $ret <= 299) { - Logger::info('Successfully pushed to ' . $subscriber['callback_url']); - - PushSubscriber::reset($subscriber['id'], $last_update); - } else { - Logger::notice('Delivery error when pushing to ' . $subscriber['callback_url'] . ' HTTP: ' . $ret); - - PushSubscriber::delay($subscriber['id']); - } - } -} diff --git a/src/Worker/PullDirectory.php b/src/Worker/PullDirectory.php index ac8f66535a..2410be7d96 100644 --- a/src/Worker/PullDirectory.php +++ b/src/Worker/PullDirectory.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Search; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; class PullDirectory { @@ -35,29 +21,29 @@ class PullDirectory public static function execute() { if (!DI::config()->get('system', 'synchronize_directory')) { - Logger::info('Synchronization deactivated'); + DI::logger()->info('Synchronization deactivated'); return; } $directory = Search::getGlobalDirectory(); if (empty($directory)) { - Logger::info('No directory configured'); + DI::logger()->info('No directory configured'); return; } $now = (int)(DI::keyValue()->get('last-directory-sync') ?? 0); - Logger::info('Synchronization started.', ['now' => $now, 'directory' => $directory]); + DI::logger()->info('Synchronization started.', ['now' => $now, 'directory' => $directory]); - $result = DI::httpClient()->fetch($directory . '/sync/pull/since/' . $now, HttpClientAccept::JSON); + $result = DI::httpClient()->fetch($directory . '/sync/pull/since/' . $now, HttpClientAccept::JSON, 0, '', HttpClientRequest::CONTACTDISCOVER); if (empty($result)) { - Logger::info('Directory server return empty result.', ['directory' => $directory]); + DI::logger()->info('Directory server return empty result.', ['directory' => $directory]); return; } $contacts = json_decode($result, true); if (empty($contacts['results'])) { - Logger::info('No results fetched.', ['directory' => $directory]); + DI::logger()->info('No results fetched.', ['directory' => $directory]); return; } @@ -66,6 +52,6 @@ class PullDirectory $now = $contacts['now'] ?? 0; DI::keyValue()->set('last-directory-sync', $now); - Logger::info('Synchronization ended', ['now' => $now, 'count' => $result['count'], 'added' => $result['added'], 'updated' => $result['updated'], 'unchanged' => $result['unchanged'], 'directory' => $directory]); + DI::logger()->info('Synchronization ended', ['now' => $now, 'count' => $result['count'], 'added' => $result['added'], 'updated' => $result['updated'], 'unchanged' => $result['unchanged'], 'directory' => $directory]); } } diff --git a/src/Worker/PushSubscription.php b/src/Worker/PushSubscription.php index 17b47f1cbd..b7cba9ccae 100644 --- a/src/Worker/PushSubscription.php +++ b/src/Worker/PushSubscription.php @@ -1,29 +1,14 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\Plaintext; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Factory\Api\Mastodon\Notification as NotificationFactory; @@ -46,30 +31,30 @@ class PushSubscription */ public static function execute(int $sid, int $nid) { - Logger::info('Start', ['subscription' => $sid, 'notification' => $nid]); + DI::logger()->info('Start', ['subscription' => $sid, 'notification' => $nid]); $subscription = DBA::selectFirst('subscription', [], ['id' => $sid]); if (empty($subscription)) { - Logger::info('Subscription not found', ['subscription' => $sid]); + DI::logger()->info('Subscription not found', ['subscription' => $sid]); return; } try { $notification = DI::notification()->selectOneById($nid); } catch (NotFoundException $e) { - Logger::info('Notification not found', ['notification' => $nid]); + DI::logger()->info('Notification not found', ['notification' => $nid]); return; } $application_token = DBA::selectFirst('application-token', [], ['application-id' => $subscription['application-id'], 'uid' => $subscription['uid']]); if (empty($application_token)) { - Logger::info('Application token not found', ['application' => $subscription['application-id']]); + DI::logger()->info('Application token not found', ['application' => $subscription['application-id']]); return; } $user = User::getById($notification->uid); if (empty($user)) { - Logger::info('User not found', ['application' => $subscription['uid']]); + DI::logger()->info('User not found', ['application' => $subscription['uid']]); return; } @@ -90,7 +75,7 @@ class PushSubscription } $message = DI::notificationFactory()->getMessageFromNotification($notification); - $title = $message['plain'] ?? ''; + $title = $message['plain'] ?? ''; $push = Subscription::create([ 'contentEncoding' => 'aesgcm', @@ -111,7 +96,7 @@ class PushSubscription 'body' => $body ?: $l10n->t('Empty Post'), ]; - Logger::info('Payload', ['payload' => $payload]); + DI::logger()->info('Payload', ['payload' => $payload]); $auth = [ 'VAPID' => [ @@ -128,9 +113,9 @@ class PushSubscription $endpoint = $report->getRequest()->getUri()->__toString(); if ($report->isSuccess()) { - Logger::info('Message sent successfully for subscription', ['subscription' => $sid, 'notification' => $nid, 'endpoint' => $endpoint]); + DI::logger()->info('Message sent successfully for subscription', ['subscription' => $sid, 'notification' => $nid, 'endpoint' => $endpoint]); } else { - Logger::info('Message failed to sent for subscription', ['subscription' => $sid, 'notification' => $nid, 'endpoint' => $endpoint, 'reason' => $report->getReason()]); + DI::logger()->info('Message failed to sent for subscription', ['subscription' => $sid, 'notification' => $nid, 'endpoint' => $endpoint, 'reason' => $report->getReason()]); } } } diff --git a/src/Worker/RemoveUnusedAvatars.php b/src/Worker/RemoveUnusedAvatars.php index 3d5c796269..3d1013a279 100644 --- a/src/Worker/RemoveUnusedAvatars.php +++ b/src/Worker/RemoveUnusedAvatars.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; -use Friendica\Core\Worker; +use Friendica\Contact\Avatar; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Photo; @@ -34,33 +20,31 @@ class RemoveUnusedAvatars { public static function execute() { - $sql = "FROM `contact` INNER JOIN `photo` ON `contact`.`id` = `contact-id` - WHERE `contact`.`uid` = ? AND NOT `self` AND (`photo` != ? OR `thumb` != ? OR `micro` != ?) - AND NOT `nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` != ?) - AND NOT `contact`.`id` IN (SELECT `author-id` FROM `post-user` WHERE `author-id` = `contact`.`id`) - AND NOT `contact`.`id` IN (SELECT `owner-id` FROM `post-user` WHERE `owner-id` = `contact`.`id`) - AND NOT `contact`.`id` IN (SELECT `causer-id` FROM `post-user` WHERE `causer-id` IS NOT NULL AND `causer-id` = `contact`.`id`) - AND NOT `contact`.`id` IN (SELECT `cid` FROM `post-tag` WHERE `cid` = `contact`.`id`) - AND NOT `contact`.`id` IN (SELECT `contact-id` FROM `post-user` WHERE `contact-id` = `contact`.`id`);"; + $condition = [ + "`id` != ? AND `uid` = ? AND NOT `self` AND (`photo` != ? OR `thumb` != ? OR `micro` != ?) + AND NOT `nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` != ?) + AND NOT `id` IN (SELECT `author-id` FROM `post-user` WHERE `author-id` = `contact`.`id`) + AND NOT `id` IN (SELECT `owner-id` FROM `post-user` WHERE `owner-id` = `contact`.`id`) + AND NOT `id` IN (SELECT `causer-id` FROM `post-user` WHERE `causer-id` IS NOT NULL AND `causer-id` = `contact`.`id`) + AND NOT `id` IN (SELECT `cid` FROM `post-tag` WHERE `cid` = `contact`.`id`) + AND NOT `id` IN (SELECT `contact-id` FROM `post-user` WHERE `contact-id` = `contact`.`id`)", + 0, 0, '', '', '', 0 + ]; - $ret = DBA::fetchFirst("SELECT COUNT(*) AS `total` " . $sql, 0, '', '', '', 0); - $total = $ret['total'] ?? 0; - Logger::notice('Starting removal', ['total' => $total]); - $count = 0; - $contacts = DBA::p("SELECT `contact`.`id` " . $sql, 0, '', '', '', 0); + $total = DBA::count('contact', $condition); + DI::logger()->notice('Starting removal', ['total' => $total]); + $count = 0; + $contacts = DBA::select('contact', ['id', 'uri-id', 'uid', 'photo', 'thumb', 'micro'], $condition); while ($contact = DBA::fetch($contacts)) { - Contact::update(['photo' => '', 'thumb' => '', 'micro' => ''], ['id' => $contact['id']]); - Photo::delete(['contact-id' => $contact['id'], 'photo-type' => [Photo::CONTACT_AVATAR, Photo::CONTACT_BANNER]]); + if (Avatar::deleteCache($contact) || Photo::delete(['uid' => 0, 'contact-id' => $contact['id'], 'photo-type' => [Photo::CONTACT_AVATAR, Photo::CONTACT_BANNER]])) { + Contact::update(['photo' => '', 'thumb' => '', 'micro' => ''], ['id' => $contact['id']]); + } if ((++$count % 1000) == 0) { - if (!Worker::isInMaintenanceWindow()) { - Logger::notice('We are outside of the maintenance window, quitting'); - return; - } - Logger::info('In removal', ['count' => $count, 'total' => $total]); + DI::logger()->info('In removal', ['count' => $count, 'total' => $total]); } } DBA::close($contacts); - Logger::notice('Removal done', ['count' => $count, 'total' => $total]); + DI::logger()->notice('Removal done', ['count' => $count, 'total' => $total]); self::fixPhotoContacts(); self::deleteDuplicates(); @@ -68,29 +52,29 @@ class RemoveUnusedAvatars private static function fixPhotoContacts() { - $total = 0; - $deleted = 0; + $total = 0; + $deleted = 0; $updated1 = 0; $updated2 = 0; - Logger::notice('Starting contact fix'); + DI::logger()->notice('Starting contact fix'); $photos = DBA::select('photo', [], ["`uid` = ? AND `contact-id` IN (SELECT `id` FROM `contact` WHERE `uid` != ?) AND `contact-id` != ? AND `scale` IN (?, ?, ?)", 0, 0, 0, 4, 5, 6]); while ($photo = DBA::fetch($photos)) { $total++; $photo_contact = Contact::getById($photo['contact-id']); - $resource = Photo::ridFromURI($photo_contact['photo']); + $resource = Photo::ridFromURI($photo_contact['photo']); if ($photo['resource-id'] == $resource) { $contact = DBA::selectFirst('contact', [], ['nurl' => $photo_contact['nurl'], 'uid' => 0]); if (!empty($contact['photo']) && ($contact['photo'] == $photo_contact['photo'])) { - Logger::notice('Photo updated to public user', ['id' => $photo['id'], 'contact-id' => $contact['id']]); + DI::logger()->notice('Photo updated to public user', ['id' => $photo['id'], 'contact-id' => $contact['id']]); DBA::update('photo', ['contact-id' => $contact['id']], ['id' => $photo['id']]); $updated1++; } } else { - $updated = false; + $updated = false; $contacts = DBA::select('contact', [], ['nurl' => $photo_contact['nurl']]); while ($contact = DBA::fetch($contacts)) { if ($photo['resource-id'] == Photo::ridFromURI($contact['photo'])) { - Logger::notice('Photo updated to given user', ['id' => $photo['id'], 'contact-id' => $contact['id'], 'uid' => $contact['uid']]); + DI::logger()->notice('Photo updated to given user', ['id' => $photo['id'], 'contact-id' => $contact['id'], 'uid' => $contact['uid']]); DBA::update('photo', ['contact-id' => $contact['id'], 'uid' => $contact['uid']], ['id' => $photo['id']]); $updated = true; $updated2++; @@ -98,36 +82,36 @@ class RemoveUnusedAvatars } DBA::close($contacts); if (!$updated) { - Logger::notice('Photo deleted', ['id' => $photo['id']]); + DI::logger()->notice('Photo deleted', ['id' => $photo['id']]); Photo::delete(['id' => $photo['id']]); $deleted++; } } } DBA::close($photos); - Logger::notice('Contact fix done', ['total' => $total, 'updated1' => $updated1, 'updated2' => $updated2, 'deleted' => $deleted]); + DI::logger()->notice('Contact fix done', ['total' => $total, 'updated1' => $updated1, 'updated2' => $updated2, 'deleted' => $deleted]); } private static function deleteDuplicates() { $size = [4 => 'photo', 5 => 'thumb', 6 => 'micro']; - $total = 0; + $total = 0; $deleted = 0; - Logger::notice('Starting duplicate removal'); + DI::logger()->notice('Starting duplicate removal'); $photos = DBA::p("SELECT `photo`.`id`, `photo`.`uid`, `photo`.`scale`, `photo`.`album`, `photo`.`contact-id`, `photo`.`resource-id`, `contact`.`photo`, `contact`.`thumb`, `contact`.`micro` FROM `photo` INNER JOIN `contact` ON `contact`.`id` = `photo`.`contact-id` and `photo`.`contact-id` != ? AND `photo`.`scale` IN (?, ?, ?)", 0, 4, 5, 6); while ($photo = DBA::fetch($photos)) { $resource = Photo::ridFromURI($photo[$size[$photo['scale']]]); if ($resource != $photo['resource-id'] && !empty($resource)) { $total++; if (DBA::exists('photo', ['resource-id' => $resource, 'scale' => $photo['scale']])) { - Logger::notice('Photo deleted', ['id' => $photo['id']]); + DI::logger()->notice('Photo deleted', ['id' => $photo['id']]); Photo::delete(['id' => $photo['id']]); $deleted++; } } } DBA::close($photos); - Logger::notice('Duplicate removal done', ['total' => $total, 'deleted' => $deleted]); + DI::logger()->notice('Duplicate removal done', ['total' => $total, 'deleted' => $deleted]); } } diff --git a/src/Worker/RemoveUnusedContacts.php b/src/Worker/RemoveUnusedContacts.php index 4187ed0057..dd6b7e58d0 100644 --- a/src/Worker/RemoveUnusedContacts.php +++ b/src/Worker/RemoveUnusedContacts.php @@ -1,31 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; use Friendica\Contact\Avatar; -use Friendica\Core\Logger; use Friendica\Core\Protocol; +use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\Database\DBStructure; +use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\Photo; use Friendica\Util\DateTimeFormat; @@ -37,24 +24,53 @@ class RemoveUnusedContacts { public static function execute() { - $condition = ["`id` != ? AND `uid` = ? AND NOT `self` AND NOT `nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` != ?) - AND (NOT `network` IN (?, ?, ?, ?, ?, ?) OR (`archive` AND `success_update` < ?)) - AND NOT `id` IN (SELECT `author-id` FROM `post-user` WHERE `author-id` = `contact`.`id`) - AND NOT `id` IN (SELECT `owner-id` FROM `post-user` WHERE `owner-id` = `contact`.`id`) - AND NOT `id` IN (SELECT `causer-id` FROM `post-user` WHERE `causer-id` IS NOT NULL AND `causer-id` = `contact`.`id`) - AND NOT `id` IN (SELECT `cid` FROM `post-tag` WHERE `cid` = `contact`.`id`) - AND NOT `id` IN (SELECT `contact-id` FROM `post-user` WHERE `contact-id` = `contact`.`id`) - AND NOT `id` IN (SELECT `cid` FROM `user-contact` WHERE `cid` = `contact`.`id`) - AND NOT `id` IN (SELECT `cid` FROM `event` WHERE `cid` = `contact`.`id`) - AND NOT `id` IN (SELECT `contact-id` FROM `group_member` WHERE `contact-id` = `contact`.`id`) - AND `created` < ?", - 0, 0, 0, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL, Protocol::ACTIVITYPUB, DateTimeFormat::utc('now - 365 days'), DateTimeFormat::utc('now - 30 days')]; + $loop = 0; + while (self::removeContacts(++$loop)) { + DI::logger()->info('In removal', ['loop' => $loop]); + } - $total = DBA::count('contact', $condition); - Logger::notice('Starting removal', ['total' => $total]); - $count = 0; - $contacts = DBA::select('contact', ['id', 'uid', 'photo', 'thumb', 'micro'], $condition); + DI::logger()->notice('Remove apcontact entries with no related contact'); + DBA::delete('apcontact', ["`uri-id` NOT IN (SELECT `uri-id` FROM `contact`) AND `updated` < ?", DateTimeFormat::utc('now - 30 days')]); + DI::logger()->notice('Removed apcontact entries with no related contact', ['count' => DBA::affectedRows()]); + + DI::logger()->notice('Remove diaspora-contact entries with no related contact'); + DBA::delete('diaspora-contact', ["`uri-id` NOT IN (SELECT `uri-id` FROM `contact`) AND `updated` < ?", DateTimeFormat::utc('now - 30 days')]); + DI::logger()->notice('Removed diaspora-contact entries with no related contact', ['count' => DBA::affectedRows()]); + } + + public static function removeContacts(int $loop): bool + { + DI::logger()->notice('Starting removal', ['loop' => $loop]); + + $condition = [ + "`id` != ? AND `uid` = ? AND NOT `self` AND NOT `uri-id` IN (SELECT `uri-id` FROM `contact` WHERE `uid` != ?) + AND NOT EXISTS(SELECT `author-id` FROM `post-user` WHERE `author-id` = `contact`.`id`) + AND NOT EXISTS(SELECT `owner-id` FROM `post-user` WHERE `owner-id` = `contact`.`id`) + AND NOT EXISTS(SELECT `causer-id` FROM `post-user` WHERE `causer-id` IS NOT NULL AND `causer-id` = `contact`.`id`) + AND NOT EXISTS(SELECT `cid` FROM `post-tag` WHERE `cid` = `contact`.`id`) + AND NOT EXISTS(SELECT `contact-id` FROM `post-user` WHERE `contact-id` = `contact`.`id`) + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `cid` = `contact`.`id`) + AND NOT EXISTS(SELECT `cid` FROM `event` WHERE `cid` = `contact`.`id`) + AND NOT EXISTS(SELECT `cid` FROM `group` WHERE `cid` = `contact`.`id`) + AND NOT EXISTS(SELECT `author-id` FROM `mail` WHERE `author-id` = `contact`.`id`) + AND NOT EXISTS(SELECT `contact-id` FROM `mail` WHERE `contact-id` = `contact`.`id`) + AND NOT EXISTS(SELECT `contact-id` FROM `group_member` WHERE `contact-id` = `contact`.`id`) + AND `created` < ?", 0, 0, 0, DateTimeFormat::utc('now - 7 days') + ]; + + if (!DI::config()->get('remove_all_unused_contacts')) { + $condition2 = [ + "(NOT `network` IN (?, ?, ?, ?, ?, ?) OR `archive`)", + Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL, Protocol::ACTIVITYPUB + ]; + + $condition = DBA::mergeConditions($condition2, $condition); + } + + $contacts = DBA::select('contact', ['id', 'uid', 'photo', 'thumb', 'micro'], $condition, ['limit' => 1000]); + $count = 0; while ($contact = DBA::fetch($contacts)) { + ++$count; Photo::delete(['uid' => $contact['uid'], 'contact-id' => $contact['id']]); Avatar::deleteCache($contact); @@ -83,11 +99,9 @@ class RemoveUnusedContacts DBA::delete('post-thread-user', ['causer-id' => $contact['id']]); Contact::deleteById($contact['id']); - if ((++$count % 1000) == 0) { - Logger::info('In removal', ['count' => $count, 'total' => $total]); - } } DBA::close($contacts); - Logger::notice('Removal done', ['count' => $count, 'total' => $total]); + DI::logger()->notice('Removal done', ['count' => $count]); + return ($count == 1000 && Worker::isInMaintenanceWindow()); } } diff --git a/src/Worker/RemoveUnusedTags.php b/src/Worker/RemoveUnusedTags.php index 2090effdd3..df42e0a977 100644 --- a/src/Worker/RemoveUnusedTags.php +++ b/src/Worker/RemoveUnusedTags.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/RemoveUser.php b/src/Worker/RemoveUser.php index ac67d69f96..08881a61d1 100644 --- a/src/Worker/RemoveUser.php +++ b/src/Worker/RemoveUser.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/SearchDirectory.php b/src/Worker/SearchDirectory.php index 0bd17e0f82..ac7242235f 100644 --- a/src/Worker/SearchDirectory.php +++ b/src/Worker/SearchDirectory.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Logger; use Friendica\Core\Search; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; class SearchDirectory { @@ -34,7 +20,7 @@ class SearchDirectory public static function execute($search) { if (!DI::config()->get('system', 'poco_local_search')) { - Logger::info('Local search is not enabled'); + DI::logger()->info('Local search is not enabled'); return; } @@ -42,12 +28,12 @@ class SearchDirectory if (!is_null($data)) { // Only search for the same item every 24 hours if (time() < $data + (60 * 60 * 24)) { - Logger::info('Already searched this in the last 24 hours', ['search' => $search]); + DI::logger()->info('Already searched this in the last 24 hours', ['search' => $search]); return; } } - $x = DI::httpClient()->fetch(Search::getGlobalDirectory() . '/lsearch?p=1&n=500&search=' . urlencode($search), HttpClientAccept::JSON); + $x = DI::httpClient()->fetch(Search::getGlobalDirectory() . '/lsearch?p=1&n=500&search=' . urlencode($search), HttpClientAccept::JSON, 0, '', HttpClientRequest::CONTACTDISCOVER); $j = json_decode($x); if (!empty($j->results)) { diff --git a/src/Worker/SetSeen.php b/src/Worker/SetSeen.php new file mode 100644 index 0000000000..ea26494abd --- /dev/null +++ b/src/Worker/SetSeen.php @@ -0,0 +1,23 @@ + false], ['unseen' => true, 'uid' => $uid]); + DI::logger()->debug('Set seen', ['uid' => $uid, 'ret' => $ret]); + } +} diff --git a/src/Worker/SpoolPost.php b/src/Worker/SpoolPost.php index 5dc021fa0b..0e70e3a440 100644 --- a/src/Worker/SpoolPost.php +++ b/src/Worker/SpoolPost.php @@ -1,44 +1,32 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\System; +use Friendica\DI; use Friendica\Model\Item; /** * Posts items that where spooled because they couldn't be posted. */ -class SpoolPost { - public static function execute() { +class SpoolPost +{ + public static function execute() + { $path = System::getSpoolPath(); - if (($path != '') && is_writable($path)){ + if (($path != '') && is_writable($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { // It is not named like a spool file, so we don't care. if (substr($file, 0, 5) != "item-") { - Logger::info('Spool file does not start with "item-"', ['file' => $file]); + DI::logger()->info('Spool file does not start with "item-"', ['file' => $file]); continue; } @@ -46,13 +34,13 @@ class SpoolPost { // We don't care about directories either if (filetype($fullfile) != "file") { - Logger::info('Spool file is no file', ['file' => $file]); + DI::logger()->info('Spool file is no file', ['file' => $file]); continue; } // We can't read or write the file? So we don't care about it. if (!is_writable($fullfile) || !is_readable($fullfile)) { - Logger::warning('Spool file has insufficent permissions', ['file' => $file, 'writable' => is_writable($fullfile), 'readable' => is_readable($fullfile)]); + DI::logger()->warning('Spool file has insufficent permissions', ['file' => $file, 'writable' => is_writable($fullfile), 'readable' => is_readable($fullfile)]); continue; } @@ -60,19 +48,19 @@ class SpoolPost { // If it isn't an array then it is no spool file if (!is_array($arr)) { - Logger::notice('Spool file is no array', ['file' => $file]); + DI::logger()->notice('Spool file is no array', ['file' => $file]); continue; } // Skip if it doesn't seem to be an item array if (!isset($arr['uid']) && !isset($arr['uri']) && !isset($arr['network'])) { - Logger::warning('Spool file does not contain the needed fields', ['file' => $file]); + DI::logger()->warning('Spool file does not contain the needed fields', ['file' => $file]); continue; } $result = Item::insert($arr); - Logger::info('Spool file is stored', ['file' => $file, 'result' => $result]); + DI::logger()->info('Spool file is stored', ['file' => $file, 'result' => $result]); unlink($fullfile); } closedir($dh); diff --git a/src/Worker/UpdateAllSuggestions.php b/src/Worker/UpdateAllSuggestions.php index 329fe25cfd..72614e2d30 100644 --- a/src/Worker/UpdateAllSuggestions.php +++ b/src/Worker/UpdateAllSuggestions.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/UpdateBlockedServers.php b/src/Worker/UpdateBlockedServers.php index 980897939c..6ecd4dd9e5 100644 --- a/src/Worker/UpdateBlockedServers.php +++ b/src/Worker/UpdateBlockedServers.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\GServer; @@ -34,9 +19,9 @@ class UpdateBlockedServers */ public static function execute() { - Logger::info('Update blocked servers - start'); - $gservers = DBA::select('gserver', ['id', 'url', 'blocked']); - $changed = 0; + DI::logger()->info('Update blocked servers - start'); + $gservers = DBA::select('gserver', ['id', 'url', 'blocked']); + $changed = 0; $unchanged = 0; while ($gserver = DBA::fetch($gservers)) { $blocked = Network::isUrlBlocked($gserver['url']); @@ -53,12 +38,12 @@ class UpdateBlockedServers $changed++; } DBA::close($gservers); - Logger::info('Update blocked servers - done', ['changed' => $changed, 'unchanged' => $unchanged]); + DI::logger()->info('Update blocked servers - done', ['changed' => $changed, 'unchanged' => $unchanged]); if (DI::config()->get('system', 'delete-blocked-servers')) { - Logger::info('Delete blocked servers - start'); + DI::logger()->info('Delete blocked servers - start'); $ret = DBA::delete('gserver', ["`blocked` AND NOT EXISTS(SELECT `gsid` FROM `inbox-status` WHERE `gsid` = `gserver`.`id`) AND NOT EXISTS(SELECT `gsid` FROM `contact` WHERE gsid= `gserver`.`id`) AND NOT EXISTS(SELECT `gsid` FROM `apcontact` WHERE `gsid` = `gserver`.`id`) AND NOT EXISTS(SELECT `gsid` FROM `delivery-queue` WHERE `gsid` = `gserver`.`id`) AND NOT EXISTS(SELECT `gsid` FROM `diaspora-contact` WHERE `gsid` = `gserver`.`id`) AND NOT EXISTS(SELECT `gserver-id` FROM `gserver-tag` WHERE `gserver-id` = `gserver`.`id`)"]); - Logger::info('Delete blocked servers - done', ['ret' => $ret, 'rows' => DBA::affectedRows()]); + DI::logger()->info('Delete blocked servers - done', ['ret' => $ret, 'rows' => DBA::affectedRows()]); } } } diff --git a/src/Worker/UpdateContact.php b/src/Worker/UpdateContact.php index 23eb5ff4ba..6928b0c2f2 100644 --- a/src/Worker/UpdateContact.php +++ b/src/Worker/UpdateContact.php @@ -1,30 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; +use Friendica\Core\Protocol; use Friendica\Core\Worker; +use Friendica\DI; use Friendica\Model\Contact; use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Util\DateTimeFormat; class UpdateContact { @@ -45,7 +33,7 @@ class UpdateContact $success = Contact::updateFromProbe($contact_id); - Logger::info('Updated from probe', ['id' => $contact_id, 'success' => $success]); + DI::logger()->info('Updated from probe', ['id' => $contact_id, 'success' => $success]); } /** @@ -65,6 +53,41 @@ class UpdateContact return 0; } + DI::logger()->debug('Update contact', ['id' => $contact_id]); return Worker::add($run_parameters, 'UpdateContact', $contact_id); } + + public static function isUpdatable(int $contact_id): bool + { + $contact = Contact::selectFirst(['next-update', 'local-data', 'url', 'network', 'uid'], ['id' => $contact_id]); + if (empty($contact)) { + return false; + } + + if ($contact['next-update'] > DateTimeFormat::utcNow()) { + return false; + } + + if (DI::config()->get('system', 'update_known_contacts') && ($contact['uid'] == 0) && !Contact::hasRelations($contact_id)) { + DI::logger()->debug('No local relations, contact will not be updated', ['id' => $contact_id, 'url' => $contact['url'], 'network' => $contact['network']]); + return false; + } + + if (DI::config()->get('system', 'update_active_contacts') && $contact['local-data']) { + DI::logger()->debug('No local data, contact will not be updated', ['id' => $contact_id, 'url' => $contact['url'], 'network' => $contact['network']]); + return false; + } + + if (Contact::isLocal($contact['url'])) { + DI::logger()->debug('Local contact will not be updated', ['id' => $contact_id, 'url' => $contact['url'], 'network' => $contact['network']]); + return false; + } + + if (!Protocol::supportsProbe($contact['network'])) { + DI::logger()->debug('Contact does not support probe, it will not be updated', ['id' => $contact_id, 'url' => $contact['url'], 'network' => $contact['network']]); + return false; + } + + return true; + } } diff --git a/src/Worker/UpdateContactBirthdays.php b/src/Worker/UpdateContactBirthdays.php index 485ecd7a36..cc82eb8e0d 100644 --- a/src/Worker/UpdateContactBirthdays.php +++ b/src/Worker/UpdateContactBirthdays.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/UpdateContacts.php b/src/Worker/UpdateContacts.php index 6fb71eec38..d73ea683c7 100644 --- a/src/Worker/UpdateContacts.php +++ b/src/Worker/UpdateContacts.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; @@ -42,13 +27,13 @@ class UpdateContacts } $updating = Worker::countWorkersByCommand('UpdateContact'); - $limit = $update_limit - $updating; + $limit = $update_limit - $updating; if ($limit <= 0) { - Logger::info('The number of currently running jobs exceed the limit'); + DI::logger()->info('The number of currently running jobs exceed the limit'); return; } - Logger::info('Updating contact', ['count' => $limit]); + DI::logger()->info('Updating contact', ['count' => $limit]); $condition = ['self' => false]; @@ -57,8 +42,8 @@ class UpdateContacts } $condition = DBA::mergeConditions(["`next-update` < ?", DateTimeFormat::utcNow()], $condition); - $contacts = DBA::select('contact', ['id', 'url', 'gsid', 'baseurl'], $condition, ['order' => ['next-update'], 'limit' => $limit]); - $count = 0; + $contacts = DBA::select('contact', ['id', 'url', 'gsid', 'baseurl'], $condition, ['order' => ['next-update'], 'limit' => $limit]); + $count = 0; while ($contact = DBA::fetch($contacts)) { if (Contact::isLocal($contact['url'])) { continue; @@ -66,22 +51,22 @@ class UpdateContacts try { if ((!empty($contact['gsid']) || !empty($contact['baseurl'])) && GServer::reachable($contact)) { - $stamp = (float)microtime(true); + $stamp = (float)microtime(true); $success = Contact::updateFromProbe($contact['id']); - Logger::debug('Direct update', ['id' => $contact['id'], 'count' => $count, 'duration' => round((float)microtime(true) - $stamp, 3), 'success' => $success]); + DI::logger()->debug('Direct update', ['id' => $contact['id'], 'count' => $count, 'duration' => round((float)microtime(true) - $stamp, 3), 'success' => $success]); ++$count; } elseif (UpdateContact::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], $contact['id'])) { - Logger::debug('Update by worker', ['id' => $contact['id'], 'count' => $count]); + DI::logger()->debug('Update by worker', ['id' => $contact['id'], 'count' => $count]); ++$count; } } catch (\InvalidArgumentException $e) { - Logger::notice($e->getMessage(), ['contact' => $contact]); + DI::logger()->notice($e->getMessage(), ['contact' => $contact]); } Worker::coolDown(); } DBA::close($contacts); - Logger::info('Initiated update for federated contacts', ['count' => $count]); + DI::logger()->info('Initiated update for federated contacts', ['count' => $count]); } } diff --git a/src/Worker/UpdateGServer.php b/src/Worker/UpdateGServer.php index a9f6ae4cd3..1c95631da4 100644 --- a/src/Worker/UpdateGServer.php +++ b/src/Worker/UpdateGServer.php @@ -1,35 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Model\GServer; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Util\Network; use Friendica\Util\Strings; -use GuzzleHttp\Psr7\Uri; -use Psr\Http\Message\UriInterface; class UpdateGServer { @@ -76,7 +60,7 @@ class UpdateGServer } $ret = GServer::check($filtered, '', true, $only_nodeinfo); - Logger::info('Updated gserver', ['url' => $filtered, 'result' => $ret]); + DI::logger()->info('Updated gserver', ['url' => $filtered, 'result' => $ret]); } /** diff --git a/src/Worker/UpdateGServers.php b/src/Worker/UpdateGServers.php index 5b1debaeaf..76736381db 100644 --- a/src/Worker/UpdateGServers.php +++ b/src/Worker/UpdateGServers.php @@ -1,27 +1,12 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; @@ -43,16 +28,16 @@ class UpdateGServers } $updating = Worker::countWorkersByCommand('UpdateGServer'); - $limit = $update_limit - $updating; + $limit = $update_limit - $updating; if ($limit <= 0) { - Logger::info('The number of currently running jobs exceed the limit'); + DI::logger()->info('The number of currently running jobs exceed the limit'); return; } - $total = DBA::count('gserver'); + $total = DBA::count('gserver'); $condition = ["NOT `blocked` AND `next_contact` < ? AND (`nurl` != ? OR `url` != ?)", DateTimeFormat::utcNow(), '', '']; - $outdated = DBA::count('gserver', $condition); - Logger::info('Server status', ['total' => $total, 'outdated' => $outdated, 'updating' => $limit]); + $outdated = DBA::count('gserver', $condition); + DI::logger()->info('Server status', ['total' => $total, 'outdated' => $outdated, 'updating' => $limit]); $gservers = DBA::select('gserver', ['id', 'url', 'nurl', 'failed', 'created', 'last_contact'], $condition, ['limit' => $limit]); if (!DBA::isResult($gservers)) { @@ -63,7 +48,7 @@ class UpdateGServers while ($gserver = DBA::fetch($gservers)) { if (DI::config()->get('system', 'update_active_contacts') && !Contact::exists(['gsid' => $gserver['id'], 'local-data' => true])) { $next_update = GServer::getNextUpdateDate(!$gserver['failed'], $gserver['created'], $gserver['last_contact']); - Logger::debug('Skip server without contacts with local data', ['url' => $gserver['url'], 'failed' => $gserver['failed'], 'next_update' => $next_update]); + DI::logger()->debug('Skip server without contacts with local data', ['url' => $gserver['url'], 'failed' => $gserver['failed'], 'next_update' => $next_update]); GServer::update(['next_contact' => $next_update], ['nurl' => $gserver['nurl']]); continue; } @@ -84,6 +69,6 @@ class UpdateGServers Worker::coolDown(); } DBA::close($gservers); - Logger::info('Updated servers', ['count' => $count]); + DI::logger()->info('Updated servers', ['count' => $count]); } } diff --git a/src/Worker/UpdatePhotoAlbums.php b/src/Worker/UpdatePhotoAlbums.php index a0e3bca027..eb8de1064d 100644 --- a/src/Worker/UpdatePhotoAlbums.php +++ b/src/Worker/UpdatePhotoAlbums.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/UpdateScores.php b/src/Worker/UpdateScores.php index f17027a09d..c2086e8cba 100644 --- a/src/Worker/UpdateScores.php +++ b/src/Worker/UpdateScores.php @@ -1,39 +1,25 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Database\DBA; +use Friendica\DI; use Friendica\Model\Contact\Relation; use Friendica\Model\Post; /** - * Update the interaction scores + * Update the interaction scores */ class UpdateScores { public static function execute($param = '', $hook_function = '') { - Logger::notice('Start score update'); + DI::logger()->notice('Start score update'); $users = DBA::select('user', ['uid'], ["`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` AND `uid` > ?", 0]); while ($user = DBA::fetch($users)) { @@ -41,7 +27,7 @@ class UpdateScores } DBA::close($users); - Logger::notice('Score update done'); + DI::logger()->notice('Score update done'); Post\Engagement::expire(); diff --git a/src/Worker/UpdateServerDirectories.php b/src/Worker/UpdateServerDirectories.php index a12c0b77b3..0f37a691d4 100644 --- a/src/Worker/UpdateServerDirectories.php +++ b/src/Worker/UpdateServerDirectories.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/src/Worker/UpdateServerDirectory.php b/src/Worker/UpdateServerDirectory.php index aacf4b8dc4..e9482124e8 100644 --- a/src/Worker/UpdateServerDirectory.php +++ b/src/Worker/UpdateServerDirectory.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\GServer; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; class UpdateServerDirectory { @@ -49,19 +35,19 @@ class UpdateServerDirectory private static function discoverPoCo(array $gserver) { - $result = DI::httpClient()->fetch($gserver['poco'] . '?fields=urls', HttpClientAccept::JSON); + $result = DI::httpClient()->fetch($gserver['poco'] . '?fields=urls', HttpClientAccept::JSON, 0, '', HttpClientRequest::SERVERDISCOVER); if (empty($result)) { - Logger::info('Empty result', ['url' => $gserver['url']]); + DI::logger()->info('Empty result', ['url' => $gserver['url']]); return; } $contacts = json_decode($result, true); if (empty($contacts['entry'])) { - Logger::info('No contacts', ['url' => $gserver['url']]); + DI::logger()->info('No contacts', ['url' => $gserver['url']]); return; } - Logger::info('PoCo discovery started', ['poco' => $gserver['poco']]); + DI::logger()->info('PoCo discovery started', ['poco' => $gserver['poco']]); $urls = []; foreach (array_column($contacts['entry'], 'urls') as $url_entries) { @@ -77,24 +63,24 @@ class UpdateServerDirectory $result = Contact::addByUrls($urls); - Logger::info('PoCo discovery ended', ['count' => $result['count'], 'added' => $result['added'], 'updated' => $result['updated'], 'unchanged' => $result['unchanged'], 'poco' => $gserver['poco']]); + DI::logger()->info('PoCo discovery ended', ['count' => $result['count'], 'added' => $result['added'], 'updated' => $result['updated'], 'unchanged' => $result['unchanged'], 'poco' => $gserver['poco']]); } private static function discoverMastodonDirectory(array $gserver) { - $result = DI::httpClient()->fetch($gserver['url'] . '/api/v1/directory?order=new&local=true&limit=200&offset=0', HttpClientAccept::JSON); + $result = DI::httpClient()->fetch($gserver['url'] . '/api/v1/directory?order=new&local=true&limit=200&offset=0', HttpClientAccept::JSON, 0, '', HttpClientRequest::SERVERDISCOVER); if (empty($result)) { - Logger::info('Empty result', ['url' => $gserver['url']]); + DI::logger()->info('Empty result', ['url' => $gserver['url']]); return; } $accounts = json_decode($result, true); - if (empty($accounts)) { - Logger::info('No contacts', ['url' => $gserver['url']]); + if (!is_array($accounts)) { + DI::logger()->info('No contacts', ['url' => $gserver['url']]); return; } - Logger::info('Account discovery started', ['url' => $gserver['url']]); + DI::logger()->info('Account discovery started', ['url' => $gserver['url']]); $urls = []; foreach ($accounts as $account) { @@ -105,6 +91,6 @@ class UpdateServerDirectory $result = Contact::addByUrls($urls); - Logger::info('Account discovery ended', ['count' => $result['count'], 'added' => $result['added'], 'updated' => $result['updated'], 'unchanged' => $result['unchanged'], 'url' => $gserver['url']]); + DI::logger()->info('Account discovery ended', ['count' => $result['count'], 'added' => $result['added'], 'updated' => $result['updated'], 'unchanged' => $result['unchanged'], 'url' => $gserver['url']]); } } diff --git a/src/Worker/UpdateServerPeers.php b/src/Worker/UpdateServerPeers.php index aa5f35c0fc..4d3846b643 100644 --- a/src/Worker/UpdateServerPeers.php +++ b/src/Worker/UpdateServerPeers.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; -use Friendica\Core\Logger; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\GServer; use Friendica\Network\HTTPClient\Client\HttpClientAccept; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientRequest; use Friendica\Util\Network; use Friendica\Util\Strings; @@ -35,7 +22,7 @@ class UpdateServerPeers /** * Query the given server for their known peers * - * @param string $gserver Server URL + * @param string $url Server URL * @return void */ public static function execute(string $url) @@ -44,19 +31,24 @@ class UpdateServerPeers return; } - $ret = DI::httpClient()->get($url . '/api/v1/instance/peers', HttpClientAccept::JSON); - if (!$ret->isSuccess() || empty($ret->getBody())) { - Logger::info('Server is not reachable or does not offer the "peers" endpoint', ['url' => $url]); + try { + $ret = DI::httpClient()->get($url . '/api/v1/instance/peers', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERDISCOVER]); + } catch (\Throwable $th) { + DI::logger()->notice('Got exception', ['code' => $th->getCode(), 'message' => $th->getMessage()]); + return; + } + if (!$ret->isSuccess() || empty($ret->getBodyString())) { + DI::logger()->info('Server is not reachable or does not offer the "peers" endpoint', ['url' => $url]); return; } - $peers = json_decode($ret->getBody()); + $peers = json_decode($ret->getBodyString()); if (empty($peers) || !is_array($peers)) { - Logger::info('Server does not have any peers listed', ['url' => $url]); + DI::logger()->info('Server does not have any peers listed', ['url' => $url]); return; } - Logger::info('Server peer update start', ['url' => $url]); + DI::logger()->info('Server peer update start', ['url' => $url]); $total = 0; $added = 0; @@ -76,6 +68,6 @@ class UpdateServerPeers ++$added; Worker::coolDown(); } - Logger::info('Server peer update ended', ['total' => $total, 'added' => $added, 'url' => $url]); + DI::logger()->info('Server peer update ended', ['total' => $total, 'added' => $added, 'url' => $url]); } } diff --git a/src/Worker/UpdateSuggestions.php b/src/Worker/UpdateSuggestions.php index f7b4b9a0ac..6e6a25e397 100644 --- a/src/Worker/UpdateSuggestions.php +++ b/src/Worker/UpdateSuggestions.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Worker; diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 64c9bdff1f..183b2281d3 100644 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Main database structure configuration file. * @@ -56,7 +44,7 @@ use Friendica\Database\DBA; // This file is required several times during the test in DbaDefinition which justifies this condition if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1542); + define('DB_UPDATE_VERSION', 1580); } return [ @@ -67,7 +55,7 @@ return [ "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], "url" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], "nurl" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "version" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], + "version" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "The version of this server software."], "site_name" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "info" => ["type" => "text", "comment" => ""], "register_policy" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => ""], @@ -79,10 +67,14 @@ return [ "local-comments" => ["type" => "int unsigned", "comment" => "Number of local comments"], "directory-type" => ["type" => "tinyint", "default" => "0", "comment" => "Type of directory service (Poco, Mastodon)"], "poco" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], + "openwebauth" => ["type" => "varbinary(383)", "comment" => "Path to the OpenWebAuth endpoint"], + "authredirect" => ["type" => "varbinary(383)", "comment" => "Path to the authRedirect endpoint"], "noscrape" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], "network" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => ""], "protocol" => ["type" => "tinyint unsigned", "comment" => "The protocol of the server"], - "platform" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], + "platform" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "The canonical name of this server software."], + "repository" => ["type" => "varbinary(383)", "comment" => "The url of the source code repository of this server software."], + "homepage" => ["type" => "varbinary(383)", "comment" => "The url of the homepage of this server software."], "relay-subscribe" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Has the server subscribed to the relay system"], "relay-scope" => ["type" => "varchar(10)", "not null" => "1", "default" => "", "comment" => "The scope of messages that the server wants to get"], "detection-method" => ["type" => "tinyint unsigned", "comment" => "Method that had been used to detect that server"], @@ -93,12 +85,14 @@ return [ "blocked" => ["type" => "boolean", "comment" => "Server is blocked"], "failed" => ["type" => "boolean", "comment" => "Connection failed"], "next_contact" => ["type" => "datetime", "default" => DBA::NULL_DATETIME, "comment" => "Next connection request"], + "redirect-gsid" => ["type" => "int unsigned", "foreign" => ["gserver" => "id"], "comment" => "Target Gserver id in case of a redirect"], ], "indexes" => [ "PRIMARY" => ["id"], "nurl" => ["UNIQUE", "nurl(190)"], "next_contact" => ["next_contact"], "network" => ["network"], + "redirect-gsid" => ["redirect-gsid"], ] ], "user" => [ @@ -123,15 +117,11 @@ return [ "theme" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "user theme preference"], "pubkey" => ["type" => "text", "comment" => "RSA public key 4096 bit"], "prvkey" => ["type" => "text", "comment" => "RSA private key 4096 bit"], - "spubkey" => ["type" => "text", "comment" => ""], - "sprvkey" => ["type" => "text", "comment" => ""], "verified" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "user is verified through email"], "blocked" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "1 for user is blocked"], "blockwall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Prohibit contacts to post to the profile page of the user"], "hidewall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Hide profile details from unknown viewers"], "blocktags" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Prohibit contacts to tag the post of this user"], - "unkmail" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Permit unknown people to send private mails to this user"], - "cntunkmail" => ["type" => "int unsigned", "not null" => "1", "default" => "10", "comment" => ""], "notify-flags" => ["type" => "smallint unsigned", "not null" => "1", "default" => "65535", "comment" => "email notification options"], "page-flags" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "page/profile type"], "account-type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], @@ -239,7 +229,6 @@ return [ "remote_self" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "rel" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "The kind of the relation between the user and the contact"], "protocol" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => "Protocol of the contact"], - "subhub" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "hub-verify" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], "rating" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Automatically detected feed poll frequency"], "priority" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Feed poll priority"], @@ -430,6 +419,7 @@ return [ "manually-approve" => ["type" => "boolean", "comment" => ""], "discoverable" => ["type" => "boolean", "comment" => "Mastodon extension: true if profile is published in their directory"], "suspended" => ["type" => "boolean", "comment" => "Mastodon extension: true if profile is suspended"], + "posting-restricted" => ["type" => "boolean", "comment" => "lemmy:postingRestrictedToMods"], "nick" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], "name" => ["type" => "varchar(255)", "comment" => ""], "about" => ["type" => "text", "comment" => ""], @@ -562,8 +552,13 @@ return [ "access-key" => ["type" => "varchar(1)", "comment" => "Access key"], "include-tags" => ["type" => "varchar(1023)", "comment" => "Comma separated list of tags that will be included in the channel"], "exclude-tags" => ["type" => "varchar(1023)", "comment" => "Comma separated list of tags that aren't allowed in the channel"], + "min-size" => ["type" => "int unsigned", "comment" => "Minimum post size"], + "max-size" => ["type" => "int unsigned", "comment" => "Maximum post size"], "full-text-search" => ["type" => "varchar(1023)", "comment" => "Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode"], "media-type" => ["type" => "smallint unsigned", "comment" => "Filtered media types"], + "languages" => ["type" => "mediumtext", "comment" => "Desired languages"], + "publish" => ["type" => "boolean", "comment" => "publish channel content"], + "valid" => ["type" => "boolean", "comment" => "Set, when the full-text-search is valid"], ], "indexes" => [ "PRIMARY" => ["id"], @@ -595,6 +590,7 @@ return [ "relation-score" => ["type" => "smallint unsigned", "comment" => "score for interactions of relation-cid on cid"], "thread-score" => ["type" => "smallint unsigned", "comment" => "score for interactions of cid on threads of relation-cid"], "relation-thread-score" => ["type" => "smallint unsigned", "comment" => "score for interactions of relation-cid on threads of cid"], + "post-score" => ["type" => "smallint unsigned", "comment" => "score for the amount of posts from cid that can be seen by relation-cid"], ], "indexes" => [ "PRIMARY" => ["cid", "relation-cid"], @@ -859,6 +855,7 @@ return [ "activity-id" => ["type" => "varbinary(383)", "comment" => "id of the incoming activity"], "object-id" => ["type" => "varbinary(383)", "comment" => ""], "in-reply-to-id" => ["type" => "varbinary(383)", "comment" => ""], + "context" => ["type" => "varbinary(383)", "comment" => ""], "conversation" => ["type" => "varbinary(383)", "comment" => ""], "type" => ["type" => "varchar(64)", "comment" => "Type of the activity"], "object-type" => ["type" => "varchar(64)", "comment" => "Type of the object activity"], @@ -869,6 +866,7 @@ return [ "push" => ["type" => "boolean", "comment" => "Is the entry pushed or have pulled it?"], "trust" => ["type" => "boolean", "comment" => "Do we trust this entry?"], "wid" => ["type" => "int unsigned", "foreign" => ["workerqueue" => "id"], "comment" => "Workerqueue id"], + "retrial" => ["type" => "tinyint unsigned", "default" => "0", "comment" => "Retrial counter"], ], "indexes" => [ "PRIMARY" => ["id"], @@ -1213,6 +1211,7 @@ return [ "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the parent uri"], "thr-parent-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the thread parent uri"], "external-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the external uri"], + "replies-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the endpoint for the replies collection"], "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Creation timestamp."], "edited" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last edit (default is created)"], "received" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "datetime"], @@ -1233,6 +1232,7 @@ return [ "parent-uri-id" => ["parent-uri-id"], "thr-parent-id" => ["thr-parent-id"], "external-id" => ["external-id"], + "replies-id" => ["replies-id"], "owner-id" => ["owner-id"], "author-id" => ["author-id"], "causer-id" => ["causer-id"], @@ -1242,7 +1242,7 @@ return [ "post-activity" => [ "comment" => "Original remote activity", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "activity" => ["type" => "mediumtext", "comment" => "Original activity"], "received" => ["type" => "datetime", "comment" => ""], ], @@ -1253,7 +1253,7 @@ return [ "post-category" => [ "comment" => "post relation to categories", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["user" => "uid"], "comment" => "User id"], "type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "primary" => "1", "comment" => ""], "tid" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "primary" => "1", "foreign" => ["tag" => "id", "on delete" => "restrict"], "comment" => ""], @@ -1264,6 +1264,21 @@ return [ "uid_uri-id" => ["uid", "uri-id"], ] ], + "post-counts" => [ + "comment" => "Original remote activity", + "fields" => [ + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "vid" => ["type" => "smallint unsigned", "not null" => "1", "primary" => "1", "foreign" => ["verb" => "id", "on delete" => "restrict"], "comment" => "Id of the verb table entry that contains the activity verbs"], + "reaction" => ["type" => "varchar(4)", "not null" => "1", "primary" => "1", "comment" => "Emoji Reaction"], + "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the parent uri"], + "count" => ["type" => "int unsigned", "default" => 0, "comment" => "Number of activities"], + ], + "indexes" => [ + "PRIMARY" => ["uri-id", "vid", "reaction"], + "vid" => ["vid"], + "parent-uri-id" => ["parent-uri-id"], + ] + ], "post-collection" => [ "comment" => "Collection of posts", "fields" => [ @@ -1289,6 +1304,7 @@ return [ "location" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "text location where this item originated"], "coord" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "longitude/latitude pair representing location where this item originated"], "language" => ["type" => "text", "comment" => "Language information about this post"], + "sensitive" => ["type" => "boolean", "comment" => "If true, this post contains sensitive content"], "app" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "application which generated this item"], "rendered-hash" => ["type" => "varchar(32)", "not null" => "1", "default" => "", "comment" => ""], "rendered-html" => ["type" => "mediumtext", "comment" => "item.body converted to html"], @@ -1303,7 +1319,6 @@ return [ "PRIMARY" => ["uri-id"], "plink" => ["plink(191)"], "resource-id" => ["resource-id"], - "title-content-warning-body" => ["FULLTEXT", "title", "content-warning", "body"], "quote-uri-id" => ["quote-uri-id"], ] ], @@ -1337,7 +1352,6 @@ return [ "dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via DFRN"], "legacy_dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via legacy DFRN"], "diaspora" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via Diaspora"], - "ostatus" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via OStatus"], ], "indexes" => [ "PRIMARY" => ["uri-id"], @@ -1346,13 +1360,15 @@ return [ "post-engagement" => [ "comment" => "Engagement data per post", "fields" => [ - "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "owner-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => "Item owner"], "contact-type" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Person, organisation, news, community, relay"], - "media-type" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Type of media in a bit array (1 = image, 2 = video, 4 = audio"], - "language" => ["type" => "varbinary(128)", "comment" => "Language information about this post"], + "media-type" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Type of media in a bit array (1 = image, 2 = video, 4 = audio)"], + "language" => ["type" => "char(2)", "comment" => "Language information about this post in the ISO 639-1 format"], "searchtext" => ["type" => "mediumtext", "comment" => "Simplified text for the full text search"], + "size" => ["type" => "int unsigned", "comment" => "Body size"], "created" => ["type" => "datetime", "comment" => ""], + "network" => ["type" => "char(4)", "comment" => ""], "restricted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "If true, this post is either unlisted or not from a federated network"], "comments" => ["type" => "mediumint unsigned", "comment" => "Number of comments"], "activities" => ["type" => "mediumint unsigned", "comment" => "Number of activities (like, dislike, ...)"], @@ -1413,6 +1429,7 @@ return [ "uri-id" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], "url" => ["type" => "varbinary(1024)", "not null" => "1", "comment" => "Media URL"], "media-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the activities uri-id"], + "attach-id" => ["type" => "int unsigned", "foreign" => ["attach" => "id"], "comment" => "In case of a local attachment, this field is filled with the id in the attach table"], "type" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Media type"], "mimetype" => ["type" => "varchar(60)", "comment" => ""], "height" => ["type" => "smallint unsigned", "comment" => "Height of the media"], @@ -1430,14 +1447,44 @@ return [ "publisher-url" => ["type" => "varbinary(383)", "comment" => "URL of the publisher of the media"], "publisher-name" => ["type" => "varchar(255)", "comment" => "Name of the publisher of the media"], "publisher-image" => ["type" => "varbinary(383)", "comment" => "Image of the publisher of the media"], + "language" => ["type" => "char(3)", "comment" => "Language information about this media in the ISO 639 format"], + "published" => ["type" => "datetime", "comment" => "Publification date of this media"], + "modified" => ["type" => "datetime", "comment" => "Modification date of this media"], ], "indexes" => [ "PRIMARY" => ["id"], "uri-id-url" => ["UNIQUE", "uri-id", "url(512)"], "uri-id-id" => ["uri-id", "id"], "media-uri-id" => ["media-uri-id"], + "attach-id" => ["attach-id"], ] ], + "post-origin" => [ + "comment" => "Posts from local users", + "fields" => [ + "id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1"], + "uri-id" => ["type" => "int unsigned", "not null" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "Owner id which owns this copy of the item"], + "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the parent uri"], + "thr-parent-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the thread parent uri"], + "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Creation timestamp."], + "received" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "datetime"], + "gravity" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], + "vid" => ["type" => "smallint unsigned", "foreign" => ["verb" => "id", "on delete" => "restrict"], "comment" => "Id of the verb table entry that contains the activity verbs"], + "private" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "0=public, 1=private, 2=unlisted"], + "wall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "This item was posted to the wall of uid"], + ], + "indexes" => [ + "PRIMARY" => ["id"], + "uid_uri-id" => ["UNIQUE", "uid", "uri-id"], + "uri-id" => ["uri-id"], + "parent-uri-id" => ["parent-uri-id"], + "thr-parent-id" => ["thr-parent-id"], + "vid" => ["vid"], + "parent-uri-id_uid" => ["parent-uri-id", "uid"], + "uid_wall_received" => ["uid", "wall", "received"], + ], + ], "post-question" => [ "comment" => "Question", "fields" => [ @@ -1464,6 +1511,25 @@ return [ "PRIMARY" => ["uri-id", "id"], ] ], + "post-searchindex" => [ + "comment" => "Content for all posts", + "fields" => [ + "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "owner-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id"], "comment" => "Item owner"], + "media-type" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Type of media in a bit array (1 = image, 2 = video, 4 = audio)"], + "language" => ["type" => "char(2)", "comment" => "Language information about this post in the ISO 639-1 format"], + "searchtext" => ["type" => "mediumtext", "comment" => "Simplified text for the full text search"], + "size" => ["type" => "int unsigned", "comment" => "Body size"], + "created" => ["type" => "datetime", "comment" => ""], + "restricted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "If true, this post is either unlisted or not from a federated network"], + ], + "indexes" => [ + "PRIMARY" => ["uri-id"], + "owner-id" => ["owner-id"], + "created" => ["created"], + "searchtext" => ["FULLTEXT", "searchtext"], + ] + ], "post-tag" => [ "comment" => "post relation to tags", "fields" => [ @@ -1482,6 +1548,7 @@ return [ "comment" => "Thread related data", "fields" => [ "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "context-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the endpoint for the context collection"], "conversation-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the conversation uri"], "owner-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id", "on delete" => "restrict"], "comment" => "Item owner"], "author-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id", "on delete" => "restrict"], "comment" => "Item author"], @@ -1494,6 +1561,7 @@ return [ ], "indexes" => [ "PRIMARY" => ["uri-id"], + "context-id" => ["context-id"], "conversation-id" => ["conversation-id"], "owner-id" => ["owner-id"], "author-id" => ["author-id"], @@ -1510,6 +1578,7 @@ return [ "parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the parent uri"], "thr-parent-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table that contains the thread parent uri"], "external-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the external uri"], + "replies-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the endpoint for the replies collection"], "created" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Creation timestamp."], "edited" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last edit (default is created)"], "received" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "datetime"], @@ -1522,6 +1591,7 @@ return [ "post-reason" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Reason why the post arrived at the user"], "vid" => ["type" => "smallint unsigned", "foreign" => ["verb" => "id", "on delete" => "restrict"], "comment" => "Id of the verb table entry that contains the activity verbs"], "private" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "0=public, 1=private, 2=unlisted"], + "restrictions" => ["type" => "tinyint unsigned", "comment" => "Bit array of post restrictions (1 = Reply, 2 = Like, 4 = Announce)"], "global" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "visible" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""], "deleted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "item has been marked for deletion"], @@ -1539,10 +1609,11 @@ return [ "indexes" => [ "PRIMARY" => ["id"], "uid_uri-id" => ["UNIQUE", "uid", "uri-id"], - "uri-id" => ["uri-id"], + "uri-id_origin_deleted" => ["uri-id", "origin", "deleted"], "parent-uri-id" => ["parent-uri-id"], "thr-parent-id" => ["thr-parent-id"], "external-id" => ["external-id"], + "replies-id" => ["replies-id"], "owner-id" => ["owner-id"], "author-id" => ["author-id"], "causer-id" => ["causer-id"], @@ -1565,6 +1636,7 @@ return [ "comment" => "Thread related data per user", "fields" => [ "uri-id" => ["type" => "int unsigned", "not null" => "1", "primary" => "1", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the item uri"], + "context-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the endpoint for the context collection"], "conversation-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Id of the item-uri table entry that contains the conversation uri"], "owner-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id", "on delete" => "restrict"], "comment" => "Item owner"], "author-id" => ["type" => "int unsigned", "not null" => "1", "default" => "0", "foreign" => ["contact" => "id", "on delete" => "restrict"], "comment" => "Item author"], @@ -1592,6 +1664,7 @@ return [ "indexes" => [ "PRIMARY" => ["uid", "uri-id"], "uri-id" => ["uri-id"], + "context-id" => ["context-id"], "conversation-id" => ["conversation-id"], "owner-id" => ["owner-id"], "author-id" => ["author-id"], @@ -1607,7 +1680,6 @@ return [ "uid_received" => ["uid", "received"], "uid_wall_received" => ["uid", "wall", "received"], "uid_commented" => ["uid", "commented"], - "uid_received" => ["uid", "received"], "uid_created" => ["uid", "created"], "uid_starred" => ["uid", "starred"], "uid_mention" => ["uid", "mention"], @@ -1692,7 +1764,6 @@ return [ "indexes" => [ "PRIMARY" => ["id"], "uid_is-default" => ["uid", "is-default"], - "pub_keywords" => ["FULLTEXT", "pub_keywords"], ] ], "profile_field" => [ @@ -1714,26 +1785,6 @@ return [ "psid" => ["psid"], ] ], - "push_subscriber" => [ - "comment" => "Used for OStatus: Contains feed subscribers", - "fields" => [ - "id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"], - "uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "foreign" => ["user" => "uid"], "comment" => "User id"], - "callback_url" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""], - "topic" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - "nickname" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - "push" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Retrial counter"], - "last_update" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last successful trial"], - "next_try" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Next retrial date"], - "renewed" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last subscription renewal"], - "secret" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""], - ], - "indexes" => [ - "PRIMARY" => ["id"], - "next_try" => ["next_try"], - "uid" => ["uid"] - ] - ], "register" => [ "comment" => "registrations requiring admin approval", "fields" => [ @@ -1898,6 +1949,7 @@ return [ "ignored" => ["type" => "boolean", "comment" => "Posts from this contact are ignored"], "collapsed" => ["type" => "boolean", "comment" => "Posts from this contact are collapsed"], "hidden" => ["type" => "boolean", "comment" => "This contact is hidden from the others"], + "channel-only" => ["type" => "boolean", "comment" => "This contact is displayed only in channels, but not in the network stream."], "is-blocked" => ["type" => "boolean", "comment" => "User is blocked by this contact"], "channel-frequency" => ["type" => "tinyint unsigned", "comment" => "Controls the frequency of the appearance of this contact in channels"], "pending" => ["type" => "boolean", "comment" => ""], @@ -1907,7 +1959,6 @@ return [ "remote_self" => ["type" => "tinyint unsigned", "comment" => "0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare"], "fetch_further_information" => ["type" => "tinyint unsigned", "comment" => "0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both"], "ffi_keyword_denylist" => ["type" => "text", "comment" => ""], - "subhub" => ["type" => "boolean", "comment" => ""], "hub-verify" => ["type" => "varbinary(383)", "comment" => ""], "protocol" => ["type" => "char(4)", "comment" => "Protocol of the contact"], "rating" => ["type" => "tinyint", "comment" => "Automatically detected feed poll frequency"], diff --git a/static/dbview.config.php b/static/dbview.config.php index b4d10de83c..3437dcb22d 100644 --- a/static/dbview.config.php +++ b/static/dbview.config.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Main view structure configuration file. * @@ -36,7 +24,7 @@ * */ - return [ +return [ "application-view" => [ "fields" => [ "id" => ["application", "id"], @@ -56,7 +44,8 @@ "push" => ["application-token", "push"], ], "query" => "FROM `application-token` - INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`" + INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id` + INNER JOIN `user` ON `user`.`uid` = `application-token`.`uid` AND `user`.`verified` AND NOT `user`.`blocked` AND NOT `user`.`account_removed` AND NOT `user`.`account_expired`" ], "circle-member-view" => [ "fields" => [ @@ -87,6 +76,49 @@ INNER JOIN `contact` ON `group_member`.`contact-id` = `contact`.`id` INNER JOIN `group` ON `group_member`.`gid` = `group`.`id`" ], + "post-counts-view" => [ + "fields" => [ + "uri-id" => ["post-counts", "uri-id"], + "vid" => ["post-counts", "vid"], + "verb" => ["verb", "name"], + "reaction" => ["post-counts", "reaction"], + "parent-uri-id" => ["post-counts", "parent-uri-id"], + "count" => ["post-counts", "count"], + ], + "query" => "FROM `post-counts` + INNER JOIN `verb` ON `verb`.`id` = `post-counts`.`vid`" + ], + "post-engagement-user-view" => [ + "fields" => [ + "uid" => ["post-thread-user", "uid"], + "uri-id" => ["post-engagement", "uri-id"], + "owner-id" => ["post-engagement", "owner-id"], + "media-type" => ["post-engagement", "media-type"], + "language" => ["post-engagement", "language"], + "searchtext" => ["post-engagement", "searchtext"], + "size" => ["post-engagement", "size"], + "commented" => ["post-thread-user", "commented"], + "received" => ["post-thread-user", "received"], + "created" => ["post-thread-user", "created"], + "network" => ["post-thread-user", "network"], + "protocol" => ["post-user", "protocol"], + "restricted" => ["post-engagement", "language"], + "comments" => "0", + "activities" => "0", + ], + "query" => "FROM `post-thread-user` + INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `post-thread-user`.`uri-id` + INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` + WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` + AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) + AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) + AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored` OR `is-blocked`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`)" + ], "post-timeline-view" => [ "fields" => [ "uid" => ["post-user", "uid"], @@ -122,6 +154,9 @@ "causer-id" => ["post-user", "causer-id"], "causer-blocked" => ["causer", "blocked"], "causer-gsid" => ["causer", "gsid"], + "parent-network" => ["post-thread-user", "network"], + "parent-owner-id" => ["post-thread-user", "owner-id"], + "parent-author-id" => ["post-thread-user", "author-id"], ], "query" => "FROM `post-user` LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` @@ -130,50 +165,129 @@ STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`" ], - "post-user-view" => [ + "post-timeline-origin-view" => [ "fields" => [ - "id" => ["post-user", "id"], - "post-user-id" => ["post-user", "id"], - "uid" => ["post-user", "uid"], + "uid" => ["post-origin", "uid"], + "uri-id" => ["post-origin", "uri-id"], + "gravity" => ["post-origin", "gravity"], + "created" => ["post-origin", "created"], + "edited" => ["post-user", "edited"], + "commented" => ["post-thread-user", "commented"], + "received" => ["post-origin", "received"], + "changed" => ["post-thread-user", "changed"], + "private" => ["post-origin", "private"], + "visible" => ["post-user", "visible"], + "deleted" => ["post-user", "deleted"], + "origin" => "true", + "global" => ["post-user", "global"], + "network" => ["post-user", "network"], + "protocol" => ["post-user", "protocol"], + "vid" => ["post-origin", "vid"], + "contact-id" => ["post-user", "contact-id"], + "contact-blocked" => ["contact", "blocked"], + "contact-readonly" => ["contact", "readonly"], + "contact-pending" => ["contact", "pending"], + "contact-rel" => ["contact", "rel"], + "contact-uid" => ["contact", "uid"], + "self" => ["contact", "self"], + "author-id" => ["post-user", "author-id"], + "author-blocked" => ["author", "blocked"], + "author-hidden" => ["author", "hidden"], + "author-gsid" => ["author", "gsid"], + "owner-id" => ["post-user", "owner-id"], + "owner-blocked" => ["owner", "blocked"], + "owner-gsid" => ["owner", "gsid"], + "causer-id" => ["post-user", "causer-id"], + "causer-blocked" => ["causer", "blocked"], + "causer-gsid" => ["causer", "gsid"], + ], + "query" => "FROM `post-origin` + INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` + LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` + LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`" + ], + "post-searchindex-user-view" => [ + "fields" => [ + "uid" => ["post-thread-user", "uid"], + "uri-id" => ["post-searchindex", "uri-id"], + "owner-id" => ["post-searchindex", "owner-id"], + "media-type" => ["post-searchindex", "media-type"], + "language" => ["post-searchindex", "language"], + "searchtext" => ["post-searchindex", "searchtext"], + "size" => ["post-searchindex", "size"], + "commented" => ["post-thread-user", "commented"], + "received" => ["post-thread-user", "received"], + "created" => ["post-thread-user", "created"], + "network" => ["post-thread-user", "network"], + "protocol" => ["post-user", "protocol"], + "restricted" => ["post-searchindex", "language"], + "comments" => "0", + "activities" => "0", + ], + "query" => "FROM `post-thread-user` + INNER JOIN `post-searchindex` ON `post-searchindex`.`uri-id` = `post-thread-user`.`uri-id` + INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` + WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` + AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) + AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) + AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored` OR `is-blocked`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`)" + ], + "post-origin-view" => [ + "fields" => [ + "id" => ["post-origin", "id"], + "post-user-id" => ["post-origin", "id"], + "uid" => ["post-origin", "uid"], "parent" => ["post-thread-user", "post-user-id"], "uri" => ["item-uri", "uri"], - "uri-id" => ["post-user", "uri-id"], + "uri-id" => ["post-origin", "uri-id"], "parent-uri" => ["parent-item-uri", "uri"], - "parent-uri-id" => ["post-user", "parent-uri-id"], + "parent-uri-id" => ["post-origin", "parent-uri-id"], "thr-parent" => ["thr-parent-item-uri", "uri"], - "thr-parent-id" => ["post-user", "thr-parent-id"], + "thr-parent-id" => ["post-origin", "thr-parent-id"], "conversation" => ["conversation-item-uri", "uri"], "conversation-id" => ["post-thread-user", "conversation-id"], + "context" => ["context-item-uri", "uri"], + "context-id" => ["post-thread-user", "context-id"], "quote-uri" => ["quote-item-uri", "uri"], "quote-uri-id" => ["post-content", "quote-uri-id"], "guid" => ["item-uri", "guid"], - "wall" => ["post-user", "wall"], - "gravity" => ["post-user", "gravity"], + "wall" => ["post-origin", "wall"], + "gravity" => ["post-origin", "gravity"], "extid" => ["external-item-uri", "uri"], "external-id" => ["post-user", "external-id"], - "created" => ["post-user", "created"], + "replies" => ["replies-item-uri", "uri"], + "replies-id" => ["post-user", "replies-id"], + "created" => ["post-origin", "created"], "edited" => ["post-user", "edited"], "commented" => ["post-thread-user", "commented"], - "received" => ["post-user", "received"], + "received" => ["post-origin", "received"], "changed" => ["post-thread-user", "changed"], "post-type" => ["post-user", "post-type"], "post-reason" => ["post-user", "post-reason"], - "private" => ["post-user", "private"], + "private" => ["post-origin", "private"], "pubmail" => ["post-thread-user", "pubmail"], "visible" => ["post-user", "visible"], "starred" => ["post-thread-user", "starred"], "unseen" => ["post-user", "unseen"], "deleted" => ["post-user", "deleted"], - "origin" => ["post-user", "origin"], + "origin" => "true", "parent-origin" => ["post-thread-user", "origin"], "mention" => ["post-thread-user", "mention"], "global" => ["post-user", "global"], - "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-user`.`uri-id`)", + "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-origin`.`uri-id`)", "network" => ["post-user", "network"], "protocol" => ["post-user", "protocol"], - "vid" => ["post-user", "vid"], + "vid" => ["post-origin", "vid"], "psid" => ["post-user", "psid"], - "verb" => "IF (`post-user`.`vid` IS NULL, '', `verb`.`name`)", + "verb" => "IF (`post-origin`.`vid` IS NULL, '', `verb`.`name`)", "title" => ["post-content", "title"], "content-warning" => ["post-content", "content-warning"], "raw-body" => ["post-content", "raw-body"], @@ -184,6 +298,8 @@ "plink" => ["post-content", "plink"], "location" => ["post-content", "location"], "coord" => ["post-content", "coord"], + "sensitive" => ["post-content", "sensitive"], + "restrictions" => ["post-user", "restrictions"], "app" => ["post-content", "app"], "object-type" => ["post-content", "object-type"], "object" => ["post-content", "object"], @@ -281,81 +397,90 @@ "question-multiple" => ["post-question", "multiple"], "question-voters" => ["post-question", "voters"], "question-end-time" => ["post-question", "end-time"], - "has-categories" => "EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-user`.`uri-id` AND `post-category`.`uid` = `post-user`.`uid`)", - "has-media" => "EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-user`.`uri-id`)", + "has-categories" => "EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-origin`.`uri-id` AND `post-category`.`uid` = `post-origin`.`uid`)", + "has-media" => "EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-origin`.`uri-id`)", "signed_text" => ["diaspora-interaction", "interaction"], "parent-guid" => ["parent-item-uri", "guid"], "parent-network" => ["post-thread-user", "network"], + "parent-owner-id" => ["post-thread-user", "owner-id"], "parent-author-id" => ["post-thread-user", "author-id"], "parent-author-link" => ["parent-post-author", "url"], "parent-author-name" => ["parent-post-author", "name"], "parent-author-nick" => ["parent-post-author", "nick"], "parent-author-network" => ["parent-post-author", "network"], ], - "query" => "FROM `post-user` - INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` + "query" => "FROM `post-origin` + INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` + INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id` - LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-user`.`uri-id` - LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` - LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` + LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-origin`.`uri-id` + LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-origin`.`thr-parent-id` + LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-origin`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` - LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` + LEFT JOIN `verb` ON `verb`.`id` = `post-origin`.`vid` LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` - LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-user`.`uri-id` - LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-origin`.`uri-id` LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id` - LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-user`.`uri-id` AND `post-user`.`origin` - LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-origin`.`uri-id` LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid` LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread-user`.`author-id`" ], - "post-thread-user-view" => [ + "post-thread-origin-view" => [ "fields" => [ - "id" => ["post-user", "id"], - "post-user-id" => ["post-user", "id"], - "uid" => ["post-thread-user", "uid"], + "id" => ["post-origin", "id"], + "post-user-id" => ["post-origin", "id"], + "uid" => ["post-origin", "uid"], "parent" => ["post-thread-user", "post-user-id"], "uri" => ["item-uri", "uri"], - "uri-id" => ["post-thread-user", "uri-id"], + "uri-id" => ["post-origin", "uri-id"], "parent-uri" => ["parent-item-uri", "uri"], - "parent-uri-id" => ["post-user", "parent-uri-id"], + "parent-uri-id" => ["post-origin", "parent-uri-id"], "thr-parent" => ["thr-parent-item-uri", "uri"], - "thr-parent-id" => ["post-user", "thr-parent-id"], + "thr-parent-id" => ["post-origin", "thr-parent-id"], "conversation" => ["conversation-item-uri", "uri"], "conversation-id" => ["post-thread-user", "conversation-id"], + "context" => ["context-item-uri", "uri"], + "context-id" => ["post-thread-user", "context-id"], "quote-uri" => ["quote-item-uri", "uri"], "quote-uri-id" => ["post-content", "quote-uri-id"], "guid" => ["item-uri", "guid"], - "wall" => ["post-thread-user", "wall"], - "gravity" => ["post-user", "gravity"], + "wall" => ["post-origin", "wall"], + "gravity" => ["post-origin", "gravity"], "extid" => ["external-item-uri", "uri"], "external-id" => ["post-user", "external-id"], - "created" => ["post-thread-user", "created"], + "replies" => ["replies-item-uri", "uri"], + "replies-id" => ["post-user", "replies-id"], + "created" => ["post-origin", "created"], "edited" => ["post-user", "edited"], "commented" => ["post-thread-user", "commented"], - "received" => ["post-thread-user", "received"], + "received" => ["post-origin", "received"], "changed" => ["post-thread-user", "changed"], "post-type" => ["post-user", "post-type"], "post-reason" => ["post-user", "post-reason"], - "private" => ["post-user", "private"], + "private" => ["post-origin", "private"], "pubmail" => ["post-thread-user", "pubmail"], "ignored" => ["post-thread-user", "ignored"], "visible" => ["post-user", "visible"], "starred" => ["post-thread-user", "starred"], "unseen" => ["post-thread-user", "unseen"], "deleted" => ["post-user", "deleted"], - "origin" => ["post-thread-user", "origin"], + "origin" => "true", "mention" => ["post-thread-user", "mention"], "global" => ["post-user", "global"], "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread-user`.`uri-id`)", "network" => ["post-thread-user", "network"], - "vid" => ["post-user", "vid"], + "protocol" => ["post-user", "protocol"], + "vid" => ["post-origin", "vid"], "psid" => ["post-thread-user", "psid"], - "verb" => "IF (`post-user`.`vid` IS NULL, '', `verb`.`name`)", + "verb" => "IF (`post-origin`.`vid` IS NULL, '', `verb`.`name`)", "title" => ["post-content", "title"], "content-warning" => ["post-content", "content-warning"], "raw-body" => ["post-content", "raw-body"], @@ -366,6 +491,8 @@ "plink" => ["post-content", "plink"], "location" => ["post-content", "location"], "coord" => ["post-content", "coord"], + "sensitive" => ["post-content", "sensitive"], + "restrictions" => ["post-user", "restrictions"], "app" => ["post-content", "app"], "object-type" => ["post-content", "object-type"], "object" => ["post-content", "object"], @@ -468,6 +595,391 @@ "signed_text" => ["diaspora-interaction", "interaction"], "parent-guid" => ["parent-item-uri", "guid"], "parent-network" => ["post-thread-user", "network"], + "parent-owner-id" => ["post-thread-user", "owner-id"], + "parent-author-id" => ["post-thread-user", "author-id"], + "parent-author-link" => ["author", "url"], + "parent-author-name" => ["author", "name"], + "parent-author-nick" => ["author", "nick"], + "parent-author-network" => ["author", "network"], + ], + "query" => "FROM `post-origin` + INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` + INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-thread-user`.`owner-id` + LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-thread-user`.`causer-id` + LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-origin`.`uri-id` + LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-origin`.`thr-parent-id` + LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-origin`.`parent-uri-id` + LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` + LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` + LEFT JOIN `verb` ON `verb`.`id` = `post-origin`.`vid` + LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` + LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id` + LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-origin`.`uri-id` + LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-thread-user`.`psid`" + ], + "post-user-view" => [ + "fields" => [ + "id" => ["post-user", "id"], + "post-user-id" => ["post-user", "id"], + "uid" => ["post-user", "uid"], + "parent" => ["post-thread-user", "post-user-id"], + "uri" => ["item-uri", "uri"], + "uri-id" => ["post-user", "uri-id"], + "parent-uri" => ["parent-item-uri", "uri"], + "parent-uri-id" => ["post-user", "parent-uri-id"], + "thr-parent" => ["thr-parent-item-uri", "uri"], + "thr-parent-id" => ["post-user", "thr-parent-id"], + "conversation" => ["conversation-item-uri", "uri"], + "conversation-id" => ["post-thread-user", "conversation-id"], + "context" => ["context-item-uri", "uri"], + "context-id" => ["post-thread-user", "context-id"], + "quote-uri" => ["quote-item-uri", "uri"], + "quote-uri-id" => ["post-content", "quote-uri-id"], + "guid" => ["item-uri", "guid"], + "wall" => ["post-user", "wall"], + "gravity" => ["post-user", "gravity"], + "extid" => ["external-item-uri", "uri"], + "external-id" => ["post-user", "external-id"], + "replies" => ["replies-item-uri", "uri"], + "replies-id" => ["post-user", "replies-id"], + "created" => ["post-user", "created"], + "edited" => ["post-user", "edited"], + "commented" => ["post-thread-user", "commented"], + "received" => ["post-user", "received"], + "changed" => ["post-thread-user", "changed"], + "post-type" => ["post-user", "post-type"], + "post-reason" => ["post-user", "post-reason"], + "private" => ["post-user", "private"], + "pubmail" => ["post-thread-user", "pubmail"], + "visible" => ["post-user", "visible"], + "starred" => ["post-thread-user", "starred"], + "unseen" => ["post-user", "unseen"], + "deleted" => ["post-user", "deleted"], + "origin" => ["post-user", "origin"], + "parent-origin" => ["post-thread-user", "origin"], + "mention" => ["post-thread-user", "mention"], + "global" => ["post-user", "global"], + "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-user`.`uri-id`)", + "network" => ["post-user", "network"], + "protocol" => ["post-user", "protocol"], + "vid" => ["post-user", "vid"], + "psid" => ["post-user", "psid"], + "verb" => "IF (`post-user`.`vid` IS NULL, '', `verb`.`name`)", + "title" => ["post-content", "title"], + "content-warning" => ["post-content", "content-warning"], + "raw-body" => ["post-content", "raw-body"], + "body" => "IFNULL (`post-content`.`body`, '')", + "rendered-hash" => ["post-content", "rendered-hash"], + "rendered-html" => ["post-content", "rendered-html"], + "language" => ["post-content", "language"], + "plink" => ["post-content", "plink"], + "location" => ["post-content", "location"], + "coord" => ["post-content", "coord"], + "sensitive" => ["post-content", "sensitive"], + "restrictions" => ["post-user", "restrictions"], + "app" => ["post-content", "app"], + "object-type" => ["post-content", "object-type"], + "object" => ["post-content", "object"], + "target-type" => ["post-content", "target-type"], + "target" => ["post-content", "target"], + "resource-id" => ["post-content", "resource-id"], + "contact-id" => ["post-user", "contact-id"], + "contact-uri-id" => ["contact", "uri-id"], + "contact-link" => ["contact", "url"], + "contact-addr" => ["contact", "addr"], + "contact-name" => ["contact", "name"], + "contact-nick" => ["contact", "nick"], + "contact-avatar" => ["contact", "thumb"], + "contact-network" => ["contact", "network"], + "contact-blocked" => ["contact", "blocked"], + "contact-hidden" => ["contact", "hidden"], + "contact-readonly" => ["contact", "readonly"], + "contact-archive" => ["contact", "archive"], + "contact-pending" => ["contact", "pending"], + "contact-rel" => ["contact", "rel"], + "contact-uid" => ["contact", "uid"], + "contact-contact-type" => ["contact", "contact-type"], + "writable" => "IF (`post-user`.`network` IN ('apub', 'dfrn', 'dspr', 'stat'), true, `contact`.`writable`)", + "self" => ["contact", "self"], + "cid" => ["contact", "id"], + "alias" => ["contact", "alias"], + "photo" => ["contact", "photo"], + "name-date" => ["contact", "name-date"], + "uri-date" => ["contact", "uri-date"], + "avatar-date" => ["contact", "avatar-date"], + "thumb" => ["contact", "thumb"], + "author-id" => ["post-user", "author-id"], + "author-uri-id" => ["author", "uri-id"], + "author-link" => ["author", "url"], + "author-addr" => ["author", "addr"], + "author-name" => "IF (`contact`.`url` = `author`.`url` AND `contact`.`name` != '', `contact`.`name`, `author`.`name`)", + "author-nick" => ["author", "nick"], + "author-alias" => ["author", "alias"], + "author-avatar" => "IF (`contact`.`url` = `author`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `author`.`thumb`)", + "author-network" => ["author", "network"], + "author-blocked" => ["author", "blocked"], + "author-hidden" => ["author", "hidden"], + "author-updated" => ["author", "updated"], + "author-contact-type" => ["author", "contact-type"], + "author-gsid" => ["author", "gsid"], + "author-baseurl" => ["author", "baseurl"], + "owner-id" => ["post-user", "owner-id"], + "owner-uri-id" => ["owner", "uri-id"], + "owner-link" => ["owner", "url"], + "owner-addr" => ["owner", "addr"], + "owner-name" => "IF (`contact`.`url` = `owner`.`url` AND `contact`.`name` != '', `contact`.`name`, `owner`.`name`)", + "owner-nick" => ["owner", "nick"], + "owner-alias" => ["owner", "alias"], + "owner-avatar" => "IF (`contact`.`url` = `owner`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `owner`.`thumb`)", + "owner-network" => ["owner", "network"], + "owner-blocked" => ["owner", "blocked"], + "owner-hidden" => ["owner", "hidden"], + "owner-updated" => ["owner", "updated"], + "owner-gsid" => ["owner", "gsid"], + "owner-contact-type" => ["owner", "contact-type"], + "causer-id" => ["post-user", "causer-id"], + "causer-uri-id" => ["causer", "uri-id"], + "causer-link" => ["causer", "url"], + "causer-addr" => ["causer", "addr"], + "causer-name" => ["causer", "name"], + "causer-nick" => ["causer", "nick"], + "causer-alias" => ["causer", "alias"], + "causer-avatar" => ["causer", "thumb"], + "causer-network" => ["causer", "network"], + "causer-blocked" => ["causer", "blocked"], + "causer-hidden" => ["causer", "hidden"], + "causer-gsid" => ["causer", "gsid"], + "causer-contact-type" => ["causer", "contact-type"], + "postopts" => ["post-delivery-data", "postopts"], + "inform" => ["post-delivery-data", "inform"], + "delivery_queue_count" => ["post-delivery-data", "queue_count"], + "delivery_queue_done" => ["post-delivery-data", "queue_done"], + "delivery_queue_failed" => ["post-delivery-data", "queue_failed"], + "allow_cid" => "IF (`post-user`.`psid` IS NULL, '', `permissionset`.`allow_cid`)", + "allow_gid" => "IF (`post-user`.`psid` IS NULL, '', `permissionset`.`allow_gid`)", + "deny_cid" => "IF (`post-user`.`psid` IS NULL, '', `permissionset`.`deny_cid`)", + "deny_gid" => "IF (`post-user`.`psid` IS NULL, '', `permissionset`.`deny_gid`)", + "event-id" => ["post-user", "event-id"], + "event-created" => ["event", "created"], + "event-edited" => ["event", "edited"], + "event-start" => ["event", "start"], + "event-finish" => ["event", "finish"], + "event-summary" => ["event", "summary"], + "event-desc" => ["event", "desc"], + "event-location" => ["event", "location"], + "event-type" => ["event", "type"], + "event-nofinish" => ["event", "nofinish"], + "event-ignore" => ["event", "ignore"], + "question-id" => ["post-question", "id"], + "question-multiple" => ["post-question", "multiple"], + "question-voters" => ["post-question", "voters"], + "question-end-time" => ["post-question", "end-time"], + "has-categories" => "EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-user`.`uri-id` AND `post-category`.`uid` = `post-user`.`uid`)", + "has-media" => "EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-user`.`uri-id`)", + "signed_text" => ["diaspora-interaction", "interaction"], + "parent-guid" => ["parent-item-uri", "guid"], + "parent-network" => ["post-thread-user", "network"], + "parent-owner-id" => ["post-thread-user", "owner-id"], + "parent-author-id" => ["post-thread-user", "author-id"], + "parent-author-link" => ["parent-post-author", "url"], + "parent-author-name" => ["parent-post-author", "name"], + "parent-author-nick" => ["parent-post-author", "nick"], + "parent-author-network" => ["parent-post-author", "network"], + ], + "query" => "FROM `post-user` + INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` + STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` + LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id` + LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-user`.`uri-id` + LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` + LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` + LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` + LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` + LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` + LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` + LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `item-uri` AS `quote-item-uri` ON `quote-item-uri`.`id` = `post-content`.`quote-uri-id` + LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-user`.`uri-id` AND `post-user`.`origin` + LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-user`.`uri-id` + LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid` + LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread-user`.`author-id`" + ], + "post-thread-user-view" => [ + "fields" => [ + "id" => ["post-user", "id"], + "post-user-id" => ["post-user", "id"], + "uid" => ["post-thread-user", "uid"], + "parent" => ["post-thread-user", "post-user-id"], + "uri" => ["item-uri", "uri"], + "uri-id" => ["post-thread-user", "uri-id"], + "parent-uri" => ["parent-item-uri", "uri"], + "parent-uri-id" => ["post-user", "parent-uri-id"], + "thr-parent" => ["thr-parent-item-uri", "uri"], + "thr-parent-id" => ["post-user", "thr-parent-id"], + "conversation" => ["conversation-item-uri", "uri"], + "conversation-id" => ["post-thread-user", "conversation-id"], + "context" => ["context-item-uri", "uri"], + "context-id" => ["post-thread-user", "context-id"], + "quote-uri" => ["quote-item-uri", "uri"], + "quote-uri-id" => ["post-content", "quote-uri-id"], + "guid" => ["item-uri", "guid"], + "wall" => ["post-thread-user", "wall"], + "gravity" => ["post-user", "gravity"], + "extid" => ["external-item-uri", "uri"], + "external-id" => ["post-user", "external-id"], + "replies" => ["replies-item-uri", "uri"], + "replies-id" => ["post-user", "replies-id"], + "created" => ["post-thread-user", "created"], + "edited" => ["post-user", "edited"], + "commented" => ["post-thread-user", "commented"], + "received" => ["post-thread-user", "received"], + "changed" => ["post-thread-user", "changed"], + "post-type" => ["post-user", "post-type"], + "post-reason" => ["post-user", "post-reason"], + "private" => ["post-user", "private"], + "pubmail" => ["post-thread-user", "pubmail"], + "ignored" => ["post-thread-user", "ignored"], + "visible" => ["post-user", "visible"], + "starred" => ["post-thread-user", "starred"], + "unseen" => ["post-thread-user", "unseen"], + "deleted" => ["post-user", "deleted"], + "origin" => ["post-thread-user", "origin"], + "mention" => ["post-thread-user", "mention"], + "global" => ["post-user", "global"], + "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread-user`.`uri-id`)", + "network" => ["post-thread-user", "network"], + "protocol" => ["post-user", "protocol"], + "vid" => ["post-user", "vid"], + "psid" => ["post-thread-user", "psid"], + "verb" => "IF (`post-user`.`vid` IS NULL, '', `verb`.`name`)", + "title" => ["post-content", "title"], + "content-warning" => ["post-content", "content-warning"], + "raw-body" => ["post-content", "raw-body"], + "body" => ["post-content", "body"], + "rendered-hash" => ["post-content", "rendered-hash"], + "rendered-html" => ["post-content", "rendered-html"], + "language" => ["post-content", "language"], + "plink" => ["post-content", "plink"], + "location" => ["post-content", "location"], + "coord" => ["post-content", "coord"], + "sensitive" => ["post-content", "sensitive"], + "restrictions" => ["post-user", "restrictions"], + "app" => ["post-content", "app"], + "object-type" => ["post-content", "object-type"], + "object" => ["post-content", "object"], + "target-type" => ["post-content", "target-type"], + "target" => ["post-content", "target"], + "resource-id" => ["post-content", "resource-id"], + "contact-id" => ["post-thread-user", "contact-id"], + "contact-uri-id" => ["contact", "uri-id"], + "contact-link" => ["contact", "url"], + "contact-addr" => ["contact", "addr"], + "contact-name" => ["contact", "name"], + "contact-nick" => ["contact", "nick"], + "contact-avatar" => ["contact", "thumb"], + "contact-network" => ["contact", "network"], + "contact-blocked" => ["contact", "blocked"], + "contact-hidden" => ["contact", "hidden"], + "contact-readonly" => ["contact", "readonly"], + "contact-archive" => ["contact", "archive"], + "contact-pending" => ["contact", "pending"], + "contact-rel" => ["contact", "rel"], + "contact-uid" => ["contact", "uid"], + "contact-gsid" => ["contact", "gsid"], + "contact-contact-type" => ["contact", "contact-type"], + "writable" => "IF (`post-user`.`network` IN ('apub', 'dfrn', 'dspr', 'stat'), true, `contact`.`writable`)", + "self" => ["contact", "self"], + "cid" => ["contact", "id"], + "alias" => ["contact", "alias"], + "photo" => ["contact", "photo"], + "name-date" => ["contact", "name-date"], + "uri-date" => ["contact", "uri-date"], + "avatar-date" => ["contact", "avatar-date"], + "thumb" => ["contact", "thumb"], + "author-id" => ["post-thread-user", "author-id"], + "author-uri-id" => ["author", "uri-id"], + "author-link" => ["author", "url"], + "author-addr" => ["author", "addr"], + "author-name" => "IF (`contact`.`url` = `author`.`url` AND `contact`.`name` != '', `contact`.`name`, `author`.`name`)", + "author-nick" => ["author", "nick"], + "author-alias" => ["author", "alias"], + "author-avatar" => "IF (`contact`.`url` = `author`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `author`.`thumb`)", + "author-network" => ["author", "network"], + "author-blocked" => ["author", "blocked"], + "author-hidden" => ["author", "hidden"], + "author-updated" => ["author", "updated"], + "author-contact-type" => ["author", "contact-type"], + "author-gsid" => ["author", "gsid"], + "owner-id" => ["post-thread-user", "owner-id"], + "owner-uri-id" => ["owner", "uri-id"], + "owner-link" => ["owner", "url"], + "owner-addr" => ["owner", "addr"], + "owner-name" => "IF (`contact`.`url` = `owner`.`url` AND `contact`.`name` != '', `contact`.`name`, `owner`.`name`)", + "owner-nick" => ["owner", "nick"], + "owner-alias" => ["owner", "alias"], + "owner-avatar" => "IF (`contact`.`url` = `owner`.`url` AND `contact`.`thumb` != '', `contact`.`thumb`, `owner`.`thumb`)", + "owner-network" => ["owner", "network"], + "owner-blocked" => ["owner", "blocked"], + "owner-hidden" => ["owner", "hidden"], + "owner-updated" => ["owner", "updated"], + "owner-gsid" => ["owner", "gsid"], + "owner-contact-type" => ["owner", "contact-type"], + "causer-id" => ["post-thread-user", "causer-id"], + "causer-uri-id" => ["causer", "uri-id"], + "causer-link" => ["causer", "url"], + "causer-addr" => ["causer", "addr"], + "causer-name" => ["causer", "name"], + "causer-nick" => ["causer", "nick"], + "causer-alias" => ["causer", "alias"], + "causer-avatar" => ["causer", "thumb"], + "causer-network" => ["causer", "network"], + "causer-blocked" => ["causer", "blocked"], + "causer-hidden" => ["causer", "hidden"], + "causer-gsid" => ["causer", "gsid"], + "causer-contact-type" => ["causer", "contact-type"], + "postopts" => ["post-delivery-data", "postopts"], + "inform" => ["post-delivery-data", "inform"], + "delivery_queue_count" => ["post-delivery-data", "queue_count"], + "delivery_queue_done" => ["post-delivery-data", "queue_done"], + "delivery_queue_failed" => ["post-delivery-data", "queue_failed"], + "allow_cid" => "IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`allow_cid`)", + "allow_gid" => "IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`allow_gid`)", + "deny_cid" => "IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`deny_cid`)", + "deny_gid" => "IF (`post-thread-user`.`psid` IS NULL, '', `permissionset`.`deny_gid`)", + "event-id" => ["post-user", "event-id"], + "event-created" => ["event", "created"], + "event-edited" => ["event", "edited"], + "event-start" => ["event", "start"], + "event-finish" => ["event", "finish"], + "event-summary" => ["event", "summary"], + "event-desc" => ["event", "desc"], + "event-location" => ["event", "location"], + "event-type" => ["event", "type"], + "event-nofinish" => ["event", "nofinish"], + "event-ignore" => ["event", "ignore"], + "question-id" => ["post-question", "id"], + "question-multiple" => ["post-question", "multiple"], + "question-voters" => ["post-question", "voters"], + "question-end-time" => ["post-question", "end-time"], + "has-categories" => "EXISTS(SELECT `uri-id` FROM `post-category` WHERE `post-category`.`uri-id` = `post-thread-user`.`uri-id` AND `post-category`.`uid` = `post-thread-user`.`uid`)", + "has-media" => "EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-thread-user`.`uri-id`)", + "signed_text" => ["diaspora-interaction", "interaction"], + "parent-guid" => ["parent-item-uri", "guid"], + "parent-network" => ["post-thread-user", "network"], + "parent-owner-id" => ["post-thread-user", "owner-id"], "parent-author-id" => ["post-thread-user", "author-id"], "parent-author-link" => ["author", "url"], "parent-author-name" => ["author", "name"], @@ -484,7 +996,9 @@ LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` LEFT JOIN `event` ON `event`.`id` = `post-user`.`event-id` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread-user`.`uri-id` @@ -504,12 +1018,16 @@ "thr-parent-id" => ["post", "thr-parent-id"], "conversation" => ["conversation-item-uri", "uri"], "conversation-id" => ["post-thread", "conversation-id"], + "context" => ["context-item-uri", "uri"], + "context-id" => ["post-thread", "context-id"], "quote-uri" => ["quote-item-uri", "uri"], "quote-uri-id" => ["post-content", "quote-uri-id"], "guid" => ["item-uri", "guid"], "gravity" => ["post", "gravity"], "extid" => ["external-item-uri", "uri"], "external-id" => ["post", "external-id"], + "replies" => ["replies-item-uri", "uri"], + "replies-id" => ["post", "replies-id"], "created" => ["post", "created"], "edited" => ["post", "edited"], "commented" => ["post-thread", "commented"], @@ -522,6 +1040,7 @@ "global" => ["post", "global"], "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post`.`uri-id`)", "network" => ["post", "network"], + "protocol" => "255", "vid" => ["post", "vid"], "verb" => "IF (`post`.`vid` IS NULL, '', `verb`.`name`)", "title" => ["post-content", "title"], @@ -534,6 +1053,7 @@ "plink" => ["post-content", "plink"], "location" => ["post-content", "location"], "coord" => ["post-content", "coord"], + "sensitive" => ["post-content", "sensitive"], "app" => ["post-content", "app"], "object-type" => ["post-content", "object-type"], "object" => ["post-content", "object"], @@ -615,6 +1135,7 @@ "signed_text" => ["diaspora-interaction", "interaction"], "parent-guid" => ["parent-item-uri", "guid"], "parent-network" => ["post-thread", "network"], + "parent-owner-id" => ["post-thread", "owner-id"], "parent-author-id" => ["post-thread", "author-id"], "parent-author-link" => ["parent-post-author", "url"], "parent-author-name" => ["parent-post-author", "name"], @@ -630,7 +1151,9 @@ LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post`.`replies-id` LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post`.`uri-id` LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post`.`uri-id` @@ -648,12 +1171,16 @@ "thr-parent-id" => ["post", "thr-parent-id"], "conversation" => ["conversation-item-uri", "uri"], "conversation-id" => ["post-thread", "conversation-id"], + "context" => ["context-item-uri", "uri"], + "context-id" => ["post-thread", "context-id"], "quote-uri" => ["quote-item-uri", "uri"], "quote-uri-id" => ["post-content", "quote-uri-id"], "guid" => ["item-uri", "guid"], "gravity" => ["post", "gravity"], "extid" => ["external-item-uri", "uri"], "external-id" => ["post", "external-id"], + "replies" => ["replies-item-uri", "uri"], + "replies-id" => ["post", "replies-id"], "created" => ["post-thread", "created"], "edited" => ["post", "edited"], "commented" => ["post-thread", "commented"], @@ -666,6 +1193,7 @@ "global" => ["post", "global"], "featured" => "EXISTS(SELECT `type` FROM `post-collection` WHERE `type` = 0 AND `uri-id` = `post-thread`.`uri-id`)", "network" => ["post-thread", "network"], + "protocol" => "255", "vid" => ["post", "vid"], "verb" => "IF (`post`.`vid` IS NULL, '', `verb`.`name`)", "title" => ["post-content", "title"], @@ -678,6 +1206,7 @@ "plink" => ["post-content", "plink"], "location" => ["post-content", "location"], "coord" => ["post-content", "coord"], + "sensitive" => ["post-content", "sensitive"], "app" => ["post-content", "app"], "object-type" => ["post-content", "object-type"], "object" => ["post-content", "object"], @@ -761,6 +1290,7 @@ "signed_text" => ["diaspora-interaction", "interaction"], "parent-guid" => ["parent-item-uri", "guid"], "parent-network" => ["post-thread", "network"], + "parent-owner-id" => ["post-thread", "owner-id"], "parent-author-id" => ["post-thread", "author-id"], "parent-author-link" => ["author", "url"], "parent-author-name" => ["author", "name"], @@ -776,7 +1306,9 @@ LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post`.`thr-parent-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post`.`parent-uri-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id` + LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post`.`external-id` + LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post`.`replies-id` LEFT JOIN `verb` ON `verb`.`id` = `post`.`vid` LEFT JOIN `diaspora-interaction` ON `diaspora-interaction`.`uri-id` = `post-thread`.`uri-id` LEFT JOIN `post-content` ON `post-content`.`uri-id` = `post-thread`.`uri-id` @@ -844,34 +1376,6 @@ LEFT JOIN `tag` ON `post-tag`.`tid` = `tag`.`id` LEFT JOIN `contact` ON `post-tag`.`cid` = `contact`.`id`" ], - "network-item-view" => [ - "fields" => [ - "uri-id" => ["post-user", "uri-id"], - "parent" => ["post-thread-user", "post-user-id"], - "received" => ["post-user", "received"], - "commented" => ["post-thread-user", "commented"], - "created" => ["post-user", "created"], - "uid" => ["post-user", "uid"], - "starred" => ["post-thread-user", "starred"], - "mention" => ["post-thread-user", "mention"], - "network" => ["post-user", "network"], - "unseen" => ["post-user", "unseen"], - "gravity" => ["post-user", "gravity"], - "contact-id" => ["post-user", "contact-id"], - "contact-type" => ["ownercontact", "contact-type"], - ], - "query" => "FROM `post-user` - INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` - STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` - STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` - STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` - WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` - AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) - AND (`post-user`.`hidden` IS NULL OR NOT `post-user`.`hidden`) - AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` - AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`)) - AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`)" - ], "network-thread-view" => [ "fields" => [ "uri-id" => ["post-thread-user", "uri-id"], @@ -883,6 +1387,7 @@ "starred" => ["post-thread-user", "starred"], "mention" => ["post-thread-user", "mention"], "network" => ["post-thread-user", "network"], + "protocol" => ["post-user", "protocol"], "contact-id" => ["post-thread-user", "contact-id"], "contact-type" => ["ownercontact", "contact-type"], ], @@ -895,8 +1400,35 @@ AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` - AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`)) - AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`)" + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`post-thread-user`.`author-id`, `post-thread-user`.`owner-id`, `post-thread-user`.`causer-id`) AND (`blocked` OR `ignored` OR `is-blocked` OR `channel-only`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`)" + ], + "network-thread-circle-view" => [ + "fields" => [ + "uri-id" => ["post-thread-user", "uri-id"], + "parent" => ["post-thread-user", "post-user-id"], + "received" => ["post-thread-user", "received"], + "commented" => ["post-thread-user", "commented"], + "created" => ["post-thread-user", "created"], + "uid" => ["post-thread-user", "uid"], + "starred" => ["post-thread-user", "starred"], + "mention" => ["post-thread-user", "mention"], + "network" => ["post-thread-user", "network"], + "protocol" => ["post-user", "protocol"], + "contact-id" => ["post-thread-user", "contact-id"], + "contact-type" => ["ownercontact", "contact-type"], + ], + "query" => "FROM `post-thread-user` + INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` + STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id` + STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id` + STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id` + WHERE `post-user`.`visible` AND NOT `post-user`.`deleted` + AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`) + AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`) + AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked` + AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`post-thread-user`.`author-id`, `post-thread-user`.`owner-id`, `post-thread-user`.`causer-id`) AND (`blocked` OR `ignored` OR `is-blocked`)) + AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`)" ], "owner-view" => [ "fields" => [ @@ -935,7 +1467,6 @@ "poll" => ["contact", "poll"], "confirm" => ["contact", "confirm"], "poco" => ["contact", "poco"], - "subhub" => ["contact", "subhub"], "hub-verify" => ["contact", "hub-verify"], "last-update" => ["contact", "last-update"], "success_update" => ["contact", "success_update"], @@ -962,6 +1493,7 @@ "unsearchable" => ["contact", "unsearchable"], "sensitive" => ["contact", "sensitive"], "baseurl" => ["contact", "baseurl"], + "gsid" => ["contact", "gsid"], "reason" => ["contact", "reason"], "info" => ["contact", "info"], "bdyear" => ["contact", "bdyear"], @@ -984,14 +1516,10 @@ "theme" => ["user", "theme"], "upubkey" => ["user", "pubkey"], "uprvkey" => ["user", "prvkey"], - "sprvkey" => ["user", "sprvkey"], - "spubkey" => ["user", "spubkey"], "verified" => ["user", "verified"], "blockwall" => ["user", "blockwall"], "hidewall" => ["user", "hidewall"], "blocktags" => ["user", "blocktags"], - "unkmail" => ["user", "unkmail"], - "cntunkmail" => ["user", "cntunkmail"], "notify-flags" => ["user", "notify-flags"], "page-flags" => ["user", "page-flags"], "account-type" => ["user", "account-type"], @@ -1096,6 +1624,7 @@ "ap-outbox" => ["apcontact", "outbox"], "ap-sharedinbox" => ["apcontact", "sharedinbox"], "ap-generator" => ["apcontact", "generator"], + "ap-posting-restricted" => ["apcontact", "posting-restricted"], "ap-following_count" => ["apcontact", "following_count"], "ap-followers_count" => ["apcontact", "followers_count"], "ap-statuses_count" => ["apcontact", "statuses_count"], @@ -1179,7 +1708,6 @@ "readonly" => ["ucontact", "readonly"], "blocked" => ["ucontact", "blocked"], "block_reason" => ["ucontact", "block_reason"], - "subhub" => ["ucontact", "subhub"], "hub-verify" => ["ucontact", "hub-verify"], "reason" => ["ucontact", "reason"], "dfrn-notify" => ["contact", "notify"], @@ -1200,6 +1728,7 @@ "ap-outbox" => ["apcontact", "outbox"], "ap-sharedinbox" => ["apcontact", "sharedinbox"], "ap-generator" => ["apcontact", "generator"], + "ap-posting-restricted" => ["apcontact", "posting-restricted"], "ap-following_count" => ["apcontact", "following_count"], "ap-followers_count" => ["apcontact", "followers_count"], "ap-statuses_count" => ["apcontact", "statuses_count"], @@ -1234,7 +1763,8 @@ ], "query" => "FROM `register` INNER JOIN `contact` ON `register`.`uid` = `contact`.`uid` - INNER JOIN `user` ON `register`.`uid` = `user`.`uid`" + INNER JOIN `user` ON `register`.`uid` = `user`.`uid` + WHERE `register`.`uid` != 0" ], "tag-search-view" => [ "fields" => [ @@ -1248,6 +1778,7 @@ "gravity" => ["post-user", "gravity"], "received" => ["post-user", "received"], "network" => ["post-user", "network"], + "protocol" => ["post-user", "protocol"], "author-id" => ["post-user", "author-id"], "name" => ["tag", "name"], ], @@ -1272,7 +1803,7 @@ "label" => ["profile_field", "label"], "value" => ["profile_field", "value"], "order" => ["profile_field", "order"], - "psid"=> ["profile_field", "psid"], + "psid" => ["profile_field", "psid"], "allow_cid" => ["permissionset", "allow_cid"], "allow_gid" => ["permissionset", "allow_gid"], "deny_cid" => ["permissionset", "deny_cid"], diff --git a/static/defaults.config.php b/static/defaults.config.php index b33b30b562..ada2aa3eac 100644 --- a/static/defaults.config.php +++ b/static/defaults.config.php @@ -1,23 +1,9 @@ . - * - * This file declares the default values for the base config of Friendica. + * SPDX-License-Identifier: AGPL-3.0-or-later * * These configuration values aren't accessible from the admin settings page and custom values must be set in config/local.config.php * @@ -97,6 +83,10 @@ return [ // Checks for missing entries in "post", "post-thread" or "post-thread-user" and creates them 'add_missing_posts' => false, + // admin_inactivity_limit (Integer) + // Days of inactivity after which an admin is considered inactive. "0" means that there will be no check for inactivity. + 'admin_inactivity_limit' => 30, + // allowed_link_protocols (Array) // Allowed protocols in links URLs, add at your own risk. http(s) is always allowed. 'allowed_link_protocols' => ['ftp://', 'ftps://', 'mailto:', 'cid:', 'gopher://'], @@ -136,6 +126,11 @@ return [ // Display "Emoji Only" posts in big. 'big_emojis' => true, + // basepath (String) + // Absolute file path to your Friendica install + // Examples: /var/www, /home/user/friendica... + 'basepath' => '', + // bulk_delivery (Boolean) // Delivers AP messages in a bulk (experimental) 'bulk_delivery' => false, @@ -162,6 +157,7 @@ return [ // config_adapter (jit|preload) // Allow to switch the configuration adapter to improve performances at the cost of memory consumption. + // jit is slightly slower, while preload consumes marginally more memory 'config_adapter' => 'jit', // crawl_permit_period (Integer) @@ -254,6 +250,10 @@ return [ // Display the first resharer as icon and text on a reshared item. 'display_resharer' => false, + // display_link_length (integer) + // Maximum length of displayed links. Default value is 30, 0 deactivates the functionality. + 'display_link_length' => 30, + // dlogfile (Path) // location of the developer log file. 'dlogfile' => '', @@ -339,7 +339,8 @@ return [ 'lock_driver' => '', // logger_config (String) - // Sets the logging adapter of Friendica globally (monolog, syslog, stream) + // Sets the logging adapter of Friendica globally (syslog, stream) + // @deprecated 2025.02 The value `monolog` is deprecated, please use `stream` or `syslog` instead. 'logger_config' => 'stream', // syslog_flags (Integer) @@ -379,6 +380,10 @@ return [ // Maximum number of feed items that are fetched and processed. For unlimited items set to 0. 'max_feed_items' => 20, + // max_fetchreplies_depth (Integer) + // Maximum number of "fetchreplies" activities in the callstack. The higher, the more complete a thread will be. + 'max_fetchreplies_depth' => 2, + // max_image_length (Integer) // An alternate way of limiting picture upload sizes. // Specify the maximum pixel length that pictures are allowed to be (for non-square pictures, it will apply to the longest side). @@ -406,6 +411,10 @@ return [ // Maximum recursion depth when fetching posts until the job is delegated to a worker task or finished. 'max_recursion_depth' => 50, + // max_video_height (Integer) + // Maximum height of videos in portrait mode. + 'max_video_height' => 640, + // memcache_host (String) // Host name of the memcache daemon. 'memcache_host' => '127.0.0.1', @@ -428,10 +437,6 @@ return [ // Don't do count calculations (currently only when showing photo albums). 'no_count' => false, - // no_oembed (Boolean) - // Don't use OEmbed to fetch more information about a link. - 'no_oembed' => false, - // no_redirect_list (Array) // List of domains where HTTP redirects should be ignored. 'no_redirect_list' => [], @@ -444,6 +449,15 @@ return [ // Optimizes all tables instead of only tables like workerqueue or the cache 'optimize_all_tables' => false, + // page_execution_logfile (Path) + // Name of a logfile to log slow page executions. + 'page_execution_logfile' => '', + + // page_execution_log_limit (Integer) + // If a page execution lasts longer than this value in seconds it is logged. + // Inactive if system => page_execution_logfile is empty. + 'page_execution_log_limit' => 2, + // paranoia (Boolean) // Log out users if their IP address changed. 'paranoia' => false, @@ -464,6 +478,14 @@ return [ // Enable internal timings to help optimize code. Needed for "rendertime" addon. 'profiler' => false, + // queue_expired_days (Integer) + // Number of days after unprocessed inbox items are removed from the queue. Minimum is 1. + 'queue_expired_days' => 7, + + // queue_retrial (Integer) + // Number of retrial after unprocessed inbox items are removed from the queue. Minimum is 3. + 'queue_retrial' => 10, + // redis_host (String) // Host name or the path to the Unix domain socket of the Redis daemon. 'redis_host' => '127.0.0.1', @@ -484,6 +506,12 @@ return [ // Redistribute incoming activities via ActivityPub 'redistribute_activities' => true, + // remove_all_unused_contacts (Boolean) + // Remove all unused contacts. + // Per default only archived contacts are removed from federated services. + // Unused contacts from connector networks will be removed in any case. + 'remove_all_unused_contacts' => false, + // session_handler (database|cache|native) // Whether to use Cache to store session data or to use PHP native session storage. 'session_handler' => 'database', @@ -534,6 +562,10 @@ return [ // Show all themes including the unsupported ones. 'show_unsupported_themes' => false, + // stats_key (String) + // A random string to be added to the /stats?key=... endpoint to enable the monitoring statistics + 'stats_key' => '', + // throttle_limit_day (Integer) // Maximum number of posts that a user can send per day with the API. 0 to disable daily throttling. 'throttle_limit_day' => 0, @@ -550,6 +582,12 @@ return [ // Transmit pending events upon accepted contact request for groups 'transmit_pending_events' => false, + // url (String) + // The absolute URL used to access your Friendica node. It should include the scheme, the domain name, and the + // sub-folder if any. Used by command-line processes to send correct links to your Friendica server. + // Example: https://example.com/friendica + 'url' => '', + // username_min_length (Integer) // The minimum character length a username can be. // This length is checked once the username has been trimmed and multiple spaces have been collapsed into one. @@ -599,6 +637,10 @@ return [ Friendica\Core\Worker::PRIORITY_NEGLIGIBLE => 720 ], + // worker_max_idletime (Integer) + // Higly experimental! Maximum number of seconds after the last worker execution to enforce a new worker process. + 'worker_max_idletime' => 0, + // worker_processes_cooldown (Integer) // Maximum number per processes that causes a cooldown before each worker function call. 'worker_processes_cooldown' => 0, @@ -613,6 +655,12 @@ return [ // Timeout in seconds for fetching the XRD links and other requests with an expected shorter timeout 'xrd_timeout' => 20, ], + 'performance' => [ + // max_response_data_size (Integer) + // Maximum allowed outgoing HTTP request response data size in Bytes. Does not affect incoming requests to this node. + // Warning: Lowering this value can help with some PHP memory exhaustion issues, but can also partially break some federation features e.g. large posts may not be fetched or received from remote servers. + 'max_response_data_size' => 1000000, + ], 'proxy' => [ // forwarded_for_headers (String) // A comma separated list of all allowed header values to retrieve the real client IP @@ -694,4 +742,13 @@ return [ // Wether the blocklist is publicly listed under /about (or in any later API) 'public' => true, ], + 'jetstream' => [ + // pidfile (Path) + // Jetstream pid file path. For example: pidfile = /path/to/jetstream.pid + 'pidfile' => '', + // did_limit (Integer) + // Maximum number of DIDs that are filtered in Jetstream. The maximum number is 10,000, + // The higher the number, the more likely the system won't be able to process the posts on time. + 'did_limit' => 1000, + ], ]; diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 20415e3fe7..04e2dd2aee 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -1,22 +1,11 @@ . - * * The configuration defines "complex" dependencies inside Friendica * So this classes shouldn't be simple or their dependencies are already defined here. * @@ -31,304 +20,304 @@ * * - $a = new ClassA($creationPassedVariable); * + * @link https://r.je/dice */ use Dice\Dice; -use Friendica\App; -use Friendica\Core\Cache; -use Friendica\Core\Config; -use Friendica\Core\Hooks\Capability\ICanCreateInstances; -use Friendica\Core\Hooks\Capability\ICanRegisterStrategies; -use Friendica\Core\Hooks\Model\DiceInstanceManager; -use Friendica\Core\PConfig; -use Friendica\Core\L10n; -use Friendica\Core\Lock; -use Friendica\Core\Session\Capability\IHandleSessions; -use Friendica\Core\Session\Capability\IHandleUserSessions; -use Friendica\Core\Storage\Repository\StorageManager; -use Friendica\Database\Database; -use Friendica\Database\Definition\DbaDefinition; -use Friendica\Database\Definition\ViewDefinition; -use Friendica\Factory; -use Friendica\Core\Storage\Capability\ICanWriteToStorage; -use Friendica\Model\User\Cookie; -use Friendica\Model\Log\ParsedLogIterator; -use Friendica\Network; -use Friendica\Util; -use Psr\Log\LoggerInterface; -return [ - '*' => [ - // marks all class result as shared for other creations, so there's just - // one instance for the whole execution - 'shared' => true, - ], - \Friendica\Core\Addon\Capability\ICanLoadAddons::class => [ - 'instanceOf' => \Friendica\Core\Addon\Model\AddonLoader::class, - 'constructParams' => [ - [Dice::INSTANCE => '$basepath'], - [Dice::INSTANCE => Dice::SELF], +/** + * @param string $basepath The base path of the Friendica installation without trailing slash + */ +return (function(string $basepath, array $getVars, array $serverVars, array $cookieVars): array { + return [ + '*' => [ + // marks all class result as shared for other creations, so there's just + // one instance for the whole execution + 'shared' => true, ], - ], - '$basepath' => [ - 'instanceOf' => Util\BasePath::class, - 'call' => [ - ['getPath', [], Dice::CHAIN_CALL], + \Friendica\Core\Addon\Capability\ICanLoadAddons::class => [ + 'instanceOf' => \Friendica\Core\Addon\Model\AddonLoader::class, + 'constructParams' => [ + $basepath, + [Dice::INSTANCE => Dice::SELF], + ], ], - 'constructParams' => [ - dirname(__FILE__, 2), - $_SERVER - ] - ], - Util\BasePath::class => [ - 'constructParams' => [ - dirname(__FILE__, 2), - $_SERVER - ] - ], - DiceInstanceManager::class => [ - 'constructParams' => [ - [Dice::INSTANCE => Dice::SELF], - ] - ], - \Friendica\Core\Hooks\Util\StrategiesFileManager::class => [ - 'constructParams' => [ - [Dice::INSTANCE => '$basepath'], + \Friendica\Core\Addon\AddonHelper::class => [ + 'instanceOf' => \Friendica\Core\Addon\AddonManagerHelper::class, + 'constructParams' => [ + $basepath . '/addon', + ], ], - 'call' => [ - ['loadConfig'], + \Friendica\Util\BasePath::class => [ + 'constructParams' => [ + $basepath, + $serverVars, + ] ], - ], - ICanRegisterStrategies::class => [ - 'instanceOf' => DiceInstanceManager::class, - 'constructParams' => [ - [Dice::INSTANCE => Dice::SELF], + \Friendica\Core\Hooks\Model\DiceInstanceManager::class => [ + 'constructParams' => [ + [Dice::INSTANCE => Dice::SELF], + ] ], - ], - ICanCreateInstances::class => [ - 'instanceOf' => DiceInstanceManager::class, - 'constructParams' => [ - [Dice::INSTANCE => Dice::SELF], + \Friendica\Core\Hooks\Util\StrategiesFileManager::class => [ + 'constructParams' => [ + $basepath, + ], + 'call' => [ + ['loadConfig'], + ], ], - ], - Config\Util\ConfigFileManager::class => [ - 'instanceOf' => Config\Factory\Config::class, - 'call' => [ - ['createConfigFileManager', [ - [Dice::INSTANCE => '$basepath'], - $_SERVER, - ], Dice::CHAIN_CALL], + \Friendica\Core\Hooks\Capability\ICanRegisterStrategies::class => [ + 'instanceOf' => \Friendica\Core\Hooks\Model\DiceInstanceManager::class, + 'constructParams' => [ + [Dice::INSTANCE => Dice::SELF], + ], ], - ], - Config\ValueObject\Cache::class => [ - 'instanceOf' => Config\Factory\Config::class, - 'call' => [ - ['createCache', [], Dice::CHAIN_CALL], + \Friendica\AppHelper::class => [ + 'instanceOf' => \Friendica\AppLegacy::class, ], - ], - App\Mode::class => [ - 'call' => [ - ['determineRunMode', [true, $_SERVER], Dice::CHAIN_CALL], - ['determine', [ - [Dice::INSTANCE => '$basepath'] - ], Dice::CHAIN_CALL], + \Friendica\Core\Hooks\Capability\ICanCreateInstances::class => [ + 'instanceOf' => \Friendica\Core\Hooks\Model\DiceInstanceManager::class, + 'constructParams' => [ + [Dice::INSTANCE => Dice::SELF], + ], ], - ], - Config\Capability\IManageConfigValues::class => [ - 'instanceOf' => Config\Model\DatabaseConfig::class, - 'constructParams' => [ - $_SERVER, + \Friendica\Core\Config\Util\ConfigFileManager::class => [ + 'instanceOf' => \Friendica\Core\Config\Factory\Config::class, + 'call' => [ + ['createConfigFileManager', [ + $basepath, + $basepath . '/addon', + $serverVars, + ], Dice::CHAIN_CALL], + ], ], - ], - PConfig\Capability\IManagePersonalConfigValues::class => [ - 'instanceOf' => PConfig\Factory\PConfig::class, - 'call' => [ - ['create', [], Dice::CHAIN_CALL], - ] - ], - DbaDefinition::class => [ - 'constructParams' => [ - [Dice::INSTANCE => '$basepath'], + \Friendica\Core\Config\ValueObject\Cache::class => [ + 'instanceOf' => \Friendica\Core\Config\Factory\Config::class, + 'call' => [ + ['createCache', [], Dice::CHAIN_CALL], + ], ], - 'call' => [ - ['load', [false], Dice::CHAIN_CALL], + \Friendica\App\Mode::class => [ + 'call' => [ + ['determineRunMode', [true, $serverVars], Dice::CHAIN_CALL], + ['determine', [ + $basepath, + ], Dice::CHAIN_CALL], + ], ], - ], - ViewDefinition::class => [ - 'constructParams' => [ - [Dice::INSTANCE => '$basepath'], + \Friendica\Core\Config\Capability\IManageConfigValues::class => [ + 'instanceOf' => \Friendica\Core\Config\Model\DatabaseConfig::class, + 'constructParams' => [ + $serverVars, + ], ], - 'call' => [ - ['load', [false], Dice::CHAIN_CALL], + \Friendica\Core\PConfig\Capability\IManagePersonalConfigValues::class => [ + 'instanceOf' => \Friendica\Core\PConfig\Factory\PConfig::class, + 'call' => [ + ['create', [], Dice::CHAIN_CALL], + ] ], - ], - Database::class => [ - 'constructParams' => [ - [Dice::INSTANCE => Config\Model\ReadOnlyFileConfig::class], + \Friendica\Database\Definition\DbaDefinition::class => [ + 'constructParams' => [ + $basepath, + ], + 'call' => [ + ['load', [false], Dice::CHAIN_CALL], + ], ], - ], - /** - * Creates the App\BaseURL - * - * Same as: - * $baseURL = new App\BaseURL($configuration, $_SERVER); - */ - App\BaseURL::class => [ - 'constructParams' => [ - $_SERVER, + \Friendica\Database\Definition\ViewDefinition::class => [ + 'constructParams' => [ + $basepath, + ], + 'call' => [ + ['load', [false], Dice::CHAIN_CALL], + ], ], - ], - '$hostname' => [ - 'instanceOf' => App\BaseURL::class, - 'constructParams' => [ - $_SERVER, + \Friendica\Database\Database::class => [ + 'constructParams' => [ + [Dice::INSTANCE => \Friendica\Core\Config\Model\ReadOnlyFileConfig::class], + ], ], - 'call' => [ - ['getHost', [], Dice::CHAIN_CALL], + \Friendica\App\BaseURL::class => [ + 'constructParams' => [ + $serverVars, + ], ], - ], - Cache\Type\AbstractCache::class => [ - 'constructParams' => [ - [Dice::INSTANCE => '$hostname'], + '$hostname' => [ + 'instanceOf' => \Friendica\App\BaseURL::class, + 'constructParams' => [ + $serverVars, + ], + 'call' => [ + ['getHost', [], Dice::CHAIN_CALL], + ], ], - ], - App\Page::class => [ - 'constructParams' => [ - [Dice::INSTANCE => '$basepath'], + \Friendica\Core\Cache\Type\AbstractCache::class => [ + 'constructParams' => [ + [Dice::INSTANCE => '$hostname'], + ], ], - ], - \Psr\Log\LoggerInterface::class => [ - 'instanceOf' => \Friendica\Core\Logger\Factory\Logger::class, - 'call' => [ - ['create', [], Dice::CHAIN_CALL], + \Friendica\App\Page::class => [ + 'constructParams' => [ + $basepath, + ], ], - ], - \Friendica\Core\Logger\Type\SyslogLogger::class => [ - 'instanceOf' => \Friendica\Core\Logger\Factory\SyslogLogger::class, - 'call' => [ - ['create', [], Dice::CHAIN_CALL], + \Psr\Log\LoggerInterface::class => [ + 'instanceOf' => \Friendica\Core\Logger\LoggerManager::class, + 'call' => [ + ['getLogger', [], Dice::CHAIN_CALL], + ], ], - ], - \Friendica\Core\Logger\Type\StreamLogger::class => [ - 'instanceOf' => \Friendica\Core\Logger\Factory\StreamLogger::class, - 'call' => [ - ['create', [], Dice::CHAIN_CALL], + \Friendica\Core\Logger\LoggerManager::class => [ + 'substitutions' => [ + \Friendica\Core\Logger\Factory\LoggerFactory::class => \Friendica\Core\Logger\Factory\DelegatingLoggerFactory::class, + ], ], - ], - \Friendica\Core\Logger\Capability\IHaveCallIntrospections::class => [ - 'instanceOf' => \Friendica\Core\Logger\Util\Introspection::class, - 'constructParams' => [ - \Friendica\Core\Logger\Capability\IHaveCallIntrospections::IGNORE_CLASS_LIST, + \Friendica\Core\Logger\Factory\LoggerFactory::class => [ + 'instanceOf' => \Friendica\Core\Logger\Factory\DelegatingLoggerFactory::class, + 'call' => [ + ['registerFactory', ['stream', [Dice::INSTANCE => '$StreamLoggerFactory']]], + ['registerFactory', ['syslog', [Dice::INSTANCE => '$SyslogLoggerFactory']]], + ], ], - ], - '$devLogger' => [ - 'instanceOf' => \Friendica\Core\Logger\Factory\StreamLogger::class, - 'call' => [ - ['createDev', [], Dice::CHAIN_CALL], + '$StreamLoggerFactory' => [ + 'instanceOf' => \Friendica\Core\Logger\Factory\StreamLoggerFactory::class, + 'substitutions' => [ + \Friendica\Core\Logger\Util\FileSystemUtil::class => \Friendica\Core\Logger\Util\FileSystem::class, + ], ], - ], - Cache\Capability\ICanCache::class => [ - 'instanceOf' => Cache\Factory\Cache::class, - 'call' => [ - ['createLocal', [], Dice::CHAIN_CALL], + '$SyslogLoggerFactory' => [ + 'instanceOf' => \Friendica\Core\Logger\Factory\SyslogLoggerFactory::class, ], - ], - Cache\Capability\ICanCacheInMemory::class => [ - 'instanceOf' => Cache\Factory\Cache::class, - 'call' => [ - ['createLocal', [], Dice::CHAIN_CALL], + \Friendica\Core\Logger\Type\SyslogLogger::class => [ + 'instanceOf' => \Friendica\Core\Logger\Factory\SyslogLogger::class, + 'call' => [ + ['create', [], Dice::CHAIN_CALL], + ], ], - ], - Lock\Capability\ICanLock::class => [ - 'instanceOf' => Lock\Factory\Lock::class, - 'call' => [ - ['create', [], Dice::CHAIN_CALL], + \Friendica\Core\Logger\Type\StreamLogger::class => [ + 'instanceOf' => \Friendica\Core\Logger\Factory\StreamLogger::class, + 'call' => [ + ['create', [], Dice::CHAIN_CALL], + ], ], - ], - App\Arguments::class => [ - 'instanceOf' => App\Arguments::class, - 'call' => [ - ['determine', [$_SERVER, $_GET], Dice::CHAIN_CALL], + \Psr\EventDispatcher\EventDispatcherInterface::class => [ + 'instanceOf' => \Friendica\Event\EventDispatcher::class, ], - ], - \Friendica\Core\System::class => [ - 'constructParams' => [ - [Dice::INSTANCE => '$basepath'], + \Friendica\Core\Logger\Capability\IHaveCallIntrospections::class => [ + 'instanceOf' => \Friendica\Core\Logger\Util\Introspection::class, + 'constructParams' => [ + \Friendica\Core\Logger\Capability\IHaveCallIntrospections::IGNORE_CLASS_LIST, + ], ], - ], - App\Router::class => [ - 'constructParams' => [ - $_SERVER, - __DIR__ . '/routes.config.php', - [Dice::INSTANCE => Dice::SELF], - null + \Friendica\Core\Cache\Capability\ICanCache::class => [ + 'instanceOf' => \Friendica\Core\Cache\Factory\Cache::class, + 'call' => [ + ['createLocal', [], Dice::CHAIN_CALL], + ], ], - ], - L10n::class => [ - 'constructParams' => [ - $_SERVER, $_GET + \Friendica\Core\Cache\Capability\ICanCacheInMemory::class => [ + 'instanceOf' => \Friendica\Core\Cache\Factory\Cache::class, + 'call' => [ + ['createLocal', [], Dice::CHAIN_CALL], + ], ], - ], - IHandleSessions::class => [ - 'instanceOf' => \Friendica\Core\Session\Factory\Session::class, - 'call' => [ - ['create', [$_SERVER], Dice::CHAIN_CALL], - ['start', [], Dice::CHAIN_CALL], + \Friendica\Core\Lock\Capability\ICanLock::class => [ + 'instanceOf' => \Friendica\Core\Lock\Factory\Lock::class, + 'call' => [ + ['create', [], Dice::CHAIN_CALL], + ], ], - ], - IHandleUserSessions::class => [ - 'instanceOf' => \Friendica\Core\Session\Model\UserSession::class, - ], - Cookie::class => [ - 'constructParams' => [ - $_COOKIE + \Friendica\App\Arguments::class => [ + 'instanceOf' => \Friendica\App\Arguments::class, + 'call' => [ + ['determine', [$serverVars, $getVars], Dice::CHAIN_CALL], + ], ], - ], - ICanWriteToStorage::class => [ - 'instanceOf' => StorageManager::class, - 'call' => [ - ['getBackend', [], Dice::CHAIN_CALL], + \Friendica\Core\System::class => [ + 'constructParams' => [ + $basepath, + ], ], - ], - \Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs::class => [ - 'instanceOf' => \Friendica\Core\KeyValueStorage\Factory\KeyValueStorage::class, - 'call' => [ - ['create', [], Dice::CHAIN_CALL], + \Friendica\App\Router::class => [ + 'constructParams' => [ + $serverVars, + __DIR__ . '/routes.config.php', + null + ], ], - ], - Network\HTTPClient\Capability\ICanSendHttpRequests::class => [ - 'instanceOf' => Network\HTTPClient\Factory\HttpClient::class, - 'call' => [ - ['createClient', [], Dice::CHAIN_CALL], + \Friendica\Core\L10n::class => [ + 'constructParams' => [ + $serverVars, $getVars + ], ], - ], - ParsedLogIterator::class => [ - 'constructParams' => [ - [Dice::INSTANCE => Util\ReversedFileReader::class], - ] - ], - \Friendica\Core\Worker\Repository\Process::class => [ - 'constructParams' => [ - $_SERVER + \Friendica\Core\Session\Capability\IHandleSessions::class => [ + 'instanceOf' => \Friendica\Core\Session\Factory\Session::class, + 'call' => [ + ['create', [$serverVars], Dice::CHAIN_CALL], + ['start', [], Dice::CHAIN_CALL], + ], ], - ], - App\Request::class => [ - 'constructParams' => [ - $_SERVER + \Friendica\Core\Session\Capability\IHandleUserSessions::class => [ + 'instanceOf' => \Friendica\Core\Session\Model\UserSession::class, ], - ], - \Psr\Clock\ClockInterface::class => [ - 'instanceOf' => Util\Clock\SystemClock::class - ], - \Friendica\Module\Special\HTTPException::class => [ - 'constructParams' => [ - $_SERVER + \Friendica\Model\User\Cookie::class => [ + 'constructParams' => [ + $cookieVars, + ], ], - ], - \Friendica\Module\Api\ApiResponse::class => [ - 'constructParams' => [ - $_SERVER, - $_GET['callback'] ?? '', + \Friendica\Core\Storage\Capability\ICanWriteToStorage::class => [ + 'instanceOf' => \Friendica\Core\Storage\Repository\StorageManager::class, + 'call' => [ + ['getBackend', [], Dice::CHAIN_CALL], + ], ], - ], -]; + \Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs::class => [ + 'instanceOf' => \Friendica\Core\KeyValueStorage\Factory\KeyValueStorage::class, + 'call' => [ + ['create', [], Dice::CHAIN_CALL], + ], + ], + \Friendica\Network\HTTPClient\Capability\ICanSendHttpRequests::class => [ + 'instanceOf' => \Friendica\Network\HTTPClient\Factory\HttpClient::class, + 'call' => [ + ['createClient', [], Dice::CHAIN_CALL], + ], + ], + \Friendica\Model\Log\ParsedLogIterator::class => [ + 'constructParams' => [ + [Dice::INSTANCE => \Friendica\Util\ReversedFileReader::class], + ] + ], + \Friendica\Core\Worker\Repository\Process::class => [ + 'constructParams' => [ + $serverVars + ], + ], + \Friendica\App\Request::class => [ + 'constructParams' => [ + $serverVars + ], + ], + \Psr\Clock\ClockInterface::class => [ + 'instanceOf' => \Friendica\Util\Clock\SystemClock::class + ], + \Friendica\Module\Special\HTTPException::class => [ + 'constructParams' => [ + $serverVars + ], + ], + \Friendica\Module\Api\ApiResponse::class => [ + 'constructParams' => [ + $serverVars, + $getVars['callback'] ?? '', + ], + ], + ]; +})( + dirname(__FILE__, 2), + $_GET, + $_SERVER, + $_COOKIE +); diff --git a/static/did-v1.jsonld b/static/did-v1.jsonld new file mode 100644 index 0000000000..ea65773c17 --- /dev/null +++ b/static/did-v1.jsonld @@ -0,0 +1,54 @@ +{ + "@context": { + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} diff --git a/static/env.config.php b/static/env.config.php index 81c399060b..e6f7c8c836 100644 --- a/static/env.config.php +++ b/static/env.config.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Main mapping table of environment variables to namespaced config values * diff --git a/static/platforms.config.php b/static/platforms.config.php index 81a04460d6..cfb5b16c0c 100644 --- a/static/platforms.config.php +++ b/static/platforms.config.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later $platforms = [ 'AlphaChat' => 'alphachat', @@ -143,3 +129,10 @@ $zap_platforms = [ 'hubzilla' => 'hubzilla', 'osada' => 'osada', ]; + +return [ + 'ap_platforms' => $ap_platforms, + 'dfrn_platforms' => $dfrn_platforms, + 'zap_platforms' => $zap_platforms, + 'platforms' => $platforms, +]; diff --git a/static/routes.config.php b/static/routes.config.php index 8aa2d22ed9..4c7b6949ec 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Configuration for the default routes in Friendica * @@ -41,14 +29,13 @@ $profileRoutes = [ '/restricted' => [Module\Profile\Restricted::class, [R::GET ]], '/schedule' => [Module\Profile\Schedule::class, [R::GET, R::POST]], '/conversations[/{category}[/{date1}[/{date2}]]]' => [Module\Profile\Conversations::class, [R::GET]], - '/unkmail' => [Module\Profile\UnkMail::class, [R::GET, R::POST]], ]; $apiRoutes = [ '/account' => [ '/verify_credentials[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Account\VerifyCredentials::class, [R::GET ]], '/rate_limit_status[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Account\RateLimitStatus::class, [R::GET ]], - '/update_profile[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Account\UpdateProfile ::class, [ R::POST]], + '/update_profile[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Account\UpdateProfile::class, [ R::POST]], '/update_profile_image[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Account\UpdateProfileImage::class, [ R::POST]], ], @@ -125,7 +112,7 @@ $apiRoutes = [ '/list[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Lists\Lists::class, [R::GET ]], '/ownerships[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Lists\Ownership::class, [R::GET ]], '/statuses[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Lists\Statuses::class, [R::GET ]], - '/subscriptions[.{extension:json|xml|rss|atom}]' => [Module\Api\Friendica\Lists\Lists::class, [R::GET ]], + '/subscriptions[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Lists\Lists::class, [R::GET ]], '/update[.{extension:json|xml|rss|atom}]' => [Module\Api\Twitter\Lists\Update::class, [ R::POST]], ], @@ -212,7 +199,7 @@ return [ '/accounts/{id:\d+}/note' => [Module\Api\Mastodon\Accounts\Note::class, [ R::POST]], '/accounts/{id:\d+}/remove_from_followers' => [Module\Api\Mastodon\Unimplemented::class, [ R::POST]], // not supported '/accounts/familiar_followers' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not supported - '/accounts/lookup' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // not supported + '/accounts/lookup' => [Module\Api\Mastodon\Accounts\Lookup::class, [R::GET ]], '/accounts/relationships' => [Module\Api\Mastodon\Accounts\Relationships::class, [R::GET ]], '/accounts/search' => [Module\Api\Mastodon\Accounts\Search::class, [R::GET ]], '/accounts/update_credentials' => [Module\Api\Mastodon\Accounts\UpdateCredentials::class, [R::PATCH ]], @@ -255,14 +242,14 @@ return [ '/instance' => [Module\Api\Mastodon\Instance::class, [R::GET ]], '/instance/activity' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo '/instance/domain_blocks' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo - '/instance/extended_description' => [Module\Api\Mastodon\Unimplemented::class, [R::GET ]], // @todo + '/instance/extended_description' => [Module\Api\Mastodon\Instance\ExtendedDescription::class, [R::GET ]], '/instance/peers' => [Module\Api\Mastodon\Instance\Peers::class, [R::GET ]], '/instance/rules' => [Module\Api\Mastodon\Instance\Rules::class, [R::GET ]], '/lists' => [Module\Api\Mastodon\Lists::class, [R::GET, R::POST]], '/lists/{id:\d+}' => [Module\Api\Mastodon\Lists::class, [R::GET, R::PUT, R::DELETE]], '/lists/{id:\d+}/accounts' => [Module\Api\Mastodon\Lists\Accounts::class, [R::GET, R::POST, R::DELETE]], '/markers' => [Module\Api\Mastodon\Markers::class, [R::GET, R::POST]], - '/media/{id:\d+}' => [Module\Api\Mastodon\Media::class, [R::GET, R::PUT ]], + '/media/{id}' => [Module\Api\Mastodon\Media::class, [R::GET, R::PUT ]], '/mutes' => [Module\Api\Mastodon\Mutes::class, [R::GET ]], '/notifications' => [Module\Api\Mastodon\Notifications::class, [R::GET ]], '/notifications/{id:\d+}' => [Module\Api\Mastodon\Notifications::class, [R::GET ]], @@ -309,7 +296,7 @@ return [ '/tags/{hashtag}/unfollow' => [Module\Api\Mastodon\Tags\Unfollow::class, [ R::POST]], '/timelines/direct' => [Module\Api\Mastodon\Timelines\Direct::class, [R::GET ]], '/timelines/home' => [Module\Api\Mastodon\Timelines\Home::class, [R::GET ]], - '/timelines/list/{id:\d+}' => [Module\Api\Mastodon\Timelines\ListTimeline::class, [R::GET ]], + '/timelines/list/{id}' => [Module\Api\Mastodon\Timelines\ListTimeline::class, [R::GET ]], '/timelines/public' => [Module\Api\Mastodon\Timelines\PublicTimeline::class, [R::GET ]], '/timelines/tag/{hashtag}' => [Module\Api\Mastodon\Timelines\Tag::class, [R::GET ]], '/trends' => [Module\Api\Mastodon\Trends\Tags::class, [R::GET ]], @@ -425,7 +412,7 @@ return [ '/credits' => [Module\Credits::class, [R::GET]], '/delegation' => [Module\User\Delegation::class, [R::GET, R::POST]], '/dfrn_notify[/{nickname}]' => [Module\DFRN\Notify::class, [ R::POST]], - '/dfrn_poll/{nickname}' => [Module\DFRN\Poll::class, [R::GET]], + '/dfrn_poll/{nickname}' => [Module\Feed::class, [R::GET]], '/dirfind' => [Module\Search\Directory::class, [R::GET]], '/directory' => [Module\Directory::class, [R::GET]], @@ -447,7 +434,7 @@ return [ '/filed' => [Module\Search\Filed::class, [R::GET]], '/filer[/{id:\d+}]' => [Module\Filer\SaveTag::class, [R::GET]], '/filerm/{id:\d+}' => [Module\Filer\RemoveTag::class, [R::GET, R::POST]], - '/follow_confirm' => [Module\FollowConfirm::class, [R::GET, R::POST]], + '/follow_confirm' => [Module\FollowConfirm::class, [R::POST]], '/followers/{nickname}' => [Module\ActivityPub\Followers::class, [R::GET]], '/following/{nickname}' => [Module\ActivityPub\Following::class, [R::GET]], '/friendica[/{format:json}]' => [Module\Friendica::class, [R::GET]], @@ -481,7 +468,9 @@ return [ '/activity/{verb}' => [Module\Item\Activity::class, [ R::POST]], '/follow' => [Module\Item\Follow::class, [ R::POST]], '/ignore' => [Module\Item\Ignore::class, [ R::POST]], + '/language' => [Module\Item\Language::class, [R::GET]], '/pin' => [Module\Item\Pin::class, [ R::POST]], + '/searchtext' => [Module\Item\Searchtext::class, [R::GET]], '/star' => [Module\Item\Star::class, [ R::POST]], ], @@ -490,7 +479,8 @@ return [ '/logout' => [Module\Security\Logout::class, [R::GET, R::POST]], '/magic' => [Module\Magic::class, [R::GET]], '/manifest' => [Module\Manifest::class, [R::GET]], - '/friendica.webmanifest' => [Module\Manifest::class, [R::GET]], + '/manifest.json' => [Module\Manifest::class, [R::GET]], + '/friendica.webmanifest' => [Module\Manifest::class, [R::GET]], '/media' => [ '/attachment/browser' => [Module\Media\Attachment\Browser::class, [R::GET]], @@ -520,10 +510,12 @@ return [ '/users/deleted' => [Module\Moderation\Users\Deleted::class, [R::GET ]], '/users/create' => [Module\Moderation\Users\Create::class, [R::GET, R::POST]], ], - '/modexp/{nick}' => [Module\PublicRSAKey::class, [R::GET]], '/newmember' => [Module\Welcome::class, [R::GET]], '/nodeinfo/1.0' => [Module\NodeInfo110::class, [R::GET]], '/nodeinfo/2.0' => [Module\NodeInfo120::class, [R::GET]], + '/nodeinfo/2.0.json' => [Module\NodeInfo120::class, [R::GET]], + '/nodeinfo/2.1' => [Module\NodeInfo121::class, [R::GET]], + '/nodeinfo/2.2' => [Module\NodeInfo122::class, [R::GET]], '/nocircle' => [Module\Circle::class, [R::GET]], '/noscrape' => [ @@ -558,40 +550,26 @@ return [ '/objects/{guid}[/{activity}]' => [Module\ActivityPub\Objects::class, [R::GET]], - '/oembed' => [ - '/b2h' => [Module\Oembed::class, [R::GET]], - '/h2b' => [Module\Oembed::class, [R::GET]], - '/{hash}' => [Module\Oembed::class, [R::GET]], - ], '/outbox/{nickname}' => [Module\ActivityPub\Outbox::class, [R::GET, R::POST]], '/owa' => [Module\Owa::class, [R::GET]], '/openid' => [Module\Security\OpenID::class, [R::GET]], '/opensearch' => [Module\OpenSearch::class, [R::GET]], '/parseurl' => [Module\ParseUrl::class, [R::GET]], - '/permission/tooltip/{type}/{id:\d+}' => [Module\PermissionTooltip::class, [R::GET]], + '/permission/tooltip/{type}/{id:\d+}' => [Module\Privacy\PermissionTooltip::class, [R::GET]], '/photo' => [ '/{size:thumb_small|scaled_full}_{name}' => [Module\Photo::class, [R::GET]], '/{name}' => [Module\Photo::class, [R::GET]], '/{type}/{id:\d+}' => [Module\Photo::class, [R::GET]], '/{type:contact|header}/{guid}' => [Module\Photo::class, [R::GET]], - // User Id Fallback, to remove after version 2021.12 - '/{type}/{uid_ext:\d+\..*}' => [Module\Photo::class, [R::GET]], '/{type}/{nickname_ext}' => [Module\Photo::class, [R::GET]], - // Contact Id Fallback, to remove after version 2021.12 '/{type:contact|header}/{customsize:\d+}/{contact_id:\d+}' => [Module\Photo::class, [R::GET]], '/{type:contact|header}/{customsize:\d+}/{guid}' => [Module\Photo::class, [R::GET]], '/{type}/{customsize:\d+}/{id:\d+}' => [Module\Photo::class, [R::GET]], - // User Id Fallback, to remove after version 2021.12 - '/{type}/{customsize:\d+}/{uid_ext:\d+\..*}' => [Module\Photo::class, [R::GET]], '/{type}/{customsize:\d+}/{nickname_ext}' => [Module\Photo::class, [R::GET]], ], - // Kept for backwards-compatibility - // @TODO remove by version 2023.12 - '/photos/{nickname}' => [Module\Profile\Photos::class, [R::GET]], - '/ping' => [Module\Notifications\Ping::class, [R::GET]], '/post' => [ @@ -608,21 +586,8 @@ return [ '/u/{nickname}' => $profileRoutes, '/~{nickname}' => $profileRoutes, - '/proxy' => [ - '[/]' => [Module\Proxy::class, [R::GET]], - '/{url}' => [Module\Proxy::class, [R::GET]], - '/{sub1}/{url}' => [Module\Proxy::class, [R::GET]], - '/{sub1}/{sub2}/{url}' => [Module\Proxy::class, [R::GET]], - ], - - // OStatus stack modules - '/ostatus/repair' => [Module\OStatus\Repair::class, [R::GET ]], '/ostatus/subscribe' => [Module\OStatus\Subscribe::class, [R::GET ]], '/poco' => [Module\User\PortableContacts::class, [R::GET ]], - '/pubsub' => [Module\OStatus\PubSub::class, [R::GET, R::POST]], - '/pubsub/{nickname}[/{cid:\d+}]' => [Module\OStatus\PubSub::class, [R::GET, R::POST]], - '/pubsubhubbub[/{nickname}]' => [Module\OStatus\PubSubHubBub::class, [ R::POST]], - '/salmon[/{nickname}]' => [Module\OStatus\Salmon::class, [ R::POST]], '/search' => [ '[/]' => [Module\Search\Index::class, [R::GET ]], @@ -637,6 +602,8 @@ return [ '/{type:users}/{guid}' => [Module\Diaspora\Receive::class, [ R::POST]], ], + '/remote_follow/{nickname}' => [Module\Profile\RemoteFollow::class, [R::GET, R::POST]], + '/security' => [ '/password_too_long' => [Module\Security\PasswordTooLong::class, [R::GET, R::POST]], ], @@ -657,6 +624,7 @@ return [ '/delegation[/{action}/{user_id}]' => [Module\Settings\Delegation::class, [R::GET, R::POST]], '/display' => [Module\Settings\Display::class, [R::GET, R::POST]], '/features' => [Module\Settings\Features::class, [R::GET, R::POST]], + '/importcontacts' => [Module\Settings\ContactImport::class, [R::GET, R::POST]], '/oauth' => [Module\Settings\OAuth::class, [R::GET, R::POST]], '/profile' => [ '[/]' => [Module\Settings\Profile\Index::class, [R::GET, R::POST]], @@ -674,10 +642,12 @@ return [ ], ], + '/stats' => [Module\Stats::class, [R::GET]], + '/stats/caching' => [Module\StatsCaching::class, [R::GET]], + '/network' => [ '[/{content}]' => [Module\Conversation\Network::class, [R::GET]], '/archive/{from:\d\d\d\d-\d\d-\d\d}[/{to:\d\d\d\d-\d\d-\d\d}]' => [Module\Conversation\Network::class, [R::GET]], - '/group/{contact_id:\d+}' => [Module\Conversation\Network::class, [R::GET]], '/circle/{circle_id:\d+}' => [Module\Conversation\Network::class, [R::GET]], ], @@ -690,6 +660,13 @@ return [ '/toggle_mobile' => [Module\ToggleMobile::class, [R::GET]], '/tos' => [Module\Tos::class, [R::GET]], + '/ping_network' => [ + '[/]' => [Module\Ping\Network::class, [R::GET]], + '/archive/{from:\d\d\d\d-\d\d-\d\d}[/{to:\d\d\d\d-\d\d-\d\d}]' => [Module\Ping\Network::class, [R::GET]], + '/group/{contact_id:\d+}' => [Module\Ping\Network::class, [R::GET]], + '/circle/{circle_id:\d+}' => [Module\Ping\Network::class, [R::GET]], + ], + '/update_channel[/{content}]' => [Module\Update\Channel::class, [R::GET]], '/update_community[/{content}]' => [Module\Update\Community::class, [R::GET]], diff --git a/static/security-data-integrity-v1.jsonld b/static/security-data-integrity-v1.jsonld index 28116e761f..267c90635f 100644 --- a/static/security-data-integrity-v1.jsonld +++ b/static/security-data-integrity-v1.jsonld @@ -35,23 +35,19 @@ }, "assertionMethod": { "@id": "https://w3id.org/security#assertionMethod", - "@type": "@id", - "@container": "@set" + "@type": "@id" }, "authentication": { "@id": "https://w3id.org/security#authenticationMethod", - "@type": "@id", - "@container": "@set" + "@type": "@id" }, "capabilityInvocation": { "@id": "https://w3id.org/security#capabilityInvocationMethod", - "@type": "@id", - "@container": "@set" + "@type": "@id" }, "capabilityDelegation": { "@id": "https://w3id.org/security#capabilityDelegationMethod", - "@type": "@id", - "@container": "@set" + "@type": "@id" }, "keyAgreement": { "@id": "https://w3id.org/security#keyAgreementMethod", diff --git a/static/settings.config.php b/static/settings.config.php index 578fdd268c..59507690e3 100644 --- a/static/settings.config.php +++ b/static/settings.config.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * This file declares the default values for the admin settings of Friendica. * @@ -54,7 +42,11 @@ return [ // adjust_poll_frequency (Boolean) // Automatically detect and set the best feed poll frequency. - 'adjust_poll_frequency' => false, + 'adjust_poll_frequency' => true, + + // allow_relay_channels (Boolean) + // Allow Users to set remote_self + 'allow_relay_channels' => true, // allowed_themes (Comma-separated list) // Themes users can change to in their settings. @@ -82,7 +74,7 @@ return [ // curl_timeout (Integer) // Value is in seconds. Set to 0 for unlimited (not recommended). - 'curl_timeout' => 60, + 'curl_timeout' => 60, // dbclean (Boolean) // Remove old remote items, orphaned database records and old content from some other helper tables. @@ -166,6 +158,10 @@ return [ // Has to be one of these values: emergency, alert, critical, error, warning, notice, info, debug 'loglevel' => 'notice', + // max_author_posts_community_page (Integer) + // The maximum number of posts on the local community page from a single author. + 'max_author_posts_community_page' => 0, + // max_image_length (Integer) // An alternate way of limiting picture upload sizes. // Specify the maximum pixel length that pictures are allowed to be (for non-square pictures, it will apply to the longest side). @@ -173,6 +169,10 @@ return [ // If you don't want to set a maximum length, set to -1. 'max_image_length' => -1, + // max_server_posts_community_page (Integer) + // The maximum number of posts on the global community page from a single server. + 'max_server_posts_community_page' => 0, + // maximagesize (Integer) // Maximum size in bytes of an uploaded photo. 'maximagesize' => 800000, @@ -225,9 +225,9 @@ return [ // Minimum value for the language detection quality for relay posts. The value must be between 0 and 1. 'relay_language_quality' => 0, - // proxify_content (Boolean) - // Use the proxy functionality for fetching external content - 'proxify_content' => true, + // relay_max_tags (Integer) + // Maximum amount of tags in a post before it is rejected as spam. + 'relay_max_tags' => 20, // relay_directly (Boolean) // Directly transmit content to relay subscribers without using a relay server @@ -257,6 +257,10 @@ return [ // When activated, only public contacts will be activated regularly that are used for example in items or tags. 'update_active_contacts' => false, + // update_known_contacts (Boolean) + // When activated, only public contacts will be activated regularly that are in a contact list of a local user. + 'update_known_contacts' => false, + // url (String) // The fully-qualified URL of this Friendica node. // Used by the worker in a non-HTTP execution environment. diff --git a/static/socialweb-webfinger.jsonld b/static/socialweb-webfinger.jsonld new file mode 100644 index 0000000000..7c0fd9e742 --- /dev/null +++ b/static/socialweb-webfinger.jsonld @@ -0,0 +1,10 @@ +{ + "@context": { + "wf": "https://purl.archive.org/socialweb/webfinger#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "webfinger": { + "@id": "wf:webfinger", + "@type": "xsd:string" + } + } +} \ No newline at end of file diff --git a/static/strategies.config.php b/static/strategies.config.php index 18872dc1d4..fb2c6c67a9 100644 --- a/static/strategies.config.php +++ b/static/strategies.config.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later use Friendica\Core\Cache; use Friendica\Core\Hooks\Util\StrategiesFileManager; diff --git a/tests/src/Module/Api/ApiTest.php b/tests/ApiTestCase.php similarity index 77% rename from tests/src/Module/Api/ApiTest.php rename to tests/ApiTestCase.php index 812993aa98..45a84c280a 100644 --- a/tests/src/Module/Api/ApiTest.php +++ b/tests/ApiTestCase.php @@ -1,49 +1,31 @@ . - * - */ -namespace Friendica\Test\src\Module\Api; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; -use Friendica\App; use Friendica\Capabilities\ICanCreateResponses; -use Friendica\Core\Addon; +use Friendica\Core\Addon\AddonHelper; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Hook; -use Friendica\Database\Database; +use Friendica\Core\Hooks\HookEventBridge; use Friendica\DI; use Friendica\Module\Special\HTTPException; use Friendica\Security\Authentication; use Friendica\Security\BasicAuth; -use Friendica\Test\FixtureTest; -use Friendica\Test\Util\AppDouble; use Friendica\Test\Util\AuthenticationDouble; use Friendica\Test\Util\AuthTestConfig; -use Mockery\MockInterface; use Psr\Http\Message\ResponseInterface; -abstract class ApiTest extends FixtureTest +abstract class ApiTestCase extends FixtureTestCase { // User data that the test database is populated with const SELF_USER = [ 'id' => 42, - 'name' => 'Self contact', + 'name' => 'Test user', 'nick' => 'selfcontact', 'nurl' => 'http://localhost/profile/selfcontact' ]; @@ -143,7 +125,7 @@ abstract class ApiTest extends FixtureTest file_put_contents( $tmpFile, base64_decode( - // Empty 1x1 px PNG image + // Empty 1x1 px PNG image 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==' ) ); @@ -175,11 +157,15 @@ abstract class ApiTest extends FixtureTest $this->dice = $this->dice ->addRule(Authentication::class, ['instanceOf' => AuthenticationDouble::class, 'shared' => true]) - ->addRule(App::class, ['instanceOf' => AppDouble::class, 'shared' => true]); + ; DI::init($this->dice); - // Manual override to bypass API authentication - DI::app()->setIsLoggedIn(true); + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = DI::eventDispatcher(); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } $this->httpExceptionMock = $this->dice->create(HTTPException::class); @@ -204,13 +190,13 @@ abstract class ApiTest extends FixtureTest public function installAuthTest() { $addon = 'authtest'; - $addon_file_path = __DIR__ . '/../../../Util/authtest/authtest.php'; + $addon_file_path = __DIR__ . '/Util/authtest/authtest.php'; $t = @filemtime($addon_file_path); @include_once($addon_file_path); if (function_exists($addon . '_install')) { $func = $addon . '_install'; - $func(DI::app()); + $func(); } /** @var $config IManageConfigValues */ @@ -223,7 +209,7 @@ abstract class ApiTest extends FixtureTest 'plugin_admin' => function_exists($addon . '_addon_admin'), ]); - Addon::loadAddons(); + $this->dice->create(AddonHelper::class)->loadAddons(); Hook::loadHooks(); } } diff --git a/tests/CacheLockTestCase.php b/tests/CacheLockTestCase.php new file mode 100644 index 0000000000..1599391ece --- /dev/null +++ b/tests/CacheLockTestCase.php @@ -0,0 +1,26 @@ +getCache()->getStats()), array_keys($this->instance->getCacheStats())); + } +} diff --git a/tests/src/Core/Cache/CacheTest.php b/tests/CacheTestCase.php similarity index 85% rename from tests/src/Core/Cache/CacheTest.php rename to tests/CacheTestCase.php index 775a29374c..c3db14b274 100644 --- a/tests/src/Core/Cache/CacheTest.php +++ b/tests/CacheTestCase.php @@ -1,33 +1,17 @@ . - * - */ -namespace Friendica\Test\src\Core\Cache; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; use Friendica\Core\Cache\Capability\ICanCache; -use Friendica\Core\Cache\Capability\ICanCacheInMemory; -use Friendica\Core\Cache\Type\AbstractCache; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\PidFile; -abstract class CacheTest extends MockedTest +abstract class CacheTestCase extends MockedTestCase { /** * @var int Start time of the mock (used for time operations) diff --git a/tests/ConsoleTestCase.php b/tests/ConsoleTestCase.php new file mode 100644 index 0000000000..05febcd735 --- /dev/null +++ b/tests/ConsoleTestCase.php @@ -0,0 +1,44 @@ +execute(); + $returnStr = Intercept::$cache; + Intercept::reset(); + + return $returnStr; + } +} diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php deleted file mode 100644 index 03bebb7b54..0000000000 --- a/tests/DatabaseTest.php +++ /dev/null @@ -1,44 +0,0 @@ -. - * - */ - -namespace Friendica\Test; - -/** - * Abstract class used by tests that need a database. - */ -abstract class DatabaseTest extends MockedTest -{ - use DatabaseTestTrait; - - protected function setUp(): void - { - $this->setUpDb(); - - parent::setUp(); - } - - protected function tearDown(): void - { - $this->tearDownDb(); - - parent::tearDown(); - } -} diff --git a/tests/DatabaseTestCase.php b/tests/DatabaseTestCase.php new file mode 100644 index 0000000000..b061a7f21a --- /dev/null +++ b/tests/DatabaseTestCase.php @@ -0,0 +1,30 @@ +setUpDb(); + + parent::setUp(); + } + + protected function tearDown(): void + { + $this->tearDownDb(); + + parent::tearDown(); + } +} diff --git a/tests/DatabaseTestTrait.php b/tests/DatabaseTestTrait.php index 6d3a75ab1b..8db8cb4d9b 100644 --- a/tests/DatabaseTestTrait.php +++ b/tests/DatabaseTestTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test; @@ -35,7 +21,7 @@ trait DatabaseTestTrait // Rollbacks every DB usage (in case the test couldn't call tearDown) StaticDatabase::statRollback(); // Rollback the first, outer transaction just 2 be sure - StaticDatabase::getGlobConnection()->rollBack(); + StaticDatabase::getGlobConnection()->rollback(); // Start the first, outer transaction StaticDatabase::getGlobConnection()->beginTransaction(); } diff --git a/tests/DiceHttpMockHandlerTrait.php b/tests/DiceHttpMockHandlerTrait.php index 56250817d6..52be7c84e1 100644 --- a/tests/DiceHttpMockHandlerTrait.php +++ b/tests/DiceHttpMockHandlerTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test; diff --git a/tests/FixtureTest.php b/tests/FixtureTest.php deleted file mode 100644 index e2e8ad725a..0000000000 --- a/tests/FixtureTest.php +++ /dev/null @@ -1,59 +0,0 @@ -. - * - * FixtureTest class. - */ - -namespace Friendica\Test; - -use Dice\Dice; -use Friendica\App\Arguments; -use Friendica\App\Router; -use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Core\Config\Factory\Config; -use Friendica\Core\Config\Util\ConfigFileManager; -use Friendica\Core\Session\Capability\IHandleSessions; -use Friendica\Core\Session\Type\Memory; -use Friendica\Database\Database; -use Friendica\Database\DBStructure; -use Friendica\DI; -use Friendica\Test\Util\Database\StaticDatabase; -use Friendica\Test\Util\VFSTrait; - -/** - * Parent class for test cases requiring fixtures - */ -abstract class FixtureTest extends MockedTest -{ - use FixtureTestTrait; - - protected function setUp(): void - { - parent::setUp(); - - $this->setUpFixtures(); - } - - protected function tearDown(): void - { - $this->tearDownFixtures(); - - parent::tearDown(); - } -} diff --git a/tests/FixtureTestCase.php b/tests/FixtureTestCase.php new file mode 100644 index 0000000000..5fa751c871 --- /dev/null +++ b/tests/FixtureTestCase.php @@ -0,0 +1,30 @@ +setUpFixtures(); + } + + protected function tearDown(): void + { + $this->tearDownFixtures(); + + parent::tearDown(); + } +} diff --git a/tests/FixtureTestTrait.php b/tests/FixtureTestTrait.php index 4bb750bf9d..8c4508eeb8 100644 --- a/tests/FixtureTestTrait.php +++ b/tests/FixtureTestTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test; @@ -58,7 +44,7 @@ trait FixtureTestTrait ->addRules(include __DIR__ . '/../static/dependencies.config.php') ->addRule(ConfigFileManager::class, [ 'instanceOf' => Config::class, - 'call' => [['createConfigFileManager', [$this->root->url(), $server,], Dice::CHAIN_CALL]]]) + 'call' => [['createConfigFileManager', [$this->root->url(), $this->root->url() . '/addon', $server,], Dice::CHAIN_CALL]]]) ->addRule(Database::class, ['instanceOf' => StaticDatabase::class, 'shared' => true]) ->addRule(IHandleSessions::class, ['instanceOf' => Memory::class, 'shared' => true, 'call' => null]) ->addRule(Arguments::class, [ diff --git a/tests/src/Core/Lock/LockTest.php b/tests/LockTestCase.php similarity index 82% rename from tests/src/Core/Lock/LockTest.php rename to tests/LockTestCase.php index 9480188842..92abf0ca7b 100644 --- a/tests/src/Core/Lock/LockTest.php +++ b/tests/LockTestCase.php @@ -1,42 +1,24 @@ . - * - */ -namespace Friendica\Test\src\Core\Lock; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; use Friendica\Core\Lock\Capability\ICanLock; -use Friendica\Test\MockedTest; -abstract class LockTest extends MockedTest +abstract class LockTestCase extends MockedTestCase { /** - * @var int Start time of the mock (used for time operations) + * Start time of the mock (used for time operations) */ - protected $startTime = 1417011228; + protected int $startTime = 1417011228; + protected ICanLock $instance; - /** - * @var ICanLock - */ - protected $instance; + abstract protected function getInstance(): ICanLock; - abstract protected function getInstance(); protected function setUp(): void { @@ -219,4 +201,6 @@ abstract class LockTest extends MockedTest self::assertFalse($this->instance->isLocked('wrongLock')); self::assertFalse($this->instance->release('wrongLock')); } + + } diff --git a/tests/src/Core/Logger/LoggerDataTrait.php b/tests/LoggerDataTrait.php similarity index 55% rename from tests/src/Core/Logger/LoggerDataTrait.php rename to tests/LoggerDataTrait.php index f3f1770afd..229a1997ea 100644 --- a/tests/src/Core/Logger/LoggerDataTrait.php +++ b/tests/LoggerDataTrait.php @@ -1,25 +1,11 @@ . - * - */ -namespace Friendica\Test\src\Core\Logger; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; trait LoggerDataTrait { diff --git a/tests/src/Core/Logger/AbstractLoggerTest.php b/tests/LoggerTestCase.php similarity index 82% rename from tests/src/Core/Logger/AbstractLoggerTest.php rename to tests/LoggerTestCase.php index 4605f6f75a..a31ba5d6a9 100644 --- a/tests/src/Core/Logger/AbstractLoggerTest.php +++ b/tests/LoggerTestCase.php @@ -1,34 +1,20 @@ . - * - */ -namespace Friendica\Test\src\Core\Logger; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Test\MockedTest; use Friendica\Core\Logger\Util\Introspection; +use Friendica\Test\MockedTestCase; use Mockery\MockInterface; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; -abstract class AbstractLoggerTest extends MockedTest +abstract class LoggerTestCase extends MockedTestCase { use LoggerDataTrait; diff --git a/tests/src/Core/Cache/MemoryCacheTest.php b/tests/MemoryCacheTestCase.php similarity index 79% rename from tests/src/Core/Cache/MemoryCacheTest.php rename to tests/MemoryCacheTestCase.php index 6704bcb11b..7563ff701b 100644 --- a/tests/src/Core/Cache/MemoryCacheTest.php +++ b/tests/MemoryCacheTestCase.php @@ -1,30 +1,16 @@ . - * - */ -namespace Friendica\Test\src\Core\Cache; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; use Exception; use Friendica\Core\Cache\Capability\ICanCacheInMemory; -abstract class MemoryCacheTest extends CacheTest +abstract class MemoryCacheTestCase extends CacheTestCase { /** * @var \Friendica\Core\Cache\Capability\ICanCacheInMemory diff --git a/tests/MockedTest.php b/tests/MockedTest.php deleted file mode 100644 index 18aee466e2..0000000000 --- a/tests/MockedTest.php +++ /dev/null @@ -1,37 +0,0 @@ -. - * - */ - -namespace Friendica\Test; - -use PHPUnit\Framework\TestCase; - -/** - * This class verifies each mock after each call - */ -abstract class MockedTest extends TestCase -{ - protected function tearDown() : void - { - \Mockery::close(); - - parent::tearDown(); - } -} diff --git a/tests/MockedTestCase.php b/tests/MockedTestCase.php new file mode 100644 index 0000000000..3fbb1b906a --- /dev/null +++ b/tests/MockedTestCase.php @@ -0,0 +1,23 @@ +. - * - */ -namespace Friendica\Test\src\Core\PConfig; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use Friendica\Core\PConfig\Type\AbstractPConfigValues; use Friendica\Core\PConfig\Repository\PConfig as PConfigModel; use Friendica\Core\PConfig\ValueObject\Cache; -use Friendica\Test\MockedTest; use Mockery; use Mockery\MockInterface; -abstract class PConfigTest extends MockedTest +abstract class PConfigTestCase extends MockedTestCase { use ArraySubsetAsserts; diff --git a/tests/README.md b/tests/README.md index 2d515ce452..883e114d54 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,13 +1,83 @@ # Using the Friendica tests -## Install PHPUnit +## Install Tools -Please use [setup-phpunit.sh](https://github.com/friendica/friendica/bin/dev/setup-phpunit.sh) to install the necessary PHPUnit version. -It will temporarily install the `phpunit` phar file into the `bin/` subdirectory +You need to install the following software: +* PHP +* MySQL or Mariadb (the latter is preferred) -Currently, Friendica uses PHPUnit 8. +For example in Ubuntu you can run: -## Supported PHP versions of these tests +``` +sudo apt install mariadb-server php +``` -The Unit-Tests of Friendica requires at least PHP 7.2. +## Install PHP extensions + +The following extensions must be installed: + +* MySQL +* Curl +* GD +* XML +* DOM +* SimpleXML +* Intl +* Multi-precision +* Multi-byte string + +For example in Ubuntu: + +``` +sudo apt install php-mysql php-curl php-gd php-xml php-intl php-gmp php-mbstring +``` + +## Create Local Database + +The default database name is `test`, username `friendica`, password +`friendica`. These can be overridden using environment variables +`DATABASE_NAME`, `DATABASE_USER`, `DATABASE_HOST`, and +`DATABASE_PASSWORD`. Whatever settings you choose, you must give the +corresponding user the necessary privileges to create and destroy the +chosen database. + +``` +GRANT ALL PRIVILEGES ON test.* TO 'friendica'@'localhost' IDENTIFIED BY 'friendica' WITH GRANT OPTION; +GRANT CREATE, DROP ON test.* TO 'friendica'@'localhost'; +``` + +## Use Docker Database + +Instead of using a local database, you can also use a database running in a docker container. + +TODO this section needs to be filled in with working examples. + +## Running Tests + +You can then run the tests using the `autotest.sh` script. You should +specify the type of database as an argument, either `mysql` or +`mariadb`: + +``` +bin/dev/autotest.sh mariadb +``` + +You can also run just one particular file of tests: + +``` +bin/dev/autotest.sh mariadb src/Util/ImagesTest.php +``` + +Example output of tests passing: + +``` +OK (2 tests, 2 assertions) +``` + +Failed tests look like this. Examine the output before this to see which tests failed. + +``` +FAILURES! +Tests: 2, Assertions: 2, Failures: 1. +``` diff --git a/tests/StorageConfigTestCase.php b/tests/StorageConfigTestCase.php new file mode 100644 index 0000000000..4b06a8ec59 --- /dev/null +++ b/tests/StorageConfigTestCase.php @@ -0,0 +1,29 @@ +getInstance(); + + $this->assertOption($instance); + } +} diff --git a/tests/src/Core/Storage/StorageTest.php b/tests/StorageTestCase.php similarity index 66% rename from tests/src/Core/Storage/StorageTest.php rename to tests/StorageTestCase.php index d22133d040..8a2ffda446 100644 --- a/tests/src/Core/Storage/StorageTest.php +++ b/tests/StorageTestCase.php @@ -1,32 +1,18 @@ . - * - */ -namespace Friendica\Test\src\Core\Storage; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test; use Friendica\Core\Storage\Capability\ICanReadFromStorage; use Friendica\Core\Storage\Capability\ICanWriteToStorage; use Friendica\Core\Storage\Exception\ReferenceStorageException; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -abstract class StorageTest extends MockedTest +abstract class StorageTestCase extends MockedTestCase { /** @return ICanWriteToStorage */ abstract protected function getInstance(); diff --git a/tests/Unit/AppTest.php b/tests/Unit/AppTest.php new file mode 100644 index 0000000000..38c53423dc --- /dev/null +++ b/tests/Unit/AppTest.php @@ -0,0 +1,27 @@ +createMock(Container::class); + $container->expects($this->never())->method('create'); + + $app = App::fromContainer($container); + + $this->assertInstanceOf(App::class, $app); + } +} diff --git a/tests/Unit/Core/Addon/AddonInfoTest.php b/tests/Unit/Core/Addon/AddonInfoTest.php new file mode 100644 index 0000000000..7a342bc059 --- /dev/null +++ b/tests/Unit/Core/Addon/AddonInfoTest.php @@ -0,0 +1,166 @@ +assertInstanceOf(AddonInfo::class, AddonInfo::fromString('addonId', '')); + } + + public static function getStringData(): array + { + return [ + 'minimal' => [ + 'test', + '', + ['id' => 'test'], + ], + 'without-author' => [ + 'test', + << 'test', + 'name' => 'Test Addon', + 'description' => 'adds awesome features to friendica', + + 'maintainers' => [ + ['name' => 'Robin'], + ], + 'version' => '100.4.50-beta.5', + 'status' => 'beta', + ], + ], + 'without-maintainer' => [ + 'test', + << 'test', + 'name' => 'Test Addon', + 'description' => 'adds awesome features to friendica', + 'authors' => [ + ['name' => 'Sam'], + ], + 'version' => '100.4.50-beta.5', + 'status' => 'beta', + ], + ], + 'complete' => [ + 'test', + << + * Maintainer: Robin + * Maintainer: Robin With Profile + * Status: beta + * Ignore: The "ignore" key is unsupported and will be ignored + */ + TEXT, + [ + 'id' => 'test', + 'name' => 'Test Addon', + 'description' => 'adds awesome features to friendica', + 'authors' => [ + ['name' => 'Sam'], + ['name' => 'Sam With Mail', 'link' => 'mail@example.org'], + ], + 'maintainers' => [ + ['name' => 'Robin'], + ['name' => 'Robin With Profile', 'link' => 'https://example.org/profile/robin'], + ], + 'version' => '100.4.50-beta.5', + 'status' => 'beta', + ], + ], + ]; + } + + /** + * @dataProvider getStringData + */ + public function testFromStringReturnsCorrectValues(string $addonId, string $raw, array $expected): void + { + $this->assertAddonInfoData($expected, AddonInfo::fromString($addonId, $raw)); + } + + public function testFromArrayCreatesObject(): void + { + $this->assertInstanceOf(AddonInfo::class, AddonInfo::fromArray([])); + } + + public function testGetterReturningCorrectValues(): void + { + $data = [ + 'id' => 'test', + 'name' => 'Test-Addon', + 'description' => 'This is an addon for tests', + 'authors' => [['name' => 'Sam']], + 'maintainers' => [['name' => 'Sam', 'link' => 'https://example.com']], + 'version' => '0.1', + 'status' => 'In Development', + ]; + + $this->assertAddonInfoData($data, AddonInfo::fromArray($data)); + } + + private function assertAddonInfoData(array $expected, AddonInfo $info): void + { + $expected = array_merge( + [ + 'id' => '', + 'name' => '', + 'description' => '', + 'authors' => [], + 'maintainers' => [], + 'version' => '', + 'status' => '', + ], + $expected + ); + + $data = [ + 'id' => $info->getId(), + 'name' => $info->getName(), + 'description' => $info->getDescription(), + 'authors' => $info->getAuthors(), + 'maintainers' => $info->getMaintainers(), + 'version' => $info->getVersion(), + 'status' => $info->getStatus(), + ]; + + $this->assertSame($expected, $data); + } +} diff --git a/tests/Unit/Core/Addon/AddonManagerHelperTest.php b/tests/Unit/Core/Addon/AddonManagerHelperTest.php new file mode 100644 index 0000000000..882082ceb0 --- /dev/null +++ b/tests/Unit/Core/Addon/AddonManagerHelperTest.php @@ -0,0 +1,472 @@ + [ + 'helloaddon.php' => << + */ + PHP, + ] + ]); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $info = $addonManagerHelper->getAddonInfo('helloaddon'); + + $this->assertInstanceOf(AddonInfo::class, $info); + + $this->assertEquals('Hello Addon', $info->getName()); + } + + public function testGetAddonInfoThrowsInvalidAddonException(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => <<url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(InvalidAddonException::class); + $this->expectExceptionMessage('Could not find valid comment block in addon file:'); + + $addonManagerHelper->getAddonInfo('helloaddon'); + } + + public function testEnabledAddons(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1738760499, + 'admin' => false, + ], + ]); + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, []); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame([], $addonManagerHelper->getEnabledAddons()); + $this->assertFalse($addonManagerHelper->isAddonEnabled('helloaddon')); + + $addonManagerHelper->loadAddons(); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); + $this->assertTrue($addonManagerHelper->isAddonEnabled('helloaddon')); + } + + public function testGetVisibleEnabledAddons(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1738760499, + 'admin' => false, + ], + ]); + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, []); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getVisibleEnabledAddons()); + } + + public function testGetEnabledAddonsWithAdminSettings(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1738760499, + 'admin' => false, + ], + 'addonwithadminsettings' => [ + 'last_update' => 1738760499, + 'admin' => true, + ], + ]); + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, []); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame(['addonwithadminsettings'], $addonManagerHelper->getEnabledAddonsWithAdminSettings()); + } + + public function testGetAvailableAddons(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => << + */ + PHP, + ], + 'invalidaddon' => [ + 'invalidaddon.php' => 'This addon should not be loaded, because it does not contain a valid comment section.', + ], + '.hidden' => [ + '.hidden.php' => 'This folder should be ignored', + ] + ]); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getAvailableAddons()); + } + + public function testInstallAddonIncludesAddonFile(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon file loaded'); + + $addonManagerHelper->installAddon('helloaddon'); + } + + public function testInstallAddonCallsInstallFunction(): void + { + // We need a unique name for the addon to avoid conflicts + // with other tests that may define the same install function. + $addonName = __FUNCTION__; + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + $addonName => [ + $addonName . '.php' => <<url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon installed'); + + $addonManagerHelper->installAddon($addonName); + } + + public function testInstallAddonUpdatesConfig(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'getChild('helloaddon/helloaddon.php')->lastModified(1234567890); + + $config = $this->createMock(IManageConfigValues::class); + $config->expects($this->once())->method('set')->with( + 'addons', + 'helloaddon', + ['last_update' => 1234567890, 'admin' => false] + ); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->installAddon('helloaddon'); + } + + public function testInstallAddonEnablesAddon(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->assertSame([], $addonManagerHelper->getEnabledAddons()); + + $this->assertTrue($addonManagerHelper->installAddon('helloaddon')); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); + } + public function testUninstallAddonIncludesAddonFile(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon file loaded'); + + $addonManagerHelper->uninstallAddon('helloaddon'); + } + + public function testUninstallAddonCallsUninstallFunction(): void + { + // We need a unique name for the addon to avoid conflicts + // with other tests that may define the same install function. + $addonName = __FUNCTION__; + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + $addonName => [ + $addonName . '.php' => <<url(), + $this->createStub(Database::class), + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon uninstalled'); + + $addonManagerHelper->uninstallAddon($addonName); + } + + public function testUninstallAddonRemovesHooksFromDatabase(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'createMock(Database::class); + $database->expects($this->once()) + ->method('delete') + ->with( + 'hook', + ['`file` LIKE ?', '%/helloaddon/helloaddon.php'] + ); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $database, + $this->createStub(IManageConfigValues::class), + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->uninstallAddon('helloaddon'); + } + + public function testUninstallAddonDisablesAddon(): void + { + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + 'helloaddon' => [ + 'helloaddon.php' => 'createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + 'helloaddon' => [ + 'last_update' => 1234567890, + 'admin' => false, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->loadAddons(); + + $this->assertSame(['helloaddon'], $addonManagerHelper->getEnabledAddons()); + + $addonManagerHelper->uninstallAddon('helloaddon'); + + $this->assertSame([], $addonManagerHelper->getEnabledAddons()); + } + + public function testReloadAddonsInstallsAddon(): void + { + // We need a unique name for the addon to avoid conflicts + // with other tests that may define the same install function. + $addonName = __FUNCTION__; + + $root = vfsStream::setup(__FUNCTION__ . '_addons', 0777, [ + $addonName => [ + $addonName . '.php' => <<getChild($addonName . '/' . $addonName . '.php')->lastModified(1234567890); + + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturn([ + $addonName => [ + 'last_update' => 0, + 'admin' => false, + ], + ]); + + $addonManagerHelper = new AddonManagerHelper( + $root->url(), + $this->createStub(Database::class), + $config, + $this->createStub(ICanCache::class), + $this->createStub(LoggerInterface::class), + $this->createStub(Profiler::class) + ); + + $addonManagerHelper->loadAddons(); + + $this->assertSame([$addonName], $addonManagerHelper->getEnabledAddons()); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Addon reinstalled'); + + $addonManagerHelper->reloadAddons(); + } +} diff --git a/tests/Unit/Core/DiceContainerTest.php b/tests/Unit/Core/DiceContainerTest.php new file mode 100644 index 0000000000..85d64de728 --- /dev/null +++ b/tests/Unit/Core/DiceContainerTest.php @@ -0,0 +1,52 @@ + [ + 'dependencies.config.php' => 'url()); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testCreateReturnsObject(): void + { + $root = vfsStream::setup('friendica', null, [ + 'static' => [ + 'dependencies.config.php' => <<< PHP + [ + 'instanceOf' => \Psr\Log\NullLogger::class, + ], + ]; + PHP, + ], + ]); + + $container = DiceContainer::fromBasePath($root->url()); + + $this->assertInstanceOf(NullLogger::class, $container->create(LoggerInterface::class)); + } +} diff --git a/tests/Unit/Core/Hooks/HookEventBridgeTest.php b/tests/Unit/Core/Hooks/HookEventBridgeTest.php new file mode 100644 index 0000000000..436e1435bc --- /dev/null +++ b/tests/Unit/Core/Hooks/HookEventBridgeTest.php @@ -0,0 +1,653 @@ + 'onNamedEvent', + Event::HOME_INIT => 'onNamedEvent', + ConfigLoadedEvent::CONFIG_LOADED => 'onConfigLoadedEvent', + CollectRoutesEvent::COLLECT_ROUTES => 'onCollectRoutesEvent', + ArrayFilterEvent::APP_MENU => 'onArrayFilterEvent', + ArrayFilterEvent::NAV_INFO => 'onArrayFilterEvent', + ArrayFilterEvent::FEATURE_ENABLED => 'onArrayFilterEvent', + ArrayFilterEvent::FEATURE_GET => 'onArrayFilterEvent', + ArrayFilterEvent::PERMISSION_TOOLTIP_CONTENT => 'onPermissionTooltipContentEvent', + ArrayFilterEvent::INSERT_POST_LOCAL_START => 'onArrayFilterEvent', + ArrayFilterEvent::INSERT_POST_LOCAL => 'onInsertPostLocalEvent', + ArrayFilterEvent::INSERT_POST_LOCAL_END => 'onInsertPostLocalEndEvent', + ArrayFilterEvent::INSERT_POST_REMOTE => 'onArrayFilterEvent', + ArrayFilterEvent::INSERT_POST_REMOTE_END => 'onArrayFilterEvent', + ArrayFilterEvent::PREPARE_POST_START => 'onPreparePostStartEvent', + ArrayFilterEvent::PREPARE_POST_FILTER_CONTENT => 'onArrayFilterEvent', + ArrayFilterEvent::PREPARE_POST => 'onArrayFilterEvent', + ArrayFilterEvent::PREPARE_POST_END => 'onArrayFilterEvent', + ArrayFilterEvent::PHOTO_UPLOAD_FORM => 'onArrayFilterEvent', + ArrayFilterEvent::PHOTO_UPLOAD_START => 'onPhotoUploadStartEvent', + ArrayFilterEvent::PHOTO_UPLOAD => 'onArrayFilterEvent', + ArrayFilterEvent::PHOTO_UPLOAD_END => 'onPhotoUploadEndEvent', + ArrayFilterEvent::NETWORK_TO_NAME => 'onArrayFilterEvent', + ArrayFilterEvent::NETWORK_CONTENT_START => 'onArrayFilterEvent', + ArrayFilterEvent::NETWORK_CONTENT_TABS => 'onArrayFilterEvent', + ArrayFilterEvent::PARSE_LINK => 'onArrayFilterEvent', + ArrayFilterEvent::CONVERSATION_START => 'onArrayFilterEvent', + ArrayFilterEvent::FETCH_ITEM_BY_LINK => 'onArrayFilterEvent', + ArrayFilterEvent::ITEM_TAGGED => 'onArrayFilterEvent', + ArrayFilterEvent::DISPLAY_ITEM => 'onArrayFilterEvent', + ArrayFilterEvent::CACHE_ITEM => 'onArrayFilterEvent', + ArrayFilterEvent::CHECK_ITEM_NOTIFICATION => 'onArrayFilterEvent', + ArrayFilterEvent::ENOTIFY => 'onArrayFilterEvent', + ArrayFilterEvent::ENOTIFY_STORE => 'onArrayFilterEvent', + ArrayFilterEvent::ENOTIFY_MAIL => 'onArrayFilterEvent', + ArrayFilterEvent::DETECT_LANGUAGES => 'onArrayFilterEvent', + ArrayFilterEvent::RENDER_LOCATION => 'onArrayFilterEvent', + ArrayFilterEvent::ITEM_PHOTO_MENU => 'onArrayFilterEvent', + ArrayFilterEvent::DIRECTORY_ITEM => 'onArrayFilterEvent', + ArrayFilterEvent::CONTACT_PHOTO_MENU => 'onArrayFilterEvent', + ArrayFilterEvent::PROFILE_SIDEBAR_ENTRY => 'onProfileSidebarEntryEvent', + ArrayFilterEvent::PROFILE_SIDEBAR => 'onArrayFilterEvent', + ArrayFilterEvent::PROFILE_TABS => 'onArrayFilterEvent', + ArrayFilterEvent::PROFILE_SETTINGS_FORM => 'onArrayFilterEvent', + ArrayFilterEvent::PROFILE_SETTINGS_POST => 'onArrayFilterEvent', + ArrayFilterEvent::MODERATION_USERS_TABS => 'onArrayFilterEvent', + ArrayFilterEvent::ACL_LOOKUP_END => 'onArrayFilterEvent', + ArrayFilterEvent::OEMBED_FETCH_END => 'onOembedFetchEndEvent', + ArrayFilterEvent::PAGE_INFO => 'onArrayFilterEvent', + ArrayFilterEvent::SMILEY_LIST => 'onArrayFilterEvent', + ArrayFilterEvent::BBCODE_TO_HTML_START => 'onBbcodeToHtmlEvent', + ArrayFilterEvent::HTML_TO_BBCODE_END => 'onHtmlToBbcodeEvent', + ArrayFilterEvent::BBCODE_TO_MARKDOWN_END => 'onBbcodeToMarkdownEvent', + ArrayFilterEvent::JOT_NETWORKS => 'onArrayFilterEvent', + ArrayFilterEvent::PROTOCOL_SUPPORTS_FOLLOW => 'onArrayFilterEvent', + ArrayFilterEvent::PROTOCOL_SUPPORTS_REVOKE_FOLLOW => 'onArrayFilterEvent', + ArrayFilterEvent::PROTOCOL_SUPPORTS_PROBE => 'onArrayFilterEvent', + ArrayFilterEvent::FOLLOW_CONTACT => 'onArrayFilterEvent', + ArrayFilterEvent::UNFOLLOW_CONTACT => 'onArrayFilterEvent', + ArrayFilterEvent::REVOKE_FOLLOW_CONTACT => 'onArrayFilterEvent', + ArrayFilterEvent::BLOCK_CONTACT => 'onArrayFilterEvent', + ArrayFilterEvent::UNBLOCK_CONTACT => 'onArrayFilterEvent', + ArrayFilterEvent::EDIT_CONTACT_FORM => 'onArrayFilterEvent', + ArrayFilterEvent::EDIT_CONTACT_POST => 'onArrayFilterEvent', + ArrayFilterEvent::AVATAR_LOOKUP => 'onArrayFilterEvent', + ArrayFilterEvent::ACCOUNT_AUTHENTICATE => 'onArrayFilterEvent', + ArrayFilterEvent::ACCOUNT_REGISTER_FORM => 'onArrayFilterEvent', + ArrayFilterEvent::ACCOUNT_REGISTER_POST => 'onArrayFilterEvent', + ArrayFilterEvent::ACCOUNT_REGISTER => 'onAccountRegisterEvent', + ArrayFilterEvent::ACCOUNT_REMOVE => 'onAccountRemoveEvent', + ArrayFilterEvent::EVENT_CREATED => 'onEventCreatedEvent', + ArrayFilterEvent::EVENT_UPDATED => 'onEventUpdatedEvent', + ArrayFilterEvent::ADD_WORKER_TASK => 'onArrayFilterEvent', + ArrayFilterEvent::STORAGE_CONFIG => 'onArrayFilterEvent', + ArrayFilterEvent::STORAGE_INSTANCE => 'onArrayFilterEvent', + ArrayFilterEvent::DB_STRUCTURE_DEFINITION => 'onArrayFilterEvent', + ArrayFilterEvent::DB_VIEW_DEFINITION => 'onArrayFilterEvent', + HtmlFilterEvent::HEAD => 'onHtmlFilterEvent', + HtmlFilterEvent::FOOTER => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_HEADER => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_CONTENT_TOP => 'onHtmlFilterEvent', + HtmlFilterEvent::PAGE_END => 'onHtmlFilterEvent', + HtmlFilterEvent::MOD_HOME_CONTENT => 'onHtmlFilterEvent', + HtmlFilterEvent::MOD_ABOUT_CONTENT => 'onHtmlFilterEvent', + HtmlFilterEvent::MOD_PROFILE_CONTENT => 'onHtmlFilterEvent', + HtmlFilterEvent::JOT_TOOL => 'onHtmlFilterEvent', + HtmlFilterEvent::CONTACT_BLOCK_END => 'onHtmlFilterEvent', + ]; + + $this->assertSame( + $expected, + HookEventBridge::getStaticSubscribedEvents() + ); + + foreach ($expected as $methodName) { + $this->assertTrue( + method_exists(HookEventBridge::class, $methodName), + $methodName . '() is not defined' + ); + + $this->assertTrue( + (new \ReflectionMethod(HookEventBridge::class, $methodName))->isStatic(), + $methodName . '() is not static' + ); + } + } + + public static function getNamedEventData(): array + { + return [ + ['test', 'test'], + [Event::INIT, 'init_1'], + [Event::HOME_INIT, 'home_init'], + ]; + } + + /** + * @dataProvider getNamedEventData + */ + public function testOnNamedEventCallsHook($name, $expected): void + { + $event = new Event($name); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, $data) use ($expected) { + $this->assertSame($expected, $name); + $this->assertSame('', $data); + + return $data; + }); + + HookEventBridge::onNamedEvent($event); + } + + public static function getConfigLoadedEventData(): array + { + return [ + ['test', 'test'], + [ConfigLoadedEvent::CONFIG_LOADED, 'load_config'], + ]; + } + + /** + * @dataProvider getConfigLoadedEventData + */ + public function testOnConfigLoadedEventCallsHookWithCorrectValue($name, $expected): void + { + $config = $this->createStub(ConfigFileManager::class); + + $event = new ConfigLoadedEvent($name, $config); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, $data) use ($expected, $config) { + $this->assertSame($expected, $name); + $this->assertSame($config, $data); + + return $data; + }); + + HookEventBridge::onConfigLoadedEvent($event); + } + + public static function getCollectRoutesEventData(): array + { + return [ + ['test', 'test'], + [CollectRoutesEvent::COLLECT_ROUTES, 'route_collection'], + ]; + } + + /** + * @dataProvider getCollectRoutesEventData + */ + public function testOnCollectRoutesEventCallsHookWithCorrectValue($name, $expected): void + { + $routeCollector = $this->createStub(RouteCollector::class); + + $event = new CollectRoutesEvent($name, $routeCollector); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, $data) use ($expected, $routeCollector) { + $this->assertSame($expected, $name); + $this->assertSame($routeCollector, $data); + + return $data; + }); + + HookEventBridge::onCollectRoutesEvent($event); + } + + public function testOnPermissionTooltipContentEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::PERMISSION_TOOLTIP_CONTENT, ['model' => ['uid' => -1]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('lockview_content', $name); + $this->assertSame(['uid' => -1], $data); + + return ['uid' => 123]; + }); + + HookEventBridge::onPermissionTooltipContentEvent($event); + + $this->assertSame( + ['model' => ['uid' => 123]], + $event->getArray(), + ); + } + + public function testOnInsertPostLocalEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL, ['item' => ['id' => -1]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('post_local', $name); + $this->assertSame(['id' => -1], $data); + + return ['id' => 123]; + }); + + HookEventBridge::onInsertPostLocalEvent($event); + + $this->assertSame( + ['item' => ['id' => 123]], + $event->getArray(), + ); + } + + public function testOnInsertPostLocalEndEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL_END, ['item' => ['id' => -1]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('post_local_end', $name); + $this->assertSame(['id' => -1], $data); + + return ['id' => 123]; + }); + + HookEventBridge::onInsertPostLocalEndEvent($event); + + $this->assertSame( + ['item' => ['id' => 123]], + $event->getArray(), + ); + } + + public function testOnPreparePostStartEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::PREPARE_POST_START, ['item' => ['id' => -1]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('prepare_body_init', $name); + $this->assertSame(['id' => -1], $data); + + return ['id' => 123]; + }); + + HookEventBridge::onPreparePostStartEvent($event); + + $this->assertSame( + ['item' => ['id' => 123]], + $event->getArray(), + ); + } + + public function testOnPhotoUploadStartEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::PHOTO_UPLOAD_START, ['request' => ['album' => -1]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('photo_post_init', $name); + $this->assertSame(['album' => -1], $data); + + return ['album' => 123]; + }); + + HookEventBridge::onPhotoUploadStartEvent($event); + + $this->assertSame( + ['request' => ['album' => 123]], + $event->getArray(), + ); + } + + public function testOnPhotoUploadEndEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::PHOTO_UPLOAD_END, ['id' => -1]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, int $data): int { + $this->assertSame('photo_post_end', $name); + $this->assertSame(-1, $data); + + return 123; + }); + + HookEventBridge::onPhotoUploadEndEvent($event); + } + + public function testOnProfileSidebarEntryEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::PROFILE_SIDEBAR_ENTRY, ['profile' => ['uid' => 0, 'name' => 'original']]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('profile_sidebar_enter', $name); + $this->assertSame(['uid' => 0, 'name' => 'original'], $data); + + return ['uid' => 0, 'name' => 'changed']; + }); + + HookEventBridge::onProfileSidebarEntryEvent($event); + + $this->assertSame( + ['profile' => ['uid' => 0, 'name' => 'changed']], + $event->getArray(), + ); + } + + public function testOnOembedFetchEndEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::OEMBED_FETCH_END, ['url' => 'original_url']); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, string $data): string { + $this->assertSame('oembed_fetch_url', $name); + $this->assertSame('original_url', $data); + + return 'changed_url'; + }); + + HookEventBridge::onOembedFetchEndEvent($event); + + $this->assertSame( + ['url' => 'changed_url'], + $event->getArray(), + ); + } + + public function testOnBbcodeToHtmlEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::BBCODE_TO_HTML_START, ['bbcode2html' => '[b]original[/b]']); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, string $data): string { + $this->assertSame('bbcode', $name); + $this->assertSame('[b]original[/b]', $data); + + return 'changed'; + }); + + HookEventBridge::onBbcodeToHtmlEvent($event); + + $this->assertSame( + ['bbcode2html' => 'changed'], + $event->getArray(), + ); + } + + public function testOnHtmlToBbcodeEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::HTML_TO_BBCODE_END, ['html2bbcode' => 'original']); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, string $data): string { + $this->assertSame('html2bbcode', $name); + $this->assertSame('original', $data); + + return '[b]changed[/b]'; + }); + + HookEventBridge::onHtmlToBbcodeEvent($event); + + $this->assertSame( + ['html2bbcode' => '[b]changed[/b]'], + $event->getArray(), + ); + } + + public function testOnBbcodeToMarkdownEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::BBCODE_TO_MARKDOWN_END, ['bbcode2markdown' => '[b]original[/b]']); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, string $data): string { + $this->assertSame('bb2diaspora', $name); + $this->assertSame('[b]original[/b]', $data); + + return '**changed**'; + }); + + HookEventBridge::onBbcodeToMarkdownEvent($event); + + $this->assertSame( + ['bbcode2markdown' => '**changed**'], + $event->getArray(), + ); + } + + public function testOnEventCreatedEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::EVENT_CREATED, ['event' => ['id' => 123]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, int $data): int { + $this->assertSame('event_created', $name); + $this->assertSame(123, $data); + + return 123; + }); + + HookEventBridge::onEventCreatedEvent($event); + } + + public function testOnAccountRegisterEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::ACCOUNT_REGISTER, ['uid' => 123]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, int $data): int { + $this->assertSame('register_account', $name); + $this->assertSame(123, $data); + + return $data; + }); + + HookEventBridge::onAccountRegisterEvent($event); + } + + public function testOnAccountRemoveEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::ACCOUNT_REMOVE, ['user' => ['uid' => 123]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, array $data): array { + $this->assertSame('remove_user', $name); + $this->assertSame(['uid' => 123], $data); + + return $data; + }); + + HookEventBridge::onAccountRemoveEvent($event); + } + + public function testOnEventUpdatedEventCallsHookWithCorrectValue(): void + { + $event = new ArrayFilterEvent(ArrayFilterEvent::EVENT_UPDATED, ['event' => ['id' => 123]]); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, int $data): int { + $this->assertSame('event_updated', $name); + $this->assertSame(123, $data); + + return 123; + }); + + HookEventBridge::onEventUpdatedEvent($event); + } + + public static function getArrayFilterEventData(): array + { + return [ + ['test', 'test'], + [ArrayFilterEvent::APP_MENU, 'app_menu'], + [ArrayFilterEvent::NAV_INFO, 'nav_info'], + [ArrayFilterEvent::FEATURE_ENABLED, 'isEnabled'], + [ArrayFilterEvent::FEATURE_GET, 'get'], + [ArrayFilterEvent::INSERT_POST_LOCAL_START, 'post_local_start'], + [ArrayFilterEvent::INSERT_POST_REMOTE, 'post_remote'], + [ArrayFilterEvent::INSERT_POST_REMOTE_END, 'post_remote_end'], + [ArrayFilterEvent::PREPARE_POST_FILTER_CONTENT, 'prepare_body_content_filter'], + [ArrayFilterEvent::PREPARE_POST, 'prepare_body'], + [ArrayFilterEvent::PREPARE_POST_END, 'prepare_body_final'], + [ArrayFilterEvent::PHOTO_UPLOAD_FORM, 'photo_upload_form'], + [ArrayFilterEvent::PHOTO_UPLOAD, 'photo_post_file'], + [ArrayFilterEvent::NETWORK_TO_NAME, 'network_to_name'], + [ArrayFilterEvent::NETWORK_CONTENT_START, 'network_content_init'], + [ArrayFilterEvent::NETWORK_CONTENT_TABS, 'network_tabs'], + [ArrayFilterEvent::PARSE_LINK, 'parse_link'], + [ArrayFilterEvent::CONVERSATION_START, 'conversation_start'], + [ArrayFilterEvent::FETCH_ITEM_BY_LINK, 'item_by_link'], + [ArrayFilterEvent::ITEM_TAGGED, 'tagged'], + [ArrayFilterEvent::DISPLAY_ITEM, 'display_item'], + [ArrayFilterEvent::CACHE_ITEM, 'put_item_in_cache'], + [ArrayFilterEvent::CHECK_ITEM_NOTIFICATION, 'check_item_notification'], + [ArrayFilterEvent::ENOTIFY, 'enotify'], + [ArrayFilterEvent::ENOTIFY_STORE, 'enotify_store'], + [ArrayFilterEvent::ENOTIFY_MAIL, 'enotify_mail'], + [ArrayFilterEvent::DETECT_LANGUAGES, 'detect_languages'], + [ArrayFilterEvent::RENDER_LOCATION, 'render_location'], + [ArrayFilterEvent::ITEM_PHOTO_MENU, 'item_photo_menu'], + [ArrayFilterEvent::DIRECTORY_ITEM, 'directory_item'], + [ArrayFilterEvent::CONTACT_PHOTO_MENU, 'contact_photo_menu'], + [ArrayFilterEvent::PROFILE_SIDEBAR, 'profile_sidebar'], + [ArrayFilterEvent::PROFILE_TABS, 'profile_tabs'], + [ArrayFilterEvent::PROFILE_SETTINGS_FORM, 'profile_edit'], + [ArrayFilterEvent::PROFILE_SETTINGS_POST, 'profile_post'], + [ArrayFilterEvent::MODERATION_USERS_TABS, 'moderation_users_tabs'], + [ArrayFilterEvent::ACL_LOOKUP_END, 'acl_lookup_end'], + [ArrayFilterEvent::PAGE_INFO, 'page_info_data'], + [ArrayFilterEvent::SMILEY_LIST, 'smilie'], + [ArrayFilterEvent::JOT_NETWORKS, 'jot_networks'], + [ArrayFilterEvent::PROTOCOL_SUPPORTS_FOLLOW, 'support_follow'], + [ArrayFilterEvent::PROTOCOL_SUPPORTS_REVOKE_FOLLOW, 'support_revoke_follow'], + [ArrayFilterEvent::PROTOCOL_SUPPORTS_PROBE, 'support_probe'], + [ArrayFilterEvent::FOLLOW_CONTACT, 'follow'], + [ArrayFilterEvent::UNFOLLOW_CONTACT, 'unfollow'], + [ArrayFilterEvent::REVOKE_FOLLOW_CONTACT, 'revoke_follow'], + [ArrayFilterEvent::BLOCK_CONTACT, 'block'], + [ArrayFilterEvent::UNBLOCK_CONTACT, 'unblock'], + [ArrayFilterEvent::EDIT_CONTACT_FORM, 'contact_edit'], + [ArrayFilterEvent::EDIT_CONTACT_POST, 'contact_edit_post'], + [ArrayFilterEvent::AVATAR_LOOKUP, 'avatar_lookup'], + [ArrayFilterEvent::ACCOUNT_AUTHENTICATE, 'authenticate'], + [ArrayFilterEvent::ACCOUNT_REGISTER_FORM, 'register_form'], + [ArrayFilterEvent::ACCOUNT_REGISTER_POST, 'register_post'], + [ArrayFilterEvent::ACCOUNT_REGISTER, 'register_account'], + [ArrayFilterEvent::ACCOUNT_REMOVE, 'remove_user'], + [ArrayFilterEvent::EVENT_CREATED, 'event_created'], + [ArrayFilterEvent::EVENT_UPDATED, 'event_updated'], + [ArrayFilterEvent::ADD_WORKER_TASK, 'proc_run'], + [ArrayFilterEvent::STORAGE_CONFIG, 'storage_config'], + [ArrayFilterEvent::STORAGE_INSTANCE, 'storage_instance'], + [ArrayFilterEvent::DB_STRUCTURE_DEFINITION, 'dbstructure_definition'], + [ArrayFilterEvent::DB_VIEW_DEFINITION, 'dbview_definition'], + ]; + } + + /** + * @dataProvider getArrayFilterEventData + */ + public function testOnArrayFilterEventCallsHookWithCorrectValue($name, $expected): void + { + $event = new ArrayFilterEvent($name, ['original']); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, $data) use ($expected) { + $this->assertSame($expected, $name); + $this->assertSame(['original'], $data); + + return $data; + }); + + HookEventBridge::onArrayFilterEvent($event); + } + + public static function getHtmlFilterEventData(): array + { + return [ + ['test', 'test'], + [HtmlFilterEvent::HEAD, 'head'], + [HtmlFilterEvent::FOOTER, 'footer'], + [HtmlFilterEvent::PAGE_HEADER, 'page_header'], + [HtmlFilterEvent::PAGE_CONTENT_TOP, 'page_content_top'], + [HtmlFilterEvent::PAGE_END, 'page_end'], + [HtmlFilterEvent::MOD_HOME_CONTENT, 'home_content'], + [HtmlFilterEvent::MOD_ABOUT_CONTENT, 'about_hook'], + [HtmlFilterEvent::MOD_PROFILE_CONTENT, 'profile_advanced'], + [HtmlFilterEvent::JOT_TOOL, 'jot_tool'], + [HtmlFilterEvent::CONTACT_BLOCK_END, 'contact_block_end'], + ]; + } + + /** + * @dataProvider getHtmlFilterEventData + */ + public function testOnHtmlFilterEventCallsHookWithCorrectValue($name, $expected): void + { + $event = new HtmlFilterEvent($name, 'original'); + + $reflectionProperty = new \ReflectionProperty(HookEventBridge::class, 'mockedCallHook'); + $reflectionProperty->setAccessible(true); + + $reflectionProperty->setValue(null, function (string $name, $data) use ($expected) { + $this->assertSame($expected, $name); + $this->assertSame('original', $data); + + return $data; + }); + + HookEventBridge::onHtmlFilterEvent($event); + } +} diff --git a/tests/Unit/Core/Logger/Factory/DelegatingLoggerFactoryTest.php b/tests/Unit/Core/Logger/Factory/DelegatingLoggerFactoryTest.php new file mode 100644 index 0000000000..b0fd92cafd --- /dev/null +++ b/tests/Unit/Core/Logger/Factory/DelegatingLoggerFactoryTest.php @@ -0,0 +1,75 @@ +createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'logger_config', null, 'test'], + ]); + + $factory = new DelegatingLoggerFactory($config); + + $factory->registerFactory('test', $this->createStub(LoggerFactory::class)); + + $this->assertInstanceOf( + LoggerInterface::class, + $factory->createLogger(LogLevel::DEBUG, LogChannel::DEFAULT) + ); + } + + public function testCreateLoggerWithoutRegisteredFactoryReturnsNullLogger(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'logger_config', null, 'not-existing-factory'], + ]); + + $factory = new DelegatingLoggerFactory($config); + + $this->assertInstanceOf( + NullLogger::class, + $factory->createLogger(LogLevel::DEBUG, LogChannel::DEFAULT) + ); + } + + public function testCreateLoggerWithExceptionThrowingFactoryReturnsNullLogger(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'logger_config', null, 'test'], + ]); + + $factory = new DelegatingLoggerFactory($config); + + $brokenFactory = $this->createStub(LoggerFactory::class); + $brokenFactory->method('createLogger')->willThrowException(new Exception()); + + $factory->registerFactory('test', $brokenFactory); + + $this->assertInstanceOf( + NullLogger::class, + $factory->createLogger(LogLevel::DEBUG, LogChannel::DEFAULT) + ); + } +} diff --git a/tests/Unit/Core/Logger/Factory/StreamLoggerFactoryTest.php b/tests/Unit/Core/Logger/Factory/StreamLoggerFactoryTest.php new file mode 100644 index 0000000000..744d597f19 --- /dev/null +++ b/tests/Unit/Core/Logger/Factory/StreamLoggerFactoryTest.php @@ -0,0 +1,81 @@ +createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'logfile', null, dirname(__DIR__, 4) . '/datasets/log/empty.friendica.log.txt'], + ]); + + $factory = new StreamLoggerFactory( + $config, + $this->createStub(IHaveCallIntrospections::class), + $this->createStub(FileSystemUtil::class), + ); + + $this->assertInstanceOf( + LoggerInterface::class, + $factory->createLogger(LogLevel::DEBUG, LogChannel::DEFAULT) + ); + } + + public function testCreateLoggerWithInvalidLogfileThrowsException(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'logfile', null, dirname(__DIR__, 1) . '/not-existing-logfile.txt'], + ]); + + $factory = new StreamLoggerFactory( + $config, + $this->createStub(IHaveCallIntrospections::class), + $this->createStub(FileSystemUtil::class), + ); + + $this->expectException(LoggerArgumentException::class); + $this->expectExceptionMessage('tests/Unit/Core/Logger/not-existing-logfile.txt" is not a valid logfile.'); + + $factory->createLogger(LogLevel::DEBUG, LogChannel::DEFAULT); + } + + public function testCreateLoggerWithInvalidLoglevelThrowsException(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'logfile', null, dirname(__DIR__, 4) . '/datasets/log/empty.friendica.log.txt'], + ]); + + $factory = new StreamLoggerFactory( + $config, + $this->createStub(IHaveCallIntrospections::class), + $this->createStub(FileSystemUtil::class), + ); + + $this->expectException(LogLevelException::class); + $this->expectExceptionMessage('The log level "unsupported-loglevel" is not supported by "Friendica\Core\Logger\Type\StreamLogger".'); + + $factory->createLogger('unsupported-loglevel', LogChannel::DEFAULT); + } +} diff --git a/tests/Unit/Core/Logger/Factory/SyslogLoggerFactoryTest.php b/tests/Unit/Core/Logger/Factory/SyslogLoggerFactoryTest.php new file mode 100644 index 0000000000..7f94c66fcd --- /dev/null +++ b/tests/Unit/Core/Logger/Factory/SyslogLoggerFactoryTest.php @@ -0,0 +1,61 @@ +createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'syslog_flags', null, SyslogLogger::DEFAULT_FLAGS], + ['system', 'syslog_facility', null, SyslogLogger::DEFAULT_FACILITY], + ]); + + $factory = new SyslogLoggerFactory( + $config, + $this->createStub(IHaveCallIntrospections::class), + ); + + $this->assertInstanceOf( + LoggerInterface::class, + $factory->createLogger(LogLevel::DEBUG, LogChannel::DEFAULT) + ); + } + + public function testCreateLoggerWithInvalidLoglevelThrowsException(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'syslog_flags', null, SyslogLogger::DEFAULT_FLAGS], + ['system', 'syslog_facility', null, SyslogLogger::DEFAULT_FACILITY], + ]); + + $factory = new SyslogLoggerFactory( + $config, + $this->createStub(IHaveCallIntrospections::class), + ); + + $this->expectException(LogLevelException::class); + $this->expectExceptionMessage('The log level "unsupported-loglevel" is not supported by "Friendica\Core\Logger\Type\SyslogLogger".'); + + $factory->createLogger('unsupported-loglevel', LogChannel::DEFAULT); + } +} diff --git a/tests/Unit/Core/Logger/LoggerManagerTest.php b/tests/Unit/Core/Logger/LoggerManagerTest.php new file mode 100644 index 0000000000..02f9c0b2e2 --- /dev/null +++ b/tests/Unit/Core/Logger/LoggerManagerTest.php @@ -0,0 +1,129 @@ +setAccessible(true); + $reflectionProperty->setValue(null, null); + + $reflectionProperty = new \ReflectionProperty(LoggerManager::class, 'logChannel'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue(null, LogChannel::DEFAULT); + } + + public function testGetLoggerReturnsPsrLogger(): void + { + $factory = new LoggerManager( + $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerFactory::class) + ); + + $this->assertInstanceOf(LoggerInterface::class, $factory->getLogger()); + } + + public function testGetLoggerReturnsSameObject(): void + { + $factory = new LoggerManager( + $this->createStub(IManageConfigValues::class), + $this->createStub(LoggerFactory::class) + ); + + $this->assertSame($factory->getLogger(), $factory->getLogger()); + } + + public function testGetLoggerWithDebugDisabledReturnsNullLogger(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'debugging', null, false], + ]); + + $factory = new LoggerManager( + $config, + $this->createStub(LoggerFactory::class) + ); + + $this->assertInstanceOf(NullLogger::class, $factory->getLogger()); + } + + public function testGetLoggerWithProfilerEnabledReturnsProfilerLogger(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'debugging', null, false], + ['system', 'profiling', null, true], + ]); + + $factory = new LoggerManager( + $config, + $this->createStub(LoggerFactory::class) + ); + + $this->assertInstanceOf(ProfilerLogger::class, $factory->getLogger()); + } + + public function testChangeLogChannelReturnsDifferentLogger(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'debugging', null, false], + ['system', 'profiling', null, true], + ]); + + $factory = new LoggerManager( + $config, + $this->createStub(LoggerFactory::class) + ); + + $logger1 = $factory->getLogger(); + + $factory->changeLogChannel(LogChannel::CONSOLE); + + $this->assertNotSame($logger1, $factory->getLogger()); + } + + public function testChangeLogChannelToWorkerReturnsWorkerLogger(): void + { + $config = $this->createStub(IManageConfigValues::class); + $config->method('get')->willReturnMap([ + ['system', 'debugging', null, false], + ['system', 'profiling', null, true], + ]); + + $factory = new LoggerManager( + $config, + $this->createStub(LoggerFactory::class) + ); + + $factory->changeLogChannel(LogChannel::WORKER); + + $this->assertInstanceOf(WorkerLogger::class, $factory->getLogger()); + } +} diff --git a/tests/Unit/Event/ArrayFilterEventTest.php b/tests/Unit/Event/ArrayFilterEventTest.php new file mode 100644 index 0000000000..85f9307a0d --- /dev/null +++ b/tests/Unit/Event/ArrayFilterEventTest.php @@ -0,0 +1,138 @@ +assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [ArrayFilterEvent::APP_MENU, 'friendica.data.app_menu'], + [ArrayFilterEvent::NAV_INFO, 'friendica.data.nav_info'], + [ArrayFilterEvent::FEATURE_ENABLED, 'friendica.data.feature_enabled'], + [ArrayFilterEvent::FEATURE_GET, 'friendica.data.feature_get'], + [ArrayFilterEvent::PERMISSION_TOOLTIP_CONTENT, 'friendica.data.permission_tooltip_content'], + [ArrayFilterEvent::INSERT_POST_LOCAL_START, 'friendica.data.insert_post_local_start'], + [ArrayFilterEvent::INSERT_POST_LOCAL, 'friendica.data.insert_post_local'], + [ArrayFilterEvent::INSERT_POST_LOCAL_END, 'friendica.data.insert_post_local_end'], + [ArrayFilterEvent::INSERT_POST_REMOTE, 'friendica.data.insert_post_remote'], + [ArrayFilterEvent::INSERT_POST_REMOTE_END, 'friendica.data.insert_post_remote_end'], + [ArrayFilterEvent::PREPARE_POST_START, 'friendica.data.prepare_post_start'], + [ArrayFilterEvent::PREPARE_POST_FILTER_CONTENT, 'friendica.data.prepare_post_filter_content'], + [ArrayFilterEvent::PREPARE_POST, 'friendica.data.prepare_post'], + [ArrayFilterEvent::PREPARE_POST_END, 'friendica.data.prepare_post_end'], + [ArrayFilterEvent::PHOTO_UPLOAD_FORM, 'friendica.data.photo_upload_form'], + [ArrayFilterEvent::PHOTO_UPLOAD_START, 'friendica.data.photo_upload_start'], + [ArrayFilterEvent::PHOTO_UPLOAD, 'friendica.data.photo_upload'], + [ArrayFilterEvent::PHOTO_UPLOAD_END, 'friendica.data.photo_upload_end'], + [ArrayFilterEvent::NETWORK_TO_NAME, 'friendica.data.network_to_name'], + [ArrayFilterEvent::NETWORK_CONTENT_START, 'friendica.data.network_content_start'], + [ArrayFilterEvent::NETWORK_CONTENT_TABS, 'friendica.data.network_content_tabs'], + [ArrayFilterEvent::PARSE_LINK, 'friendica.data.parse_link'], + [ArrayFilterEvent::CONVERSATION_START, 'friendica.data.conversation_start'], + [ArrayFilterEvent::FETCH_ITEM_BY_LINK, 'friendica.data.fetch_item_by_link'], + [ArrayFilterEvent::ITEM_TAGGED, 'friendica.data.item_tagged'], + [ArrayFilterEvent::DISPLAY_ITEM, 'friendica.data.display_item'], + [ArrayFilterEvent::CACHE_ITEM, 'friendica.data.cache_item'], + [ArrayFilterEvent::CHECK_ITEM_NOTIFICATION, 'friendica.data.check_item_notification'], + [ArrayFilterEvent::ENOTIFY, 'friendica.data.enotify'], + [ArrayFilterEvent::ENOTIFY_STORE, 'friendica.data.enotify_store'], + [ArrayFilterEvent::ENOTIFY_MAIL, 'friendica.data.enotify_mail'], + [ArrayFilterEvent::DETECT_LANGUAGES, 'friendica.data.detect_languages'], + [ArrayFilterEvent::RENDER_LOCATION, 'friendica.data.render_location'], + [ArrayFilterEvent::ITEM_PHOTO_MENU, 'friendica.data.item_photo_menu'], + [ArrayFilterEvent::DIRECTORY_ITEM, 'friendica.data.directory_item'], + [ArrayFilterEvent::CONTACT_PHOTO_MENU, 'friendica.data.contact_photo_menu'], + [ArrayFilterEvent::PROFILE_SIDEBAR_ENTRY, 'friendica.data.profile_sidebar_entry'], + [ArrayFilterEvent::PROFILE_SIDEBAR, 'friendica.data.profile_sidebar'], + [ArrayFilterEvent::PROFILE_TABS, 'friendica.data.profile_tabs'], + [ArrayFilterEvent::PROFILE_SETTINGS_FORM, 'friendica.data.profile_settings_form'], + [ArrayFilterEvent::PROFILE_SETTINGS_POST, 'friendica.data.profile_settings_post'], + [ArrayFilterEvent::MODERATION_USERS_TABS, 'friendica.data.moderation_users_tabs'], + [ArrayFilterEvent::ACL_LOOKUP_END, 'friendica.data.acl_lookup_end'], + [ArrayFilterEvent::OEMBED_FETCH_END, 'friendica.data.oembed_fetch_end'], + [ArrayFilterEvent::PAGE_INFO, 'friendica.data.page_info'], + [ArrayFilterEvent::SMILEY_LIST, 'friendica.data.smiley_list'], + [ArrayFilterEvent::BBCODE_TO_HTML_START, 'friendica.data.bbcode_to_html_start'], + [ArrayFilterEvent::HTML_TO_BBCODE_END, 'friendica.data.html_to_bbcode_end'], + [ArrayFilterEvent::BBCODE_TO_MARKDOWN_END, 'friendica.data.bbcode_to_markdown_end'], + [ArrayFilterEvent::JOT_NETWORKS, 'friendica.data.jot_networks'], + [ArrayFilterEvent::PROTOCOL_SUPPORTS_FOLLOW, 'friendica.data.protocol_supports_follow'], + [ArrayFilterEvent::PROTOCOL_SUPPORTS_REVOKE_FOLLOW, 'friendica.data.protocol_supports_revoke_follow'], + [ArrayFilterEvent::PROTOCOL_SUPPORTS_PROBE, 'friendica.data.protocol_supports_probe'], + [ArrayFilterEvent::FOLLOW_CONTACT, 'friendica.data.follow_contact'], + [ArrayFilterEvent::UNFOLLOW_CONTACT, 'friendica.data.unfollow_contact'], + [ArrayFilterEvent::REVOKE_FOLLOW_CONTACT, 'friendica.data.revoke_follow_contact'], + [ArrayFilterEvent::BLOCK_CONTACT, 'friendica.data.block_contact'], + [ArrayFilterEvent::UNBLOCK_CONTACT, 'friendica.data.unblock_contact'], + [ArrayFilterEvent::EDIT_CONTACT_FORM, 'friendica.data.edit_contact_form'], + [ArrayFilterEvent::EDIT_CONTACT_POST, 'friendica.data.edit_contact_post'], + [ArrayFilterEvent::AVATAR_LOOKUP, 'friendica.data.avatar_lookup'], + [ArrayFilterEvent::ACCOUNT_AUTHENTICATE, 'friendica.data.account_authenticate'], + [ArrayFilterEvent::ACCOUNT_REGISTER_FORM, 'friendica.data.account_register_form'], + [ArrayFilterEvent::ACCOUNT_REGISTER_POST, 'friendica.data.account_register_post'], + [ArrayFilterEvent::ACCOUNT_REGISTER, 'friendica.data.account_register'], + [ArrayFilterEvent::ACCOUNT_REMOVE, 'friendica.data.account_remove'], + [ArrayFilterEvent::EVENT_CREATED, 'friendica.data.event_created'], + [ArrayFilterEvent::EVENT_UPDATED, 'friendica.data.event_updated'], + [ArrayFilterEvent::ADD_WORKER_TASK, 'friendica.data.add_worker_task'], + [ArrayFilterEvent::STORAGE_CONFIG, 'friendica.data.storage_config'], + [ArrayFilterEvent::STORAGE_INSTANCE, 'friendica.data.storage_instance'], + [ArrayFilterEvent::DB_STRUCTURE_DEFINITION, 'friendica.data.db_structure_definition'], + [ArrayFilterEvent::DB_VIEW_DEFINITION, 'friendica.data.db_view_definition'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new ArrayFilterEvent('test', []); + + $this->assertSame('test', $event->getName()); + } + + public function testGetArrayReturnsCorrectString(): void + { + $data = ['original']; + + $event = new ArrayFilterEvent('test', $data); + + $this->assertSame($data, $event->getArray()); + } + + public function testSetArrayUpdatesHtml(): void + { + $event = new ArrayFilterEvent('test', ['original']); + + $expected = ['updated']; + + $event->setArray($expected); + + $this->assertSame($expected, $event->getArray()); + } +} diff --git a/tests/Unit/Event/CollectRoutesEventTest.php b/tests/Unit/Event/CollectRoutesEventTest.php new file mode 100644 index 0000000000..0a1bb88d59 --- /dev/null +++ b/tests/Unit/Event/CollectRoutesEventTest.php @@ -0,0 +1,67 @@ +createStub(RouteCollector::class)); + + $this->assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [CollectRoutesEvent::COLLECT_ROUTES, 'friendica.collect_routes'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new CollectRoutesEvent('test', $this->createStub(RouteCollector::class)); + + $this->assertSame('test', $event->getName()); + } + + public function testGetRouteCollectorReturnsCorrectString(): void + { + $routeCollector = $this->createStub(RouteCollector::class); + + $event = new CollectRoutesEvent('test', $routeCollector); + + $this->assertSame($routeCollector, $event->getRouteCollector()); + } + + public function testSetRouteCollector(): void + { + $event = new CollectRoutesEvent('test', $this->createStub(RouteCollector::class)); + + $routeCollector = $this->createStub(RouteCollector::class); + + $event->setRouteCollector($routeCollector); + + $this->assertSame($routeCollector, $event->getRouteCollector()); + } +} diff --git a/tests/Unit/Event/ConfigLoadedEventTest.php b/tests/Unit/Event/ConfigLoadedEventTest.php new file mode 100644 index 0000000000..473b75ac89 --- /dev/null +++ b/tests/Unit/Event/ConfigLoadedEventTest.php @@ -0,0 +1,56 @@ +createStub(ConfigFileManager::class)); + + $this->assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [ConfigLoadedEvent::CONFIG_LOADED, 'friendica.config_loaded'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new ConfigLoadedEvent('test', $this->createStub(ConfigFileManager::class)); + + $this->assertSame('test', $event->getName()); + } + + public function testGetConfigReturnsCorrectString(): void + { + $config = $this->createStub(ConfigFileManager::class); + + $event = new ConfigLoadedEvent('test', $config); + + $this->assertSame($config, $event->getConfig()); + } +} diff --git a/tests/Unit/Event/EventDispatcherTest.php b/tests/Unit/Event/EventDispatcherTest.php new file mode 100644 index 0000000000..2cc1527164 --- /dev/null +++ b/tests/Unit/Event/EventDispatcherTest.php @@ -0,0 +1,37 @@ +assertInstanceOf(EventDispatcherInterface::class, $eventDispatcher); + } + + public function testDispatchANamedEventUsesNameAsEventName(): void + { + $eventDispatcher = new EventDispatcher(); + + $eventDispatcher->addListener('test', function (NamedEvent $event) { + $this->assertSame('test', $event->getName()); + }); + + $eventDispatcher->dispatch(new Event('test')); + } +} diff --git a/tests/Unit/Event/EventTest.php b/tests/Unit/Event/EventTest.php new file mode 100644 index 0000000000..8d7f882451 --- /dev/null +++ b/tests/Unit/Event/EventTest.php @@ -0,0 +1,46 @@ +assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [Event::INIT, 'friendica.init'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new Event('test'); + + $this->assertSame('test', $event->getName()); + } +} diff --git a/tests/Unit/Event/HtmlFilterEventTest.php b/tests/Unit/Event/HtmlFilterEventTest.php new file mode 100644 index 0000000000..bae1669ca0 --- /dev/null +++ b/tests/Unit/Event/HtmlFilterEventTest.php @@ -0,0 +1,75 @@ +assertInstanceOf(NamedEvent::class, $event); + } + + public static function getPublicConstants(): array + { + return [ + [HtmlFilterEvent::HEAD, 'friendica.html.head'], + [HtmlFilterEvent::FOOTER, 'friendica.html.footer'], + [HtmlFilterEvent::PAGE_HEADER, 'friendica.html.page_header'], + [HtmlFilterEvent::PAGE_CONTENT_TOP, 'friendica.html.page_content_top'], + [HtmlFilterEvent::PAGE_END, 'friendica.html.page_end'], + [HtmlFilterEvent::MOD_HOME_CONTENT, 'friendica.html.mod_home_content'], + [HtmlFilterEvent::MOD_ABOUT_CONTENT, 'friendica.html.mod_about_content'], + [HtmlFilterEvent::MOD_PROFILE_CONTENT, 'friendica.html.mod_profile_content'], + [HtmlFilterEvent::JOT_TOOL, 'friendica.html.jot_tool'], + [HtmlFilterEvent::CONTACT_BLOCK_END, 'friendica.html.contact_block_end'], + ]; + } + + /** + * @dataProvider getPublicConstants + */ + public function testPublicConstantsAreAvailable($value, $expected): void + { + $this->assertSame($expected, $value); + } + + public function testGetNameReturnsName(): void + { + $event = new HtmlFilterEvent('test', ''); + + $this->assertSame('test', $event->getName()); + } + + public function testGetHtmlReturnsCorrectString(): void + { + $data = 'original'; + + $event = new HtmlFilterEvent('test', $data); + + $this->assertSame($data, $event->getHtml()); + } + + public function testSetHtmlUpdatesHtml(): void + { + $event = new HtmlFilterEvent('test', 'original'); + + $expected = 'updated'; + + $event->setHtml($expected); + + $this->assertSame($expected, $event->getHtml()); + } +} diff --git a/tests/Unit/Object/Api/Mastodon/PreferencesTest.php b/tests/Unit/Object/Api/Mastodon/PreferencesTest.php new file mode 100644 index 0000000000..e77a4272e0 --- /dev/null +++ b/tests/Unit/Object/Api/Mastodon/PreferencesTest.php @@ -0,0 +1,46 @@ + 'visibility', + 'posting:default:sensitive' => true, + 'posting:default:language' => 'language', + 'reading:expand:media' => 'media', + 'reading:expand:spoilers' => false, + ], + $preferences->toArray(), + ); + } + + public function testJsonSerializeReturnsArray(): void + { + $preferences = new Preferences('visibility', true, 'language', 'media', false); + + self::assertSame( + [ + 'posting:default:visibility' => 'visibility', + 'posting:default:sensitive' => true, + 'posting:default:language' => 'language', + 'reading:expand:media' => 'media', + 'reading:expand:spoilers' => false, + ], + $preferences->jsonSerialize(), + ); + } +} diff --git a/tests/Unit/Util/BasePathTest.php b/tests/Unit/Util/BasePathTest.php new file mode 100644 index 0000000000..dbdf9bd261 --- /dev/null +++ b/tests/Unit/Util/BasePathTest.php @@ -0,0 +1,88 @@ + [ + 'server' => [], + 'baseDir' => $configPath, + 'expected' => $configPath, + ], + 'relative' => [ + 'server' => [], + 'baseDir' => 'config', + 'expected' => $configPath, + ], + 'document_root' => [ + 'server' => [ + 'DOCUMENT_ROOT' => $configPath, + ], + 'baseDir' => '/noooop', + 'expected' => $configPath, + ], + 'pwd' => [ + 'server' => [ + 'PWD' => $configPath, + ], + 'baseDir' => '/noooop', + 'expected' => $configPath, + ], + 'no_overwrite' => [ + 'server' => [ + 'DOCUMENT_ROOT' => $basePath, + 'PWD' => $basePath, + ], + 'baseDir' => 'config', + 'expected' => $configPath, + ], + 'no_overwrite_if_invalid' => [ + 'server' => [ + 'DOCUMENT_ROOT' => '/nopopop', + 'PWD' => $configPath, + ], + 'baseDir' => '/noatgawe22fafa', + 'expected' => $configPath, + ] + ]; + } + + /** + * Test the basepath determination + * @dataProvider getDataPaths + */ + public function testDetermineBasePath(array $server, string $baseDir, string $expected): void + { + $basepath = new BasePath($baseDir, $server); + self::assertEquals($expected, $basepath->getPath()); + } + + /** + * Test the basepath determination with a complete wrong path + */ + public function testFailedBasePath(): void + { + $basepath = new BasePath('/now23452sgfgas', []); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('\'/now23452sgfgas\' is not a valid basepath'); + + $basepath->getPath(); + } +} diff --git a/tests/Unit/Util/CryptoTest.php b/tests/Unit/Util/CryptoTest.php new file mode 100644 index 0000000000..d04e0323c3 --- /dev/null +++ b/tests/Unit/Util/CryptoTest.php @@ -0,0 +1,48 @@ +getFunctionMock('Friendica\Util', 'random_int'); + $random_int->expects($this->any())->willReturnCallback(function ($min, $max) { + return 1; + }); + + self::assertSame('1', Crypto::randomDigits(1)); + self::assertSame('11111111', Crypto::randomDigits(8)); + } + + public function testDiasporaPubRsaToMe() + { + $key = 'LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tDQpNSUdKQW9HQkFORjVLTmJzN2k3aTByNVFZckNpRExEZ09pU1BWbmgvdlFnMXpnSk9VZVRheWVETk5yZTR6T1RVDQpSVDcyZGlLQ294OGpYOE5paElJTFJtcUtTOWxVYVNzd21QcVNFenVpdE5xeEhnQy8xS2ZuaXM1Qm96NnRwUUxjDQpsZDMwQjJSMWZIVWdFTHZWd0JkV29pRDhSRUt1dFNuRVBGd1RwVmV6aVlWYWtNY25pclRWQWdNQkFBRT0NCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0'; + + // TODO PHPUnit 10: Replace with assertStringEqualsStringIgnoringLineEndings() + self::assertSame( + str_replace("\n", "\r\n", <<< TXT + -----BEGIN PUBLIC KEY----- + MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDReSjW7O4u4tK+UGKwogyw4Dok + j1Z4f70INc4CTlHk2sngzTa3uMzk1EU+9nYigqMfI1/DYoSCC0ZqikvZVGkrMJj6 + khM7orTasR4Av9Sn54rOQaM+raUC3JXd9AdkdXx1IBC71cAXVqIg/ERCrrUpxDxc + E6VXs4mFWpDHJ4q01QIDAQAB + -----END PUBLIC KEY----- + TXT), + Crypto::rsaToPem(base64_decode($key)) + ); + } +} diff --git a/tests/Util/AppDouble.php b/tests/Util/AppDouble.php deleted file mode 100644 index 59ad25c1c7..0000000000 --- a/tests/Util/AppDouble.php +++ /dev/null @@ -1,50 +0,0 @@ -. - * - */ - -namespace Friendica\Test\Util; - -use Friendica\App; - -/** - * Making the App class overridable for specific situations - * - * @see App - */ -class AppDouble extends App -{ - /** @var bool Marks/Overwrites if the user is currently logged in */ - protected $isLoggedIn = false; - - /** - * Manually overwrite the "isLoggedIn" behavior - * - * @param bool $isLoggedIn - */ - public function setIsLoggedIn(bool $isLoggedIn) - { - $this->isLoggedIn = $isLoggedIn; - } - - public function isLoggedIn(): bool - { - return $this->isLoggedIn; - } -} diff --git a/tests/Util/AuthTestConfig.php b/tests/Util/AuthTestConfig.php index 249eda795e..439274ea43 100644 --- a/tests/Util/AuthTestConfig.php +++ b/tests/Util/AuthTestConfig.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/AuthenticationDouble.php b/tests/Util/AuthenticationDouble.php index 396b09bd8e..25188269aa 100644 --- a/tests/Util/AuthenticationDouble.php +++ b/tests/Util/AuthenticationDouble.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/CollectionDouble.php b/tests/Util/CollectionDouble.php index f2678a6145..1c4490f408 100644 --- a/tests/Util/CollectionDouble.php +++ b/tests/Util/CollectionDouble.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/CreateDatabaseTrait.php b/tests/Util/CreateDatabaseTrait.php index 3d83306532..05d3d6c0ca 100644 --- a/tests/Util/CreateDatabaseTrait.php +++ b/tests/Util/CreateDatabaseTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; @@ -29,8 +15,6 @@ use Friendica\Database\Definition\DbaDefinition; use Friendica\Database\Definition\ViewDefinition; use Friendica\Test\DatabaseTestTrait; use Friendica\Test\Util\Database\StaticDatabase; -use Friendica\Util\Profiler; -use Psr\Log\NullLogger; trait CreateDatabaseTrait { @@ -46,8 +30,13 @@ trait CreateDatabaseTrait return $this->dba; } - $configFileManager = new ConfigFileManager($this->root->url(), $this->root->url() . '/config/', $this->root->url() . '/static/'); - $config = new ReadOnlyFileConfig(new Cache([ + $configFileManager = new ConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + $this->root->url() . '/config', + $this->root->url() . '/static' + ); + $config = new ReadOnlyFileConfig(new Cache([ 'database' => [ 'disable_pdo' => true ], diff --git a/tests/Util/Database/ExtendedPDO.php b/tests/Util/Database/ExtendedPDO.php index 153babf283..0babf01370 100644 --- a/tests/Util/Database/ExtendedPDO.php +++ b/tests/Util/Database/ExtendedPDO.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util\Database; diff --git a/tests/Util/Database/StaticDatabase.php b/tests/Util/Database/StaticDatabase.php index 3ae497e9af..2660029322 100644 --- a/tests/Util/Database/StaticDatabase.php +++ b/tests/Util/Database/StaticDatabase.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util\Database; @@ -211,7 +197,7 @@ class StaticDatabase extends Database { if (isset(self::$staticConnection)) { while (self::$staticConnection->getTransactionDepth() > 0) { - self::$staticConnection->rollBack(); + self::$staticConnection->rollback(); } } } diff --git a/tests/Util/EmailerSpy.php b/tests/Util/EmailerSpy.php index 9a490d2ef3..4ca782aaf3 100644 --- a/tests/Util/EmailerSpy.php +++ b/tests/Util/EmailerSpy.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/EntityDouble.php b/tests/Util/EntityDouble.php index 37a987c57c..8a743257a0 100644 --- a/tests/Util/EntityDouble.php +++ b/tests/Util/EntityDouble.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/FakeEventDispatcher.php b/tests/Util/FakeEventDispatcher.php new file mode 100644 index 0000000000..434e9ba5a6 --- /dev/null +++ b/tests/Util/FakeEventDispatcher.php @@ -0,0 +1,31 @@ +. - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/HookMockTrait.php b/tests/Util/HookMockTrait.php index 822bfe473e..b60370ca37 100644 --- a/tests/Util/HookMockTrait.php +++ b/tests/Util/HookMockTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/Hooks/InstanceMocks/FakeInstance.php b/tests/Util/Hooks/InstanceMocks/FakeInstance.php index 64fe2c5adf..0e1c5dadbb 100644 --- a/tests/Util/Hooks/InstanceMocks/FakeInstance.php +++ b/tests/Util/Hooks/InstanceMocks/FakeInstance.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util\Hooks\InstanceMocks; diff --git a/tests/Util/Hooks/InstanceMocks/FakeInstanceDecorator.php b/tests/Util/Hooks/InstanceMocks/FakeInstanceDecorator.php index 40ab78a257..f10e09a6a3 100644 --- a/tests/Util/Hooks/InstanceMocks/FakeInstanceDecorator.php +++ b/tests/Util/Hooks/InstanceMocks/FakeInstanceDecorator.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util\Hooks\InstanceMocks; diff --git a/tests/Util/Hooks/InstanceMocks/IAmADecoratedInterface.php b/tests/Util/Hooks/InstanceMocks/IAmADecoratedInterface.php index fe93aa998d..ef400f90e7 100644 --- a/tests/Util/Hooks/InstanceMocks/IAmADecoratedInterface.php +++ b/tests/Util/Hooks/InstanceMocks/IAmADecoratedInterface.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util\Hooks\InstanceMocks; diff --git a/tests/Util/Intercept.php b/tests/Util/Intercept.php index 483d274fca..846e92ec64 100644 --- a/tests/Util/Intercept.php +++ b/tests/Util/Intercept.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/RendererMockTrait.php b/tests/Util/RendererMockTrait.php index b41473ba49..7ffe699da6 100644 --- a/tests/Util/RendererMockTrait.php +++ b/tests/Util/RendererMockTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/SampleMailBuilder.php b/tests/Util/SampleMailBuilder.php index eb03cbc36b..b5e854629b 100644 --- a/tests/Util/SampleMailBuilder.php +++ b/tests/Util/SampleMailBuilder.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/SampleStorageBackend.php b/tests/Util/SampleStorageBackend.php index ac9a21b662..c554b802d4 100644 --- a/tests/Util/SampleStorageBackend.php +++ b/tests/Util/SampleStorageBackend.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/SampleStorageBackendInstance.php b/tests/Util/SampleStorageBackendInstance.php index c1c2869e3e..e8423db62d 100644 --- a/tests/Util/SampleStorageBackendInstance.php +++ b/tests/Util/SampleStorageBackendInstance.php @@ -1,24 +1,9 @@ . - * - * Contains a test-hook call for creating a storage instance - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later use Friendica\App; use Friendica\Core\L10n; diff --git a/tests/Util/SerializableObjectDouble.php b/tests/Util/SerializableObjectDouble.php index e159655012..5b4d49faab 100644 --- a/tests/Util/SerializableObjectDouble.php +++ b/tests/Util/SerializableObjectDouble.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/SmileyWhitespaceAddon.php b/tests/Util/SmileyWhitespaceAddon.php index 5277d3d3fa..1fe4836fe5 100644 --- a/tests/Util/SmileyWhitespaceAddon.php +++ b/tests/Util/SmileyWhitespaceAddon.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later use Friendica\Content\Smilies; diff --git a/tests/Util/StaticCookie.php b/tests/Util/StaticCookie.php index 6660863733..faeb5433df 100644 --- a/tests/Util/StaticCookie.php +++ b/tests/Util/StaticCookie.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/VFSTrait.php b/tests/Util/VFSTrait.php index 4c31a5e64e..1583ea8a1e 100644 --- a/tests/Util/VFSTrait.php +++ b/tests/Util/VFSTrait.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\Util; diff --git a/tests/Util/authtest/authtest.php b/tests/Util/authtest/authtest.php index ca46d53e4b..71d1fa5ad5 100644 --- a/tests/Util/authtest/authtest.php +++ b/tests/Util/authtest/authtest.php @@ -1,4 +1,9 @@ . - * - * This file is loaded by PHPUnit before any test. - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later use PHPUnit\Framework\TestCase; if (!file_exists(__DIR__ . '/../vendor/autoload.php')) { - die('Vendor path not found. Please execute "bin/composer.phar --no-dev install" on the command line in the web root.'); + die('Vendor path not found. Please execute "bin/composer.phar install" on the command line in the web root.'); } +error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED); require __DIR__ . '/../vendor/autoload.php'; // Backward compatibility diff --git a/tests/datasets/api.fixture.php b/tests/datasets/api.fixture.php index 1d2bb5f883..5a31cfb3b9 100644 --- a/tests/datasets/api.fixture.php +++ b/tests/datasets/api.fixture.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later use Friendica\Core\Protocol; use Friendica\Model\Contact; @@ -25,23 +11,13 @@ use Friendica\Model\Item; use Friendica\Model\Notification; return [ - // Empty these tables - 'profile_field', - 'permissionset', - 'cache', - 'conversation', - 'pconfig', - 'photo', - 'workerqueue', - 'mail', - 'post-delivery-data', 'gserver' => [ [ - 'url' => 'https://friendica.local', - 'nurl' => 'http://friendica.local', - 'register_policy' => 0, + 'url' => 'https://friendica.local', + 'nurl' => 'http://friendica.local', + 'register_policy' => 0, 'registered-users' => 0, - 'network' => 'unkn', + 'network' => 'unkn', ], ], // Base test config to avoid notice messages @@ -94,24 +70,29 @@ return [ ], [ 'id' => 42, - 'uri' => 'http://localhost/profile/selfcontact', + 'uri' => 'https://friendica.local/profile/selfcontact', 'guid' => '42', ], [ 'id' => 43, - 'uri' => 'http://localhost/profile/othercontact', + 'uri' => 'https://friendica.local/profile/othercontact', 'guid' => '43', ], [ 'id' => 44, - 'uri' => 'http://localhost/profile/friendcontact', + 'uri' => 'https://friendica.local/profile/friendcontact', 'guid' => '44', ], [ 'id' => 46, - 'uri' => 'http://localhost/profile/mutualcontact', + 'uri' => 'https://friendica.local/profile/mutualcontact', 'guid' => '46', ], + [ + 'id' => 49, + 'uri' => 'https://domain.tld/profile/remotecontact', + 'guid' => '49', + ], [ 'id' => 100, 'uri' => 'https://friendica.local/posts/100', @@ -126,9 +107,9 @@ return [ 'name' => 'Self contact', 'nick' => 'selfcontact', 'self' => 1, - 'nurl' => 'http://localhost/profile/selfcontact', - 'url' => 'http://localhost/profile/selfcontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/selfcontact', + 'url' => 'https://friendica.local/profile/selfcontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'about' => 'User used in tests', 'prvkey' => "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDVqxF9kIgtgRL0+q+jTi578FA1r1+crEmlYc0pdxcbmmrhjuRc\nrK1gX3r0mnP25fkHzG+6CAjgbDBRFM1/RXBCyp/KHVks7eQ4yr4MxTRlsxo5qf2o\nnbyNzM7Q+LZhFhe/yIoGN/fuEjlqBE98IfPOrUjsQPX240vGNXIkfLiAWwIDAQAB\nAoGBAIwuiPIdggqAtWQ+mD8HCx5LQwSFw6/xpPu5F7ZNqL52aAsGCbL3o2QoIG4c\na1qf9Ot16BNgNBqxQF3hzRTkBMrKYlmNTUkwJXun/zjQJq2JvOlcrSuXlIucUjs4\nXekVN25aYPHrX9m2FEIUwZTb4UYXbR80KbIDI53BkQ6EwSbpAkEA7aO49CR2Hf1Y\n1d2GaUI/Z0wvbj//+t0Kg0bPt16ca8KVjEQQA5ylsDaiw510jDz9NBQxSOk6If23\nUeRixc1RDQJBAOYtN4YnPM1Zfp6IxXlqMCc+xUWRTPEPFt+WpG+v79koNamAeA6o\nZzTl92hl58IqSdbgojeE2zXWQRvlimFMLQcCQQCV6jND0byyLqFcSeQBg0l8YROK\n+dUC7W80YfeoNod3c8nkMwvnO2tLPyxvO2XLEq6prBNra7bAus5rWyj0oBIBAkEA\n1EvUMFm0TLpEfLgtWuTD8Q6GKLnxO0ztjd+FXrXpBGN/ywyArxRHzJRmctW6wmz6\nmcOqGobhIHCysKYv0bnOtQJAc2M5RwlASHH4jGJzXgt3nboyiJfufM0RV9iry3ho\nCXQRWAONKoLqnsfC6qNP8OzY8FMJcwmPWj7Q/6z6yLBFTA==\n-----END RSA PRIVATE KEY-----", 'pubkey' => "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVqxF9kIgtgRL0+q+jTi578FA1\nr1+crEmlYc0pdxcbmmrhjuRcrK1gX3r0mnP25fkHzG+6CAjgbDBRFM1/RXBCyp/K\nHVks7eQ4yr4MxTRlsxo5qf2onbyNzM7Q+LZhFhe/yIoGN/fuEjlqBE98IfPOrUjs\nQPX240vGNXIkfLiAWwIDAQAB\n-----END PUBLIC KEY-----", @@ -137,6 +118,7 @@ return [ 'rel' => Contact::FOLLOWER, 'network' => Protocol::DFRN, 'location' => 'DFRN', + 'baseurl' => 'https://friendica.local', ], // Having the same name and nick allows us to test // the fallback to api_get_nick() in api_get_user() @@ -147,9 +129,9 @@ return [ 'name' => 'othercontact', 'nick' => 'othercontact', 'self' => 0, - 'nurl' => 'http://localhost/profile/othercontact', - 'url' => 'http://localhost/profile/othercontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/othercontact', + 'url' => 'https://friendica.local/profile/othercontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::NOTHING, @@ -163,9 +145,9 @@ return [ 'name' => 'Friend contact', 'nick' => 'friendcontact', 'self' => 0, - 'nurl' => 'http://localhost/profile/friendcontact', - 'url' => 'http://localhost/profile/friendcontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/friendcontact', + 'url' => 'https://friendica.local/profile/friendcontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::SHARING, @@ -179,9 +161,9 @@ return [ 'name' => 'Friend contact', 'nick' => 'friendcontact', 'self' => 0, - 'nurl' => 'http://localhost/profile/friendcontact', - 'url' => 'http://localhost/profile/friendcontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/friendcontact', + 'url' => 'https://friendica.local/profile/friendcontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::SHARING, @@ -195,9 +177,9 @@ return [ 'name' => 'Mutual contact', 'nick' => 'mutualcontact', 'self' => 0, - 'nurl' => 'http://localhost/profile/mutualcontact', - 'url' => 'http://localhost/profile/mutualcontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/mutualcontact', + 'url' => 'https://friendica.local/profile/mutualcontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::FRIEND, @@ -211,9 +193,9 @@ return [ 'name' => 'Mutual contact', 'nick' => 'mutualcontact', 'self' => 0, - 'nurl' => 'http://localhost/profile/mutualcontact', - 'url' => 'http://localhost/profile/mutualcontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/mutualcontact', + 'url' => 'https://friendica.local/profile/mutualcontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'pending' => 0, 'blocked' => 0, 'rel' => Contact::SHARING, @@ -224,12 +206,12 @@ return [ 'id' => 48, 'uid' => 0, 'uri-id' => 42, - 'name' => 'Self contact', + 'name' => 'Test user', 'nick' => 'selfcontact', 'self' => 0, - 'nurl' => 'http://localhost/profile/selfcontact', - 'url' => 'http://localhost/profile/selfcontact', - 'notify' => 'http://localhost/friendica/inbox', + 'nurl' => 'http://friendica.local/profile/selfcontact', + 'url' => 'https://friendica.local/profile/selfcontact', + 'notify' => 'https://friendica.local/friendica/inbox', 'about' => 'User used in tests', 'pending' => 0, 'blocked' => 0, @@ -237,18 +219,35 @@ return [ 'network' => Protocol::DFRN, 'location' => 'DFRN', ], + [ + 'id' => 49, + 'uid' => 0, + 'uri-id' => 43, + 'name' => 'Remote user', + 'nick' => 'remotecontact', + 'self' => 0, + 'nurl' => 'http://domain.tld/profile/remotecontact', + 'url' => 'https://domain.tld/profile/remotecontact', + 'alias' => 'https://domain.tld/~remotecontact', + 'about' => 'User used in tests', + 'pending' => 0, + 'blocked' => 0, + 'rel' => Contact::FOLLOWER, + 'network' => Protocol::ACTIVITYPUB, + 'location' => 'AP', + ], ], 'apcontact' => [ [ - "url" => "http://localhost/profile/selfcontact", + "url" => "https://friendica.local/profile/selfcontact", "uri-id" => 1, "uuid" => "42", "type" => "Person", - "following" => "http://localhost/following/selfcontact", - "followers" => "http://localhost/followers/selfcontact", - "inbox" => "http://localhost/inbox/selfcontact", - "outbox" => "http://localhost/outbox/selfcontact", - "sharedinbox" => "http://localhost/inbox", + "following" => "https://friendica.local/following/selfcontact", + "followers" => "https://friendica.local/followers/selfcontact", + "inbox" => "https://friendica.local/inbox/selfcontact", + "outbox" => "https://friendica.local/outbox/selfcontact", + "sharedinbox" => "https://friendica.local/inbox", "manually-approve" => 1, "discoverable" => 0, "nick" => "selfcontact", @@ -256,7 +255,7 @@ return [ "about" => "User used in tests", "xmpp" => null, "matrix" => null, - "photo" => "http://localhost/photo/profile/admin.jpeg", + "photo" => "https://friendica.local/photo/profile/admin.jpeg", "header" => null, "addr" => "selfcontact@localhost", "alias" => null, @@ -313,32 +312,32 @@ return [ [ 'uri-id' => 1, 'body' => 'Parent status', - 'plink' => 'http://localhost/display/1', + 'plink' => 'https://friendica.local/display/1', ], [ 'uri-id' => 2, 'body' => 'Reply', - 'plink' => 'http://localhost/display/2', + 'plink' => 'https://friendica.local/display/2', ], [ 'uri-id' => 3, 'body' => 'Other user status', - 'plink' => 'http://localhost/display/3', + 'plink' => 'https://friendica.local/display/3', ], [ 'uri-id' => 4, 'body' => 'Friend user reply', - 'plink' => 'http://localhost/display/4', + 'plink' => 'https://friendica.local/display/4', ], [ 'uri-id' => 5, 'body' => '[share]Shared status[/share]', - 'plink' => 'http://localhost/display/5', + 'plink' => 'https://friendica.local/display/5', ], [ 'uri-id' => 6, 'body' => 'Friend user status', - 'plink' => 'http://localhost/display/6', + 'plink' => 'https://friendica.local/display/6', ], [ 'uri-id' => 7, @@ -366,7 +365,7 @@ return [ 'suscipit aut facilis ut inventore omnis exercitationem quo magnam ' . 'consequatur maxime aut illum soluta quaerat natus unde aspernatur ' . 'et sed beatae nihil ullam temporibus corporis ratione blanditiis', - 'plink' => 'http://localhost/display/6', + 'plink' => 'https://friendica.local/display/6', ], [ 'uri-id' => 100, @@ -382,9 +381,9 @@ return [ 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -397,9 +396,9 @@ return [ 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -412,7 +411,7 @@ return [ 'thr-parent-id' => 3, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, + 'owner-id' => 43, 'author-id' => 43, 'causer-id' => 43, 'vid' => 8, @@ -427,9 +426,9 @@ return [ 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 44, - 'causer-id' => 44, + 'owner-id' => 43, + 'author-id' => 45, + 'causer-id' => 45, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -442,9 +441,9 @@ return [ 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -457,9 +456,9 @@ return [ 'thr-parent-id' => 6, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 44, - 'causer-id' => 44, + 'owner-id' => 43, + 'author-id' => 45, + 'causer-id' => 45, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -472,9 +471,9 @@ return [ 'thr-parent-id' => 7, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 44, - 'causer-id' => 44, + 'owner-id' => 43, + 'author-id' => 45, + 'causer-id' => 45, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -487,10 +486,10 @@ return [ 'id' => 1, 'uri-id' => 1, 'visible' => 1, - 'contact-id' => 42, - 'author-id' => 42, - 'owner-id' => 42, - 'causer-id' => 42, + 'contact-id' => 43, + 'author-id' => 43, + 'owner-id' => 43, + 'causer-id' => 43, 'uid' => 42, 'vid' => 8, 'unseen' => 1, @@ -507,16 +506,16 @@ return [ 'id' => 2, 'uri-id' => 2, 'uid' => 42, - 'contact-id' => 42, + 'contact-id' => 43, 'unseen' => 0, 'origin' => 1, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -535,7 +534,7 @@ return [ 'thr-parent-id' => 3, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, + 'owner-id' => 43, 'author-id' => 43, 'causer-id' => 43, 'vid' => 8, @@ -549,16 +548,16 @@ return [ 'id' => 4, 'uri-id' => 4, 'uid' => 42, - 'contact-id' => 44, + 'contact-id' => 45, 'unseen' => 0, 'origin' => 1, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 44, - 'causer-id' => 44, + 'owner-id' => 43, + 'author-id' => 45, + 'causer-id' => 45, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -570,16 +569,16 @@ return [ 'id' => 5, 'uri-id' => 5, 'uid' => 42, - 'contact-id' => 42, + 'contact-id' => 43, 'unseen' => 0, 'origin' => 1, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -591,16 +590,16 @@ return [ 'id' => 6, 'uri-id' => 6, 'uid' => 42, - 'contact-id' => 44, + 'contact-id' => 45, 'unseen' => 0, 'origin' => 1, 'parent-uri-id' => 6, 'thr-parent-id' => 6, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 44, - 'causer-id' => 44, + 'owner-id' => 43, + 'author-id' => 45, + 'causer-id' => 45, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -612,16 +611,16 @@ return [ 'id' => 7, 'uri-id' => 1, 'uid' => 0, - 'contact-id' => 42, + 'contact-id' => 43, 'unseen' => 1, 'origin' => 0, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -633,16 +632,16 @@ return [ 'id' => 8, 'uri-id' => 2, 'uid' => 0, - 'contact-id' => 42, + 'contact-id' => 43, 'unseen' => 0, 'origin' => 0, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -661,7 +660,7 @@ return [ 'thr-parent-id' => 3, 'gravity' => Item::GRAVITY_PARENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, + 'owner-id' => 43, 'author-id' => 43, 'causer-id' => 43, 'vid' => 8, @@ -675,16 +674,16 @@ return [ 'id' => 10, 'uri-id' => 4, 'uid' => 0, - 'contact-id' => 44, + 'contact-id' => 45, 'unseen' => 0, 'origin' => 0, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 44, - 'causer-id' => 44, + 'owner-id' => 43, + 'author-id' => 45, + 'causer-id' => 45, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -696,16 +695,16 @@ return [ 'id' => 11, 'uri-id' => 5, 'uid' => 0, - 'contact-id' => 42, + 'contact-id' => 43, 'unseen' => 0, 'origin' => 0, 'parent-uri-id' => 1, 'thr-parent-id' => 1, 'gravity' => Item::GRAVITY_COMMENT, 'network' => Protocol::DFRN, - 'owner-id' => 42, - 'author-id' => 42, - 'causer-id' => 42, + 'owner-id' => 43, + 'author-id' => 43, + 'causer-id' => 43, 'vid' => 8, 'private' => Item::PUBLIC, 'global' => true, @@ -717,10 +716,10 @@ return [ 'id' => 12, 'uri-id' => 6, 'visible' => 1, - 'contact-id' => 44, - 'author-id' => 44, - 'owner-id' => 42, - 'causer-id' => 44, + 'contact-id' => 45, + 'author-id' => 45, + 'owner-id' => 43, + 'causer-id' => 45, 'uid' => 0, 'vid' => 8, 'unseen' => 0, @@ -738,10 +737,10 @@ return [ 'id' => 13, 'uri-id' => 7, 'visible' => 1, - 'contact-id' => 44, - 'author-id' => 44, - 'owner-id' => 42, - 'causer-id' => 44, + 'contact-id' => 45, + 'author-id' => 45, + 'owner-id' => 43, + 'causer-id' => 45, 'uid' => 0, 'vid' => 8, 'unseen' => 0, @@ -760,10 +759,10 @@ return [ 'id' => 14, 'uri-id' => 100, 'visible' => 1, - 'contact-id' => 44, - 'author-id' => 44, - 'owner-id' => 42, - 'causer-id' => 44, + 'contact-id' => 45, + 'author-id' => 45, + 'owner-id' => 43, + 'causer-id' => 45, 'uid' => 0, 'vid' => 8, 'unseen' => 0, @@ -781,9 +780,9 @@ return [ 'post-thread' => [ [ 'uri-id' => 1, - 'author-id' => 42, - 'owner-id' => 42, - 'causer-id' => 42, + 'author-id' => 43, + 'owner-id' => 43, + 'causer-id' => 43, 'network' => Protocol::DFRN, ], [ @@ -795,16 +794,16 @@ return [ ], [ 'uri-id' => 6, - 'author-id' => 44, - 'owner-id' => 44, - 'causer-id' => 44, + 'author-id' => 45, + 'owner-id' => 45, + 'causer-id' => 45, 'network' => Protocol::DFRN, ], [ 'uri-id' => 7, - 'author-id' => 44, - 'owner-id' => 44, - 'causer-id' => 44, + 'author-id' => 45, + 'owner-id' => 45, + 'causer-id' => 45, 'network' => Protocol::DFRN, ], ], @@ -814,10 +813,10 @@ return [ 'uid' => 42, 'wall' => 1, 'post-user-id' => 1, - 'author-id' => 42, - 'owner-id' => 42, - 'causer-id' => 42, - 'contact-id' => 42, + 'author-id' => 43, + 'owner-id' => 43, + 'causer-id' => 43, + 'contact-id' => 43, 'network' => Protocol::DFRN, 'starred' => 1, 'origin' => 1, @@ -840,10 +839,10 @@ return [ 'uid' => 42, 'wall' => 1, 'post-user-id' => 6, - 'author-id' => 44, - 'owner-id' => 44, - 'causer-id' => 44, - 'contact-id' => 44, + 'author-id' => 45, + 'owner-id' => 45, + 'causer-id' => 45, + 'contact-id' => 45, 'network' => Protocol::DFRN, 'starred' => 0, 'origin' => 1, @@ -853,10 +852,10 @@ return [ 'uid' => 0, 'wall' => 0, 'post-user-id' => 7, - 'author-id' => 42, - 'owner-id' => 42, - 'causer-id' => 42, - 'contact-id' => 42, + 'author-id' => 43, + 'owner-id' => 43, + 'causer-id' => 43, + 'contact-id' => 43, 'network' => Protocol::DFRN, 'starred' => 0, 'origin' => 0, @@ -879,10 +878,10 @@ return [ 'uid' => 0, 'wall' => 0, 'post-user-id' => 12, - 'author-id' => 44, - 'owner-id' => 44, - 'causer-id' => 44, - 'contact-id' => 44, + 'author-id' => 45, + 'owner-id' => 45, + 'causer-id' => 45, + 'contact-id' => 45, 'network' => Protocol::DFRN, 'starred' => 0, 'origin' => 0, @@ -892,10 +891,10 @@ return [ 'uid' => 42, 'wall' => 1, 'post-user-id' => 7, - 'author-id' => 44, - 'owner-id' => 44, - 'causer-id' => 44, - 'contact-id' => 44, + 'author-id' => 45, + 'owner-id' => 45, + 'causer-id' => 45, + 'contact-id' => 45, 'network' => Protocol::DFRN, 'starred' => 0, 'origin' => 1, @@ -905,10 +904,10 @@ return [ 'uid' => 0, 'wall' => 0, 'post-user-id' => 12, - 'author-id' => 44, - 'owner-id' => 44, - 'causer-id' => 44, - 'contact-id' => 44, + 'author-id' => 45, + 'owner-id' => 45, + 'causer-id' => 45, + 'contact-id' => 45, 'network' => Protocol::DFRN, 'starred' => 0, 'origin' => 0, @@ -919,12 +918,12 @@ return [ 'id' => 1, 'type' => 8, 'name' => 'Friend contact', - 'url' => 'http://localhost/profile/friendcontact', - 'photo' => 'http://localhost/', + 'url' => 'https://friendica.local/profile/friendcontact', + 'photo' => 'https://friendica.local/', 'date' => '2020-01-01 12:12:02', 'msg' => 'A test reply from an item', 'uid' => 42, - 'link' => 'http://localhost/display/1', + 'link' => 'https://friendica.local/display/1', 'iid' => 4, 'seen' => 0, 'verb' => \Friendica\Protocol\Activity::POST, @@ -935,8 +934,9 @@ return [ ], 'profile' => [ [ - 'id' => 1, - 'uid' => 42, + 'id' => 1, + 'uid' => 42, + 'locality' => 'DFRN', ], ], 'group' => [ @@ -955,18 +955,18 @@ return [ ], 'group_member' => [ [ - 'id' => 1, - 'gid' => 1, - 'contact-id' => 42, + 'id' => 1, + 'gid' => 1, + 'contact-id' => 43, ], [ - 'id' => 2, - 'gid' => 1, - 'contact-id' => 42, + 'id' => 2, + 'gid' => 1, + 'contact-id' => 43, ], [ - 'id' => 3, - 'gid' => 2, + 'id' => 3, + 'gid' => 2, 'contact-id' => 43, ], ], diff --git a/tests/datasets/config/A.config.php b/tests/datasets/config/A.config.php index f28e1f2e85..80529a10ed 100644 --- a/tests/datasets/config/A.config.php +++ b/tests/datasets/config/A.config.php @@ -1,5 +1,9 @@ [ 'hostname' => 'testhost', diff --git a/tests/datasets/config/B.config.php b/tests/datasets/config/B.config.php index 59fadcf55e..449535f4ce 100644 --- a/tests/datasets/config/B.config.php +++ b/tests/datasets/config/B.config.php @@ -1,5 +1,9 @@ [ 'hostname' => 'testhost', diff --git a/tests/datasets/config/node.config.php b/tests/datasets/config/node.config.php index 335ef19c9d..3126d663e2 100644 --- a/tests/datasets/config/node.config.php +++ b/tests/datasets/config/node.config.php @@ -1,5 +1,9 @@ [ 'hostname' => 'localhost', diff --git a/tests/datasets/config/transformer/C.node.config.php b/tests/datasets/config/transformer/C.node.config.php index 6d93d8d7ad..0b2964acd2 100644 --- a/tests/datasets/config/transformer/C.node.config.php +++ b/tests/datasets/config/transformer/C.node.config.php @@ -1,5 +1,9 @@ [ 'hostname' => 'friendica.local', @@ -13,7 +17,6 @@ return [ 'temppath' => '/tmp/friendica.local', 'theme' => 'frio', 'url' => 'https://friendica.local', - 'urlpath' => '', 'build' => 1508, 'maintenance' => false, 'dbupdate' => 1, diff --git a/tests/datasets/config/transformer/D.node.config.php b/tests/datasets/config/transformer/D.node.config.php index 9e5707a9bb..a167b7d659 100644 --- a/tests/datasets/config/transformer/D.node.config.php +++ b/tests/datasets/config/transformer/D.node.config.php @@ -1,5 +1,9 @@ [ 'string_1_not_true' => '1', diff --git a/tests/datasets/config/transformer/object.node.config.php b/tests/datasets/config/transformer/object.node.config.php index f1807199bc..21dbf89ac8 100644 --- a/tests/datasets/config/transformer/object.node.config.php +++ b/tests/datasets/config/transformer/object.node.config.php @@ -1,5 +1,9 @@ [ 'objects_not_supported' => new stdClass(), diff --git a/tests/datasets/config/transformer/object_valid.node.config.php b/tests/datasets/config/transformer/object_valid.node.config.php index bfe61f3c8d..4e2eb6a126 100644 --- a/tests/datasets/config/transformer/object_valid.node.config.php +++ b/tests/datasets/config/transformer/object_valid.node.config.php @@ -1,5 +1,9 @@ [ 'resources_not_allowed' => new \GuzzleHttp\Psr7\AppendStream(), diff --git a/tests/datasets/config/transformer/small_types.node.config.php b/tests/datasets/config/transformer/small_types.node.config.php index 4bf92e3b9b..9a78f40faf 100644 --- a/tests/datasets/config/transformer/small_types.node.config.php +++ b/tests/datasets/config/transformer/small_types.node.config.php @@ -1,5 +1,9 @@ [ [ diff --git a/tests/datasets/config/transformer/types.node.config.php b/tests/datasets/config/transformer/types.node.config.php index d2a7dfe57d..dd099ee1d8 100644 --- a/tests/datasets/config/transformer/types.node.config.php +++ b/tests/datasets/config/transformer/types.node.config.php @@ -1,5 +1,9 @@ [ 'bool_true' => true, diff --git a/tests/datasets/crypto/rsa/diaspora-public-pem b/tests/datasets/crypto/rsa/diaspora-public-pem deleted file mode 100644 index 09dd1640d3..0000000000 --- a/tests/datasets/crypto/rsa/diaspora-public-pem +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDReSjW7O4u4tK+UGKwogyw4Dok -j1Z4f70INc4CTlHk2sngzTa3uMzk1EU+9nYigqMfI1/DYoSCC0ZqikvZVGkrMJj6 -khM7orTasR4Av9Sn54rOQaM+raUC3JXd9AdkdXx1IBC71cAXVqIg/ERCrrUpxDxc -E6VXs4mFWpDHJ4q01QIDAQAB ------END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/datasets/crypto/rsa/diaspora-public-rsa-base64 b/tests/datasets/crypto/rsa/diaspora-public-rsa-base64 deleted file mode 100644 index ba835a4711..0000000000 --- a/tests/datasets/crypto/rsa/diaspora-public-rsa-base64 +++ /dev/null @@ -1 +0,0 @@ -LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tDQpNSUdKQW9HQkFORjVLTmJzN2k3aTByNVFZckNpRExEZ09pU1BWbmgvdlFnMXpnSk9VZVRheWVETk5yZTR6T1RVDQpSVDcyZGlLQ294OGpYOE5paElJTFJtcUtTOWxVYVNzd21QcVNFenVpdE5xeEhnQy8xS2ZuaXM1Qm96NnRwUUxjDQpsZDMwQjJSMWZIVWdFTHZWd0JkV29pRDhSRUt1dFNuRVBGd1RwVmV6aVlWYWtNY25pclRWQWdNQkFBRT0NCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0 \ No newline at end of file diff --git a/tests/datasets/curl/about.head.php b/tests/datasets/curl/about.head.php index 2369bb65c3..e80257e1e9 100644 --- a/tests/datasets/curl/about.head.php +++ b/tests/datasets/curl/about.head.php @@ -1,5 +1,9 @@ [''], 'date' => ['Thu, 11 Oct 2018 18:43:54 GMT'], diff --git a/tests/datasets/curl/about.redirect.php b/tests/datasets/curl/about.redirect.php index 63ae12637f..776e869237 100644 --- a/tests/datasets/curl/about.redirect.php +++ b/tests/datasets/curl/about.redirect.php @@ -1,5 +1,9 @@ [''], 'date' => ['Thu, 11 Oct 2018 18:43:54 GMT'], diff --git a/tests/datasets/legacy/legacy.php b/tests/datasets/legacy/legacy.php index 4b0a76e609..fbed88b70d 100644 --- a/tests/datasets/legacy/legacy.php +++ b/tests/datasets/legacy/legacy.php @@ -1,5 +1,9 @@ [ [ diff --git a/tests/datasets/photo/photo.fixture.php b/tests/datasets/photo/photo.fixture.php index b8c14e6ad6..8e280a5266 100644 --- a/tests/datasets/photo/photo.fixture.php +++ b/tests/datasets/photo/photo.fixture.php @@ -1,5 +1,9 @@ [ [ diff --git a/tests/datasets/storage/database.fixture.php b/tests/datasets/storage/database.fixture.php index 9fbd7f983f..6fa9498ae9 100644 --- a/tests/datasets/storage/database.fixture.php +++ b/tests/datasets/storage/database.fixture.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later use Friendica\Core\Protocol; use Friendica\Model\Contact; diff --git a/tests/functional/DependencyCheckTest.php b/tests/functional/DependencyCheckTest.php index 5d0b63a6af..ec27d205d6 100644 --- a/tests/functional/DependencyCheckTest.php +++ b/tests/functional/DependencyCheckTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\functional; @@ -28,14 +14,14 @@ use Friendica\Core\Config\ValueObject\Cache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Database\Database; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Friendica\Util\BasePath; use Friendica\Core\Config\Util\ConfigFileManager; use Psr\Log\LoggerInterface; -class DependencyCheckTest extends FixtureTest +class DependencyCheckTest extends FixtureTestCase { - protected function setUp() : void + protected function setUp(): void { parent::setUp(); @@ -132,18 +118,6 @@ class DependencyCheckTest extends FixtureTest self::assertInstanceOf(LoggerInterface::class, $logger); } - public function testDevLogger() - { - /** @var IManageConfigValues $config */ - $config = $this->dice->create(IManageConfigValues::class); - $config->set('system', 'dlogfile', $this->root->url() . '/friendica.log'); - - /** @var LoggerInterface $logger */ - $logger = $this->dice->create('$devLogger', ['dev']); - - self::assertInstanceOf(LoggerInterface::class, $logger); - } - public function testCache() { /** @var ICanCache $cache */ diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 6f16c7a73e..f163ae5bea 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -7,10 +7,16 @@ timeoutForMediumTests="900" timeoutForLargeTests="900" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> - - functional/ - src/ - + + + functional/ + src/ + Unit/ + + + Unit/ + + diff --git a/tests/src/App/ArgumentsTest.php b/tests/src/App/ArgumentsTest.php index a066c04e2d..078a58529c 100644 --- a/tests/src/App/ArgumentsTest.php +++ b/tests/src/App/ArgumentsTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\App; diff --git a/tests/src/App/BaseURLTest.php b/tests/src/App/BaseURLTest.php index fb79fd4d3c..9c3333feff 100644 --- a/tests/src/App/BaseURLTest.php +++ b/tests/src/App/BaseURLTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\App; @@ -25,10 +11,10 @@ use Friendica\App\BaseURL; use Friendica\Core\Config\Model\ReadOnlyFileConfig; use Friendica\Core\Config\ValueObject\Cache; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Psr\Log\NullLogger; -class BaseURLTest extends MockedTest +class BaseURLTest extends MockedTestCase { public function dataSystemUrl(): array { diff --git a/tests/src/App/ModeTest.php b/tests/src/App/ModeTest.php index c70ebf6f6d..b1ddca1d33 100644 --- a/tests/src/App/ModeTest.php +++ b/tests/src/App/ModeTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\App; @@ -26,13 +12,13 @@ use Friendica\App\Arguments; use Friendica\App\Mode; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Database\Database; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\VFSTrait; use Friendica\Util\BasePath; use Mockery; use Mockery\MockInterface; -class ModeTest extends MockedTest +class ModeTest extends MockedTestCase { use VFSTrait; diff --git a/tests/src/App/RequestTest.php b/tests/src/App/RequestTest.php index 43e06a74fe..648d2b6386 100644 --- a/tests/src/App/RequestTest.php +++ b/tests/src/App/RequestTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\App; use Friendica\App\Request; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class RequestTest extends MockedTest +class RequestTest extends MockedTestCase { public function dataServerArray(): array { diff --git a/tests/src/App/RouterTest.php b/tests/src/App/RouterTest.php index e0416dbb32..e312823b8d 100644 --- a/tests/src/App/RouterTest.php +++ b/tests/src/App/RouterTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\App; diff --git a/tests/src/BaseCollectionTest.php b/tests/src/BaseCollectionTest.php index ce7fb4670e..012725903e 100644 --- a/tests/src/BaseCollectionTest.php +++ b/tests/src/BaseCollectionTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src; diff --git a/tests/src/CollectionTest.php b/tests/src/CollectionTest.php index f4fd6ab248..f82bb5dfea 100644 --- a/tests/src/CollectionTest.php +++ b/tests/src/CollectionTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\CollectionDouble; use Friendica\Test\Util\EntityDouble; -class CollectionTest extends MockedTest +class CollectionTest extends MockedTestCase { /** * Test if the BaseCollection::column() works as expected diff --git a/tests/src/Console/AutomaticInstallationConsoleTest.php b/tests/src/Console/AutomaticInstallationConsoleTest.php index b4ad96dcaf..f3466c0203 100644 --- a/tests/src/Console/AutomaticInstallationConsoleTest.php +++ b/tests/src/Console/AutomaticInstallationConsoleTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Console; @@ -27,18 +13,17 @@ use Friendica\Console\AutomaticInstallation; use Friendica\Core\Config\ValueObject\Cache; use Friendica\Core\Installer; use Friendica\Core\L10n; -use Friendica\Core\Logger; use Friendica\Database\Database; use Friendica\DI; +use Friendica\Test\ConsoleTestCase; use Friendica\Test\Util\RendererMockTrait; use Friendica\Test\Util\VFSTrait; use Mockery; use Mockery\MockInterface; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamFile; -use Psr\Log\NullLogger; -class AutomaticInstallationConsoleTest extends ConsoleTest +class AutomaticInstallationConsoleTest extends ConsoleTestCase { use VFSTrait; use RendererMockTrait; @@ -72,13 +57,14 @@ class AutomaticInstallationConsoleTest extends ConsoleTest */ private $dice; - public function setUp() : void + public function setUp(): void { static::markTestSkipped('Needs class \'Installer\' as constructing argument for console tests'); parent::setUp(); - $this->setUpVfsDir();; + $this->setUpVfsDir(); + ; if ($this->root->hasChild('config' . DIRECTORY_SEPARATOR . 'local.config.php')) { $this->root->getChild('config') @@ -90,8 +76,8 @@ class AutomaticInstallationConsoleTest extends ConsoleTest $l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); $this->dice->shouldReceive('create') - ->with(L10n::class) - ->andReturn($l10nMock); + ->with(L10n::class) + ->andReturn($l10nMock); DI::init($this->dice); @@ -117,7 +103,6 @@ class AutomaticInstallationConsoleTest extends ConsoleTest }); $this->mode->shouldReceive('isInstall')->andReturn(true); - Logger::init(new NullLogger()); } /** @@ -131,11 +116,11 @@ class AutomaticInstallationConsoleTest extends ConsoleTest 'empty' => [ 'data' => [ 'database' => [ - 'hostname' => '', - 'username' => '', - 'password' => '', - 'database' => '', - 'port' => '', + 'hostname' => '', + 'username' => '', + 'password' => '', + 'database' => '', + 'port' => '', ], 'config' => [ 'php_path' => '', @@ -143,23 +128,22 @@ class AutomaticInstallationConsoleTest extends ConsoleTest 'admin_email' => '', ], 'system' => [ - 'basepath' => '', - 'urlpath' => '', - 'url' => 'http://friendica.local', - 'ssl_policy' => 0, + 'basepath' => '', + 'url' => 'http://friendica.local', + 'ssl_policy' => 0, 'default_timezone' => '', - 'language' => '', + 'language' => '', ], ], ], 'normal' => [ 'data' => [ 'database' => [ - 'hostname' => 'testhost', - 'port' => 3306, - 'username' => 'friendica', - 'password' => 'a password', - 'database' => 'database', + 'hostname' => 'testhost', + 'port' => 3306, + 'username' => 'friendica', + 'password' => 'a password', + 'database' => 'database', ], 'config' => [ 'php_path' => '', @@ -167,23 +151,22 @@ class AutomaticInstallationConsoleTest extends ConsoleTest 'admin_email' => 'admin@philipp.info', ], 'system' => [ - 'urlpath' => 'test/it', - 'url' => 'http://friendica.local/test/it', - 'basepath' => '', - 'ssl_policy' => '2', + 'url' => 'http://friendica.local/test/it', + 'basepath' => '', + 'ssl_policy' => '2', 'default_timezone' => 'en', - 'language' => 'Europe/Berlin', + 'language' => 'Europe/Berlin', ], ], ], 'special' => [ 'data' => [ 'database' => [ - 'hostname' => 'testhost.new.domain', - 'port' => 3341, - 'username' => 'fr"§%ica', - 'password' => '$%\"gse', - 'database' => 'db', + 'hostname' => 'testhost.new.domain', + 'port' => 3341, + 'username' => 'fr"§%ica', + 'password' => '$%\"gse', + 'database' => 'db', ], 'config' => [ 'php_path' => '', @@ -191,12 +174,11 @@ class AutomaticInstallationConsoleTest extends ConsoleTest 'admin_email' => 'admin@philipp.info', ], 'system' => [ - 'urlpath' => 'test/it', - 'url' => 'https://friendica.local/test/it', - 'basepath' => '', - 'ssl_policy' => '1', + 'url' => 'https://friendica.local/test/it', + 'basepath' => '', + 'ssl_policy' => '1', 'default_timezone' => 'en', - 'language' => 'Europe/Berlin', + 'language' => 'Europe/Berlin', ], ], ], @@ -287,7 +269,7 @@ Creating config file... Checking database... [Error] -------- -Could not connect to database.: +Could not connect to database.: FIN; @@ -367,7 +349,6 @@ FIN; self::assertConfigEntry('system', 'default_timezone', $assertion, ($default) ? Installer::DEFAULT_TZ : null); self::assertConfigEntry('system', 'language', $assertion, ($default) ? Installer::DEFAULT_LANG : null); self::assertConfigEntry('system', 'url', $assertion); - self::assertConfigEntry('system', 'urlpath', $assertion); self::assertConfigEntry('system', 'ssl_policy', $assertion, ($default) ? App\BaseURL::DEFAULT_SSL_SCHEME : null); self::assertConfigEntry('system', 'basepath', ($realBasepath) ? $this->root->url() : $assertion); } @@ -461,7 +442,6 @@ return [ ], 'system' => [ 'basepath' => '{$conf('system', 'basepath')}', - 'urlpath' => '{$conf('system', 'urlpath')}', 'url' => '{$conf('system', 'url')}', 'ssl_policy' => '{$conf('system', 'ssl_policy')}', 'default_timezone' => '{$conf('system', 'default_timezone')}', @@ -578,22 +558,22 @@ CONF; $console = new AutomaticInstallation($this->consoleArgv); - $option = function($var, $cat, $key) use ($data, $console) { + $option = function ($var, $cat, $key) use ($data, $console) { if (!empty($data[$cat][$key])) { $console->setOption($var, $data[$cat][$key]); } }; - $option('dbhost' , 'database', 'hostname'); - $option('dbport' , 'database', 'port'); - $option('dbuser' , 'database', 'username'); - $option('dbpass' , 'database', 'password'); - $option('dbdata' , 'database', 'database'); - $option('url' , 'system' , 'url'); - $option('phppath' , 'config' , 'php_path'); - $option('admin' , 'config' , 'admin_email'); - $option('tz' , 'system' , 'default_timezone'); - $option('lang' , 'system' , 'language'); - $option('basepath' , 'system' , 'basepath'); + $option('dbhost', 'database', 'hostname'); + $option('dbport', 'database', 'port'); + $option('dbuser', 'database', 'username'); + $option('dbpass', 'database', 'password'); + $option('dbdata', 'database', 'database'); + $option('url', 'system', 'url'); + $option('phppath', 'config', 'php_path'); + $option('admin', 'config', 'admin_email'); + $option('tz', 'system', 'default_timezone'); + $option('lang', 'system', 'language'); + $option('basepath', 'system', 'basepath'); $txt = $this->dumpExecute($console); @@ -619,7 +599,7 @@ CONF; self::assertStuckDB($txt); self::assertTrue($this->root->hasChild('config' . DIRECTORY_SEPARATOR . 'local.config.php')); - self::assertConfig(['config' => ['hostname' => 'friendica.local'], 'system' => ['url' => 'http://friendica.local', 'ssl_policy' => 0, 'urlpath' => '']], false, true, false, true); + self::assertConfig(['config' => ['hostname' => 'friendica.local'], 'system' => ['url' => 'http://friendica.local', 'ssl_policy' => 0]], false, true, false, true); } public function testGetHelp() @@ -647,12 +627,12 @@ Options -d|--dbdata The name of the mysql/mariadb database (env MYSQL_DATABASE) -U|--dbuser The username of the mysql/mariadb database login (env MYSQL_USER or MYSQL_USERNAME) -P|--dbpass The password of the mysql/mariadb database login (env MYSQL_PASSWORD) - -U|--url The full base URL of Friendica - f.e. 'https://friendica.local/sub' (env FRIENDICA_URL) + -U|--url The full base URL of Friendica - f.e. 'https://friendica.local/sub' (env FRIENDICA_URL) -B|--phppath The path of the PHP binary (env FRIENDICA_PHP_PATH) -b|--basepath The basepath of Friendica (env FRIENDICA_BASE_PATH) -t|--tz The timezone of Friendica (env FRIENDICA_TZ) -L|--lang The language of Friendica (env FRIENDICA_LANG) - + Environment variables MYSQL_HOST The host of the mysql/mariadb database (mandatory if mysql and environment is used) MYSQL_PORT The port of the mysql/mariadb database @@ -665,7 +645,7 @@ Environment variables FRIENDICA_ADMIN_MAIL The admin email address of Friendica (this email will be used for admin access) FRIENDICA_TZ The timezone of Friendica FRIENDICA_LANG The langauge of Friendica - + Examples bin/console autoinstall -f 'input.config.php Installs Friendica with the prepared 'input.config.php' file diff --git a/tests/src/Console/ConfigConsoleTest.php b/tests/src/Console/ConfigConsoleTest.php index b26b6baf3f..3c983e04af 100644 --- a/tests/src/Console/ConfigConsoleTest.php +++ b/tests/src/Console/ConfigConsoleTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Console; @@ -25,11 +11,12 @@ use Friendica\App; use Friendica\App\Mode; use Friendica\Console\Config; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Test\ConsoleTestCase; use Mockery; use Mockery\LegacyMockInterface; use Mockery\MockInterface; -class ConfigConsoleTest extends ConsoleTest +class ConfigConsoleTest extends ConsoleTestCase { /** * @var App\Mode|MockInterface $appMode diff --git a/tests/src/Console/ConsoleTest.php b/tests/src/Console/ConsoleTest.php deleted file mode 100644 index 8721abb20a..0000000000 --- a/tests/src/Console/ConsoleTest.php +++ /dev/null @@ -1,58 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Console; - -use Asika\SimpleConsole\Console; -use Friendica\Test\MockedTest; -use Friendica\Test\Util\Intercept; - -abstract class ConsoleTest extends MockedTest -{ - /** - * @var array The default argv for a Console Instance - */ - protected $consoleArgv = [ 'consoleTest.php' ]; - - protected function setUp() : void - { - parent::setUp(); - - Intercept::setUp(); - } - - /** - * Dumps the execution of an console output to a string and returns it - * - * @param Console $console The current console instance - * - * @return string the output of the execution - */ - protected function dumpExecute(Console $console) - { - Intercept::reset(); - $console->execute(); - $returnStr = Intercept::$cache; - Intercept::reset(); - - return $returnStr; - } -} diff --git a/tests/src/Console/LockConsoleTest.php b/tests/src/Console/LockConsoleTest.php index 29e2a90cb8..d059de696f 100644 --- a/tests/src/Console/LockConsoleTest.php +++ b/tests/src/Console/LockConsoleTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Console; @@ -25,10 +11,11 @@ use Friendica\App; use Friendica\App\Mode; use Friendica\Console\Lock; use Friendica\Core\Lock\Capability\ICanLock; +use Friendica\Test\ConsoleTestCase; use Mockery; use Mockery\MockInterface; -class LockConsoleTest extends ConsoleTest +class LockConsoleTest extends ConsoleTestCase { /** * @var App\Mode|MockInterface $appMode diff --git a/tests/src/Console/ServerBlockConsoleTest.php b/tests/src/Console/ServerBlockConsoleTest.php index 6179cf733e..cbbb8bdb39 100644 --- a/tests/src/Console/ServerBlockConsoleTest.php +++ b/tests/src/Console/ServerBlockConsoleTest.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Console; use Friendica\Console\ServerBlock; use Friendica\Moderation\DomainPatternBlocklist; +use Friendica\Test\ConsoleTestCase; use Friendica\Test\FixtureTestTrait; use Mockery; -class ServerBlockConsoleTest extends ConsoleTest +class ServerBlockConsoleTest extends ConsoleTestCase { use FixtureTestTrait; diff --git a/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php b/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php index 25ed555551..dd4102d0ab 100644 --- a/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php +++ b/tests/src/Contact/FriendSuggest/Factory/FriendSuggestTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Contact\FriendSuggest\Factory; use Friendica\Contact\FriendSuggest\Factory\FriendSuggest; use Friendica\Contact\FriendSuggest\Entity; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Psr\Log\NullLogger; -class FriendSuggestTest extends MockedTest +class FriendSuggestTest extends MockedTestCase { public function dataCreate() { diff --git a/tests/src/Contact/Introduction/Factory/IntroductionTest.php b/tests/src/Contact/Introduction/Factory/IntroductionTest.php index c9c3f6acb3..311483f019 100644 --- a/tests/src/Contact/Introduction/Factory/IntroductionTest.php +++ b/tests/src/Contact/Introduction/Factory/IntroductionTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Contact\Introduction\Factory; diff --git a/tests/src/Content/ItemTest.php b/tests/src/Content/ItemTest.php index 66f6e40dac..c2adbccb15 100644 --- a/tests/src/Content/ItemTest.php +++ b/tests/src/Content/ItemTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class ItemTest extends MockedTest +class ItemTest extends MockedTestCase { /** * @doesNotPerformAssertions diff --git a/tests/src/Content/PageInfoMock.php b/tests/src/Content/PageInfoMock.php index 5a35e6b178..62c6ebfc48 100644 --- a/tests/src/Content/PageInfoMock.php +++ b/tests/src/Content/PageInfoMock.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content; diff --git a/tests/src/Content/PageInfoTest.php b/tests/src/Content/PageInfoTest.php index 701771049e..324381a4dd 100644 --- a/tests/src/Content/PageInfoTest.php +++ b/tests/src/Content/PageInfoTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content; -use Friendica\Test\DatabaseTest; +use Friendica\Test\DatabaseTestCase; -class PageInfoTest extends DatabaseTest +class PageInfoTest extends DatabaseTestCase { public function dataGetRelevantUrlFromBody() { diff --git a/tests/src/Content/SmiliesTest.php b/tests/src/Content/SmiliesTest.php index 820e378fb3..69236efe2d 100644 --- a/tests/src/Content/SmiliesTest.php +++ b/tests/src/Content/SmiliesTest.php @@ -1,37 +1,20 @@ . - * - * Created by PhpStorm. - * User: benlo - * Date: 25/03/19 - * Time: 21:36 - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content; use Friendica\Content\Smilies; use Friendica\Core\Hook; +use Friendica\Core\Hooks\HookEventBridge; use Friendica\DI; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class SmiliesTest extends FixtureTest +class SmiliesTest extends FixtureTestCase { protected function setUp(): void { @@ -39,22 +22,29 @@ class SmiliesTest extends FixtureTest DI::config()->set('system', 'no_smilies', false); + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = DI::eventDispatcher(); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } + Hook::register('smilie', 'tests/Util/SmileyWhitespaceAddon.php', 'add_test_unicode_smilies'); Hook::loadHooks(); } - public function dataLinks() + public function dataLinks(): array { return [ /** @see https://github.com/friendica/friendica/pull/6933 */ 'bug-6933-1' => [ - 'data' => '/', - 'smilies' => ['texts' => [], 'icons' => []], + 'data' => '/', + 'smilies' => ['texts' => [], 'icons' => []], 'expected' => '/', ], 'bug-6933-2' => [ - 'data' => 'code', - 'smilies' => ['texts' => [], 'icons' => []], + 'data' => 'code', + 'smilies' => ['texts' => [], 'icons' => []], 'expected' => 'code', ], ]; @@ -71,7 +61,7 @@ class SmiliesTest extends FixtureTest * * @throws InternalServerErrorException */ - public function testReplaceFromArray(string $text, array $smilies, string $expected) + public function testReplaceFromArray(string $text, array $smilies, string $expected): void { $output = Smilies::replaceFromArray($text, $smilies); self::assertEquals($expected, $output); @@ -82,141 +72,137 @@ class SmiliesTest extends FixtureTest return [ 'emoji' => [ 'expected' => true, - 'body' => '👀', + 'body' => '👀', ], 'emojis' => [ 'expected' => true, - 'body' => '👀🤷', + 'body' => '👀🤷', ], 'emoji+whitespace' => [ 'expected' => true, - 'body' => ' 👀 ', + 'body' => ' 👀 ', ], 'empty' => [ 'expected' => false, - 'body' => '', + 'body' => '', ], 'whitespace' => [ 'expected' => false, - 'body' => ' + 'body' => ' ', ], 'emoji+ASCII' => [ 'expected' => false, - 'body' => '🤷a', + 'body' => '🤷a', ], 'HTML entity whitespace' => [ 'expected' => false, - 'body' => ' ', + 'body' => ' ', ], 'HTML entity else' => [ 'expected' => false, - 'body' => '°', + 'body' => '°', ], 'emojis+HTML whitespace' => [ 'expected' => true, - 'body' => '👀 🤷', + 'body' => '👀 🤷', ], 'emojis+HTML else' => [ 'expected' => false, - 'body' => '👀<🤷', + 'body' => '👀<🤷', ], 'zwj' => [ 'expected' => true, - 'body' => '👨‍👨‍👧‍', + 'body' => '👨‍👨‍👧‍', ], 'zwj+whitespace' => [ 'expected' => true, - 'body' => ' 👨‍👨‍👧‍ ', + 'body' => ' 👨‍👨‍👧‍ ', ], 'zwj+HTML whitespace' => [ 'expected' => true, - 'body' => ' 👨‍👨‍👧‍ ', + 'body' => ' 👨‍👨‍👧‍ ', ], ]; } /** * @dataProvider dataIsEmojiPost - * - * @param bool $expected - * @param string $body - * @return void */ - public function testIsEmojiPost(bool $expected, string $body) + public function testIsEmojiPost(bool $expected, string $body): void { $this->assertEquals($expected, Smilies::isEmojiPost($body)); } - public function dataReplace(): array { $data = [ 'simple-1' => [ 'expected' => 'alt=":-p"', - 'body' => ':-p', + 'body' => ':-p', ], - 'simple-1' => [ + 'simple-2' => [ 'expected' => 'alt=":-p"', - 'body' => ' :-p ', + 'body' => ' :-p ', ], 'word-boundary-1' => [ 'expected' => ':-pppp', - 'body' => ':-pppp', + 'body' => ':-pppp', ], 'word-boundary-2' => [ 'expected' => '~friendicaca', - 'body' => '~friendicaca', + 'body' => '~friendicaca', ], 'symbol-boundary-1' => [ 'expected' => 'alt=":-p"', - 'body' => '(:-p)', + 'body' => '(:-p)', ], 'hearts-1' => [ 'expected' => '❤ (❤) ❤', - 'body' => '<3 (<3) <3', + 'body' => '<3 (<3) <3', ], 'hearts-8' => [ 'expected' => '(❤❤❤❤❤❤❤❤)', - 'body' => '(<33333333)', + 'body' => '(<33333333)', ], 'no-hearts-1' => [ 'expected' => '(<30)', - 'body' => '(<30)', + 'body' => '(<30)', ], 'no-hearts-2' => [ 'expected' => '(3<33)', - 'body' => '(3<33)', + 'body' => '(3<33)', ], 'space' => [ 'expected' => 'alt="smiley-heart"', - 'body' => ':smiley heart 333:', + 'body' => ':smiley heart 333:', ], 'substitution-1' => [ 'expected' => '🔥', - 'body' => '⽕', + 'body' => '⽕', ], 'substitution-2' => [ 'expected' => '🤗', - 'body' => ':hugging face:', + 'body' => ':hugging face:', ], 'substitution-3' => [ 'expected' => '🤭', - 'body' => ':face with hand over mouth:', + 'body' => ':face with hand over mouth:', ], 'mixed' => [ 'expected' => '🔥 🤭 invalid:hugging face: 🤗', - 'body' => '⽕ :face with hand over mouth: invalid:hugging face: :hugging face:', + 'body' => '⽕ :face with hand over mouth: invalid:hugging face: :hugging face:', ], ]; + foreach ([':-[', ':-D', 'o.O'] as $emoji) { foreach (['A', '_', ':', '-'] as $prefix) { foreach (['', ' ', 'A', ':', '-'] as $suffix) { $no_smile = ($prefix !== '' && ctype_alnum($prefix)) || ($suffix !== '' && ctype_alnum($suffix)); - $s = $prefix . $emoji . $suffix; - $data[] = [ + $s = $prefix . $emoji . $suffix; + $data[] = [ 'expected' => $no_smile ? $s : 'alt="' . $emoji . '"', - 'body' => $s, + 'body' => $s, ]; } } @@ -226,11 +212,8 @@ class SmiliesTest extends FixtureTest /** * @dataProvider dataReplace - * - * @param string $expected - * @param string $body */ - public function testReplace(string $expected, string $body) + public function testReplace(string $expected, string $body): void { $result = Smilies::replace($body); $this->assertStringContainsString($expected, $result); @@ -240,58 +223,58 @@ class SmiliesTest extends FixtureTest { return [ 'symbols' => [ - 'expected' => ['p', 'heart', 'embarrassed', 'kiss'], - 'body' => ':-p <3 ":-[:-"', + 'expected' => ['p', 'heart', 'embarrassed', 'kiss'], + 'body' => ':-p <3 ":-[:-"', 'normalized' => ':p: :heart: ":embarrassed::kiss:', ], 'single-smiley' => [ - 'expected' => ['like'], - 'body' => ':like', + 'expected' => ['like'], + 'body' => ':like', 'normalized' => ':like:', ], 'multiple-smilies' => [ - 'expected' => ['like', 'dislike'], - 'body' => ':like :dislike', + 'expected' => ['like', 'dislike'], + 'body' => ':like :dislike', 'normalized' => ':like: :dislike:', ], 'nosmile' => [ - 'expected' => [], - 'body' => '[nosmile] :like :like', + 'expected' => [], + 'body' => '[nosmile] :like :like', 'normalized' => '[nosmile] :like :like' ], 'in-code' => [ - 'expected' => [], - 'body' => '[code]:like :like :like[/code]', + 'expected' => [], + 'body' => '[code]:like :like :like[/code]', 'normalized' => '[code]:like :like :like[/code]' ], '~friendica' => [ - 'expected' => ['friendica'], - 'body' => '~friendica', + 'expected' => ['friendica'], + 'body' => '~friendica', 'normalized' => ':friendica:' ], 'space' => [ - 'expected' => ['smileyheart333'], - 'body' => ':smiley heart 333:', + 'expected' => ['smileyheart333'], + 'body' => ':smiley heart 333:', 'normalized' => ':smileyheart333:' ], 'substitution-1' => [ - 'expected' => [], - 'body' => '⽕', + 'expected' => [], + 'body' => '⽕', 'normalized' => '🔥', ], 'substitution-2' => [ - 'expected' => [], - 'body' => ':hugging face:', + 'expected' => [], + 'body' => ':hugging face:', 'normalized' => '🤗', ], 'substitution-3' => [ - 'expected' => [], - 'body' => ':face with hand over mouth:', + 'expected' => [], + 'body' => ':face with hand over mouth:', 'normalized' => '🤭', ], 'mixed' => [ - 'expected' => [], - 'body' => '⽕ :face with hand over mouth: invalid:hugging face: :hugging face:', + 'expected' => [], + 'body' => '⽕ :face with hand over mouth: invalid:hugging face: :hugging face:', 'normalized' => '🔥 🤭 invalid:hugging face: 🤗', ], ]; @@ -299,15 +282,11 @@ class SmiliesTest extends FixtureTest /** * @dataProvider dataExtractUsedSmilies - * - * @param array $expected - * @param string $body - * @param stirng $normalized */ - public function testExtractUsedSmilies(array $expected, string $body, string $normalized) + public function testExtractUsedSmilies(array $expected, string $body, string $normalized): void { $extracted = Smilies::extractUsedSmilies($body, $converted); - $expected = array_fill_keys($expected, true); + $expected = array_fill_keys($expected, true); $this->assertEquals($normalized, $converted); foreach (array_keys($extracted) as $shortcode) { $this->assertArrayHasKey($shortcode, $expected); diff --git a/tests/src/Content/Text/BBCode/VideoTest.php b/tests/src/Content/Text/BBCode/VideoTest.php index 6591504fa1..a2b4188d38 100644 --- a/tests/src/Content/Text/BBCode/VideoTest.php +++ b/tests/src/Content/Text/BBCode/VideoTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content\Text\BBCode; use Friendica\Content\Text\BBCode\Video; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class VideoTest extends MockedTest +class VideoTest extends MockedTestCase { public function dataVideo() { diff --git a/tests/src/Content/Text/BBCodeTest.php b/tests/src/Content/Text/BBCodeTest.php index fc8fd4bff5..ddce6975e9 100644 --- a/tests/src/Content/Text/BBCodeTest.php +++ b/tests/src/Content/Text/BBCodeTest.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content\Text; use Friendica\Content\Text\BBCode; use Friendica\DI; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; +use Friendica\Util\Strings; -class BBCodeTest extends FixtureTest +class BBCodeTest extends FixtureTestCase { /** @var \HTMLPurifier */ public $HTMLPurifier; @@ -35,7 +22,6 @@ class BBCodeTest extends FixtureTest { parent::setUp(); DI::config()->set('system', 'remove_multiplicated_lines', false); - DI::config()->set('system', 'no_oembed', false); DI::config()->set('system', 'allowed_link_protocols', []); DI::config()->set('system', 'url', 'https://friendica.local'); DI::config()->set('system', 'no_smilies', false); @@ -46,7 +32,7 @@ class BBCodeTest extends FixtureTest $config->set('HTML.Doctype', 'HTML5'); $config->set('Attr.AllowedRel', [ 'noreferrer' => true, - 'noopener' => true, + 'noopener' => true, ]); $config->set('Attr.AllowedFrameTargets', [ '_blank' => true, @@ -60,78 +46,78 @@ class BBCodeTest extends FixtureTest return [ /** @see https://github.com/friendica/friendica/issues/2487 */ 'bug-2487-1' => [ - 'data' => 'https://de.wikipedia.org/wiki/Juha_Sipilä', + 'data' => 'https://de.wikipedia.org/wiki/Juha_Sipilä', 'assertHTML' => true, ], 'bug-2487-2' => [ - 'data' => 'https://de.wikipedia.org/wiki/Dnepr_(Motorradmarke)', + 'data' => 'https://de.wikipedia.org/wiki/Dnepr_(Motorradmarke)', 'assertHTML' => true, ], 'bug-2487-3' => [ - 'data' => 'https://friendica.wäckerlin.ch/friendica', + 'data' => 'https://friendica.wäckerlin.ch/friendica', 'assertHTML' => true, ], 'bug-2487-4' => [ - 'data' => 'https://mastodon.social/@morevnaproject', + 'data' => 'https://mastodon.social/@morevnaproject', 'assertHTML' => true, ], /** @see https://github.com/friendica/friendica/issues/5795 */ 'bug-5795' => [ - 'data' => 'https://social.nasqueron.org/@liw/100798039015010628', + 'data' => 'https://social.nasqueron.org/@liw/100798039015010628', 'assertHTML' => true, ], /** @see https://github.com/friendica/friendica/issues/6095 */ 'bug-6095' => [ - 'data' => 'https://en.wikipedia.org/wiki/Solid_(web_decentralization_project)', + 'data' => 'https://en.wikipedia.org/wiki/Solid_(web_decentralization_project)', 'assertHTML' => true, ], 'no-protocol' => [ - 'data' => 'example.com/path', - 'assertHTML' => false + 'data' => 'example.com/path', + 'assertHTML' => false, ], 'wrong-protocol' => [ - 'data' => 'ftp://example.com', - 'assertHTML' => false + 'data' => 'ftp://example.com', + 'assertHTML' => false, ], 'wrong-domain-without-path' => [ - 'data' => 'http://example', - 'assertHTML' => false + 'data' => 'http://example', + 'assertHTML' => false, ], 'wrong-domain-with-path' => [ - 'data' => 'http://example/path', - 'assertHTML' => false + 'data' => 'http://example/path', + 'assertHTML' => false, ], 'bug-6857-domain-start' => [ - 'data' => "http://\nexample.com", - 'assertHTML' => false + 'data' => "http://\nexample.com", + 'assertHTML' => false, ], 'bug-6857-domain-end' => [ - 'data' => "http://example\n.com", - 'assertHTML' => false + 'data' => "http://example\n.com", + 'assertHTML' => false, ], 'bug-6857-tld' => [ - 'data' => "http://example.\ncom", - 'assertHTML' => false + 'data' => "http://example.\ncom", + 'assertHTML' => false, ], 'bug-6857-end' => [ - 'data' => "http://example.com\ntest", - 'assertHTML' => false + 'data' => "http://example.com\ntest", + 'assertHTML' => false, ], 'bug-6901' => [ - 'data' => "http://example.com
                                ", - 'assertHTML' => false + 'data' => "http://example.com
                                  ", + 'assertHTML' => false, ], 'bug-7150' => [ - 'data' => html_entity_decode('http://example.com ', ENT_QUOTES, 'UTF-8'), - 'assertHTML' => false + 'data' => html_entity_decode('http://example.com ', ENT_QUOTES, 'UTF-8'), + 'assertHTML' => false, ], 'bug-7271-query-string-brackets' => [ - 'data' => 'https://example.com/search?q=square+brackets+[url]', - 'assertHTML' => true + 'data' => 'https://example.com/search?q=square+brackets+[url]', + 'assertHTML' => true, ], 'bug-7271-path-brackets' => [ - 'data' => 'http://example.com/path/to/file[3].html', - 'assertHTML' => true + 'data' => 'http://example.com/path/to/file[3].html', + 'assertHTML' => true, ], ]; } @@ -149,7 +135,7 @@ class BBCodeTest extends FixtureTest public function testAutoLinking(string $data, bool $assertHTML) { $output = BBCode::convert($data); - $assert = $this->HTMLPurifier->purify('' . $data . ''); + $assert = $this->HTMLPurifier->purify('' . Strings::getStyledURL($data) . ''); if ($assertHTML) { self::assertEquals($assert, $output); } else { @@ -161,95 +147,95 @@ class BBCodeTest extends FixtureTest { return [ 'bug-7271-condensed-space' => [ - 'expectedHtml' => '
                                  1. http://example.com/
                                  ', - 'text' => '[ol][*] http://example.com/[/ol]', + 'expectedHtml' => '
                                  1. example.com/
                                  ', + 'text' => '[ol][li] http://example.com/[/ol]', ], 'bug-7271-condensed-nospace' => [ - 'expectedHtml' => '
                                  1. http://example.com/
                                  ', - 'text' => '[ol][*]http://example.com/[/ol]', + 'expectedHtml' => '
                                  1. example.com/
                                  ', + 'text' => '[ol][li]http://example.com/[/ol]', ], 'bug-7271-indented-space' => [ - 'expectedHtml' => '', - 'text' => '[ul] -[*] http://example.com/ + 'expectedHtml' => '', + 'text' => '[ul] +[li] http://example.com/ [/ul]', ], 'bug-7271-indented-nospace' => [ - 'expectedHtml' => '', - 'text' => '[ul] -[*]http://example.com/ + 'expectedHtml' => '', + 'text' => '[ul] +[li]http://example.com/ [/ul]', ], 'bug-2199-named-size' => [ 'expectedHtml' => 'Test text', - 'text' => '[size=xx-large]Test text[/size]', + 'text' => '[size=xx-large]Test text[/size]', ], 'bug-2199-numeric-size' => [ 'expectedHtml' => 'Test text', - 'text' => '[size=24]Test text[/size]', + 'text' => '[size=24]Test text[/size]', ], 'bug-2199-diaspora-no-named-size' => [ 'expectedHtml' => 'Test text', - 'text' => '[size=xx-large]Test text[/size]', - 'try_oembed' => false, + 'text' => '[size=xx-large]Test text[/size]', + 'try_oembed' => false, // Triggers the diaspora compatible output 'simpleHtml' => BBCode::DIASPORA, ], 'bug-2199-diaspora-no-numeric-size' => [ 'expectedHtml' => 'Test text', - 'text' => '[size=24]Test text[/size]', - 'try_oembed' => false, + 'text' => '[size=24]Test text[/size]', + 'try_oembed' => false, // Triggers the diaspora compatible output 'simpleHtml' => BBCode::DIASPORA, ], 'bug-7665-audio-tag' => [ 'expectedHtml' => '', - 'text' => '[audio]http://www.cendrones.fr/colloque2017/jonathanbocquet.mp3[/audio]', - 'try_oembed' => true, + 'text' => '[audio]http://www.cendrones.fr/colloque2017/jonathanbocquet.mp3[/audio]', + 'try_oembed' => true, ], 'bug-7808-code-lt' => [ 'expectedHtml' => '<', - 'text' => '[code]<[/code]', + 'text' => '[code]<[/code]', ], 'bug-7808-code-gt' => [ 'expectedHtml' => '>', - 'text' => '[code]>[/code]', + 'text' => '[code]>[/code]', ], 'bug-7808-code-amp' => [ 'expectedHtml' => '&', - 'text' => '[code]&[/code]', + 'text' => '[code]&[/code]', ], 'task-8800-pre-spaces-notag' => [ 'expectedHtml' => '[test] Space', - 'text' => '[test] Space', + 'text' => '[test] Space', ], 'task-8800-pre-spaces' => [ 'expectedHtml' => '    Spaces', - 'text' => '[pre] Spaces[/pre]', + 'text' => '[pre] Spaces[/pre]', ], 'bug-9611-purify-xss-nobb' => [ 'expectedHTML' => 'dare to move your mouse here', - 'text' => '[nobb]dare to move your mouse here[/nobb]' + 'text' => '[nobb]dare to move your mouse here[/nobb]', ], 'bug-9611-purify-xss-noparse' => [ 'expectedHTML' => 'dare to move your mouse here', - 'text' => '[noparse]dare to move your mouse here[/noparse]' + 'text' => '[noparse]dare to move your mouse here[/noparse]', ], 'bug-9611-purify-xss-attributes' => [ 'expectedHTML' => 'dare to move your mouse here', - 'text' => '[color="onmouseover=alert(0) style="]dare to move your mouse here[/color]' + 'text' => '[color="onmouseover=alert(0) style="]dare to move your mouse here[/color]', ], 'bug-9611-purify-attributes-correct' => [ 'expectedHTML' => 'dare to move your mouse here', - 'text' => '[color=FFFFFF]dare to move your mouse here[/color]' + 'text' => '[color=FFFFFF]dare to move your mouse here[/color]', ], 'bug-9639-span-classes' => [ 'expectedHTML' => 'Test', - 'text' => '[class=arbitrary classes]Test[/class]', + 'text' => '[class=arbitrary classes]Test[/class]', ], 'bug-10772-duplicated-links' => [ 'expectedHTML' => 'Jetzt wird mir klar, warum Kapitalisten jedes Mal durchdrehen wenn Marx und das Kapital ins Gespräch kommt. Soziopathen.
                                  Karl Marx - Die ursprüngliche Akkumulation
                                  https://wohlstandfueralle.podigee.io/107-urspruengliche-akkumulation
                                  #Podcast #Kapitalismus', - 'text' => "Jetzt wird mir klar, warum Kapitalisten jedes Mal durchdrehen wenn Marx und das Kapital ins Gespräch kommt. Soziopathen. + 'text' => "Jetzt wird mir klar, warum Kapitalisten jedes Mal durchdrehen wenn Marx und das Kapital ins Gespräch kommt. Soziopathen. Karl Marx - Die ursprüngliche Akkumulation [url=https://wohlstandfueralle.podigee.io/107-urspruengliche-akkumulation]https://wohlstandfueralle.podigee.io/107-urspruengliche-akkumulation[/url] #[url=https://horche.demkontinuum.de/search?tag=Podcast]Podcast[/url] #[url=https://horche.demkontinuum.de/search?tag=Kapitalismus]Kapitalismus[/url] @@ -259,27 +245,27 @@ Karl Marx - Die ursprüngliche Akkumulation ], 'task-10886-deprecate-class' => [ 'expectedHTML' => ':heart_nb:', - 'text' => '[emoji=https://fedi.underscore.world/emoji/custom/custom/heart_nb.png]:heart_nb:[/emoji]', + 'text' => '[emoji=https://fedi.underscore.world/emoji/custom/custom/heart_nb.png]:heart_nb:[/emoji]', ], 'task-12900-multiple-paragraphs' => [ - 'expectedHTML' => '

                                  Header

                                  • One
                                  • Two

                                  This is a paragraph
                                  with a line feed.

                                  Second Chapter

                                  ', - 'text' => "[h4]Header[/h4][ul][*]One[*]Two[/ul]\n\nThis is a paragraph\nwith a line feed.\n\nSecond Chapter", + 'expectedHTML' => '

                                  Header

                                  • One
                                  • Two

                                  This is a paragraph
                                  with a line feed.

                                  Second Chapter

                                  ', + 'text' => "[h4]Header[/h4][ul][li]One[li]Two[/ul]\n\nThis is a paragraph\nwith a line feed.\n\nSecond Chapter", ], 'task-12900-header-with-paragraphs' => [ - 'expectedHTML' => '

                                  Header

                                  Some Chapter

                                  ', - 'text' => '[h4]Header[/h4]Some Chapter', + 'expectedHTML' => '

                                  Header

                                  Some Chapter

                                  ', + 'text' => '[h4]Header[/h4]Some Chapter', ], 'bug-12842-ul-newlines' => [ 'expectedHTML' => '

                                  This is:

                                  • some
                                  • amazing
                                  • list
                                  ', - 'text' => "This is:\r\n[ul]\r\n[*]some\r\n[*]amazing\r\n[*]list\r\n[/ul]", + 'text' => "This is:\r\n[ul]\r\n[li]some\r\n[li]amazing\r\n[li]list\r\n[/ul]", ], 'bug-12842-ol-newlines' => [ 'expectedHTML' => '

                                  This is:

                                  1. some
                                  2. amazing
                                  3. list
                                  ', - 'text' => "This is:\r\n[ol]\r\n[*]some\r\n[*]amazing\r\n[*]list\r\n[/ol]", + 'text' => "This is:\r\n[ol]\r\n[li]some\r\n[li]amazing\r\n[li]list\r\n[/ol]", ], 'task-12917-tabs-between-linebreaks' => [ 'expectedHTML' => '

                                  Paragraph

                                  New Paragraph

                                  ', - 'text' => "Paragraph\n\t\nNew Paragraph", + 'text' => "Paragraph\n\t\nNew Paragraph", ], ]; } @@ -310,23 +296,28 @@ Karl Marx - Die ursprüngliche Akkumulation return [ 'bug-7808-gt' => [ 'expected' => '>`>`', - 'text' => '>[code]>[/code]', + 'text' => '>[code]>[/code]', ], 'bug-7808-lt' => [ 'expected' => '<`<`', - 'text' => '<[code]<[/code]', + 'text' => '<[code]<[/code]', ], 'bug-7808-amp' => [ 'expected' => '&`&`', - 'text' => '&[code]&[/code]', + 'text' => '&[code]&[/code]', ], 'bug-12701-quotes' => [ 'expected' => '[![abc"fgh](https://domain.tld/photo/86912721086415cdc8e0a03226831581-1.png)](https://domain.tld/photos/user/image/86912721086415cdc8e0a03226831581)', - 'text' => '[url=https://domain.tld/photos/user/image/86912721086415cdc8e0a03226831581][img=https://domain.tld/photo/86912721086415cdc8e0a03226831581-1.png]abc"fgh[/img][/url]' + 'text' => '[url=https://domain.tld/photos/user/image/86912721086415cdc8e0a03226831581][img=https://domain.tld/photo/86912721086415cdc8e0a03226831581-1.png]abc"fgh[/img][/url]', ], 'bug-12701-no-quotes' => [ 'expected' => '[![abcfgh](https://domain.tld/photo/86912721086415cdc8e0a03226831581-1.png "abcfgh")](https://domain.tld/photos/user/image/86912721086415cdc8e0a03226831581)', - 'text' => '[url=https://domain.tld/photos/user/image/86912721086415cdc8e0a03226831581][img=https://domain.tld/photo/86912721086415cdc8e0a03226831581-1.png]abcfgh[/img][/url]' + 'text' => '[url=https://domain.tld/photos/user/image/86912721086415cdc8e0a03226831581][img=https://domain.tld/photo/86912721086415cdc8e0a03226831581-1.png]abcfgh[/img][/url]', + ], + /** @see https://github.com/friendica/friendica/pull/14908 */ + 'task-14908-strip-tags' => [ + 'expected' => 'Norddeutscher Bürger !\[Noddeutscher Bürger - Bismark Brötchen (Roger Cziwerny - pixapay)\](/rscamo/……)', + 'text' => '[class=postbox-ocean]Norddeutscher Bürger ![Noddeutscher Bürger - Bismark Brötchen (Roger Cziwerny - pixapay)](/rscamo/……)[/class]', ], ]; } @@ -359,7 +350,7 @@ Karl Marx - Die ursprüngliche Akkumulation 'bug-10692-start-line' => [ '#[url=https://friendica.local/search?tag=L160]L160[/url]', '#L160', - ] + ], ]; } @@ -376,58 +367,114 @@ Karl Marx - Die ursprüngliche Akkumulation self::assertEquals($expected, $actual); } + public function dataExpandVideoLinks(): array + { + return [ + /** @see https://github.com/friendica/friendica/pull/14940 */ + 'task-14940-youtube-watch-with-www' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://www.youtube.com/watch?v=hfwbmTzBFT0[/youtube]', + ], + 'task-14940-youtube-watch-without-www' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://youtube.com/watch?v=hfwbmTzBFT0[/youtube]', + ], + 'task-14940-youtube-shorts-with-www' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://www.youtube.com/shorts/hfwbmTzBFT0[/youtube]', + ], + 'task-14940-youtube-shorts-without-www' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://youtube.com/shorts/hfwbmTzBFT0[/youtube]', + ], + 'task-14940-youtube-embed-with-www' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://www.youtube.com/embed/hfwbmTzBFT0[/youtube]', + ], + 'task-14940-youtube-embed-without-www' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://youtube.com/embed/hfwbmTzBFT0[/youtube]', + ], + 'task-14940-youtube-mobile' => [ + 'expectedBBCode' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + 'text' => '[youtube]https://m.youtube.com/watch?v=hfwbmTzBFT0[/youtube]', + ], + 'task-14940-vimeo' => [ + 'expectedBBCode' => '[url=https://vimeo.com/2345345]https://vimeo.com/2345345[/url]', + 'text' => '[vimeo]https://vimeo.com/2345345[/vimeo]', + ], + 'task-14940-player-vimeo' => [ + 'expectedBBCode' => '[url=https://vimeo.com/2345345]https://vimeo.com/2345345[/url]', + 'text' => '[vimeo]https://player.vimeo.com/video/2345345[/vimeo]', + ], + ]; + } + + /** + * @dataProvider dataExpandVideoLinks + * + * @param string $expected Expected BBCode output + * @param string $text Input text + */ + public function testExpandVideoLinks(string $expected, string $text) + { + $actual = BBCode::expandVideoLinks($text); + + self::assertEquals($expected, $actual); + } + public function dataGetAbstract(): array { return [ 'no-abstract' => [ 'expected' => '', - 'text' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'addon' => '', + 'text' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'addon' => '', ], 'no-abstract-addon' => [ 'expected' => '', - 'text' => 'Tingling of the spine tendrils of gossamer clouds Flatland trillion rich in heavy atoms of brilliant syntheses. Extraordinary claims require extraordinary evidence a very small stage in a vast cosmic arena made in the interiors of collapsing stars kindling the energy hidden in matter vastness is bearable only through love kindling the energy hidden in matter? Dispassionate extraterrestrial observer preserve and cherish that pale blue dot vastness is bearable only through love emerged into consciousness encyclopaedia galactica a still more glorious dawn awaits and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'addon' => 'dfrn', + 'text' => 'Tingling of the spine tendrils of gossamer clouds Flatland trillion rich in heavy atoms of brilliant syntheses. Extraordinary claims require extraordinary evidence a very small stage in a vast cosmic arena made in the interiors of collapsing stars kindling the energy hidden in matter vastness is bearable only through love kindling the energy hidden in matter? Dispassionate extraterrestrial observer preserve and cherish that pale blue dot vastness is bearable only through love emerged into consciousness encyclopaedia galactica a still more glorious dawn awaits and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'addon' => 'dfrn', ], 'abstract' => [ 'expected' => 'Abstract at the beginning of the text', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract]A very small stage in a vast cosmic arena the ash of stellar alchemy rich in heavy atoms a still more glorious dawn awaits are creatures of the cosmos Orion\'s sword. Brain is the seed of intelligence dream of the mind\'s eye inconspicuous motes of rock and gas extraordinary claims require extraordinary evidence vastness is bearable only through love quasar? Made in the interiors of collapsing stars the carbon in our apple pies cosmic ocean citizens of distant epochs paroxysm of global death dispassionate extraterrestrial observer and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'addon' => '', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract]A very small stage in a vast cosmic arena the ash of stellar alchemy rich in heavy atoms a still more glorious dawn awaits are creatures of the cosmos Orion\'s sword. Brain is the seed of intelligence dream of the mind\'s eye inconspicuous motes of rock and gas extraordinary claims require extraordinary evidence vastness is bearable only through love quasar? Made in the interiors of collapsing stars the carbon in our apple pies cosmic ocean citizens of distant epochs paroxysm of global death dispassionate extraterrestrial observer and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'addon' => '', ], 'abstract-addon-not-present' => [ 'expected' => 'Abstract at the beginning of the text', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract]With pretty stories for which there\'s little good evidence rogue not a sunrise but a galaxyrise tingling of the spine birth cosmic fugue. Cosmos hundreds of thousands Apollonius of Perga network of wormholes rich in mystery globular star cluster. Another world vastness is bearable only through love encyclopaedia galactica something incredible is waiting to be known invent the universe hearts of the stars. Extraordinary claims require extraordinary evidence the sky calls to us the only home we\'ve ever known the sky calls to us the sky calls to us extraordinary claims require extraordinary evidence and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'addon' => '', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract]With pretty stories for which there\'s little good evidence rogue not a sunrise but a galaxyrise tingling of the spine birth cosmic fugue. Cosmos hundreds of thousands Apollonius of Perga network of wormholes rich in mystery globular star cluster. Another world vastness is bearable only through love encyclopaedia galactica something incredible is waiting to be known invent the universe hearts of the stars. Extraordinary claims require extraordinary evidence the sky calls to us the only home we\'ve ever known the sky calls to us the sky calls to us extraordinary claims require extraordinary evidence and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'addon' => '', ], 'abstract-addon-present' => [ 'expected' => 'Abstract DFRN in the middle of the text', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=dfrn]Abstract DFRN in the middle of the text[/abstract]Kindling the energy hidden in matter hydrogen atoms at the edge of forever vanquish the impossible ship of the imagination take root and flourish. Tingling of the spine white dwarf as a patch of light the sky calls to us Drake Equation citizens of distant epochs. Concept of the number one dispassionate extraterrestrial observer citizens of distant epochs descended from astronomers extraordinary claims require extraordinary evidence something incredible is waiting to be known and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'addon' => 'dfrn', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=dfrn]Abstract DFRN in the middle of the text[/abstract]Kindling the energy hidden in matter hydrogen atoms at the edge of forever vanquish the impossible ship of the imagination take root and flourish. Tingling of the spine white dwarf as a patch of light the sky calls to us Drake Equation citizens of distant epochs. Concept of the number one dispassionate extraterrestrial observer citizens of distant epochs descended from astronomers extraordinary claims require extraordinary evidence something incredible is waiting to be known and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'addon' => 'dfrn', ], 'abstract-multiple-addon-present' => [ 'expected' => 'Abstract DFRN at the end of the text', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=ap]Abstract AP in the middle of the text[/abstract]Cambrian explosion rich in heavy atoms take root and flourish radio telescope light years cosmic fugue. Dispassionate extraterrestrial observer white dwarf the sky calls to us another world courage of our questions two ghostly white figures in coveralls and helmets are softly dancing. Extraordinary claims require extraordinary evidence concept of the number one not a sunrise but a galaxyrise are creatures of the cosmos two ghostly white figures in coveralls and helmets are softly dancing white dwarf and billions upon billions upon billions upon billions upon billions upon billions upon billions.[abstract=dfrn]Abstract DFRN at the end of the text[/abstract]', - 'addon' => 'dfrn', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=ap]Abstract AP in the middle of the text[/abstract]Cambrian explosion rich in heavy atoms take root and flourish radio telescope light years cosmic fugue. Dispassionate extraterrestrial observer white dwarf the sky calls to us another world courage of our questions two ghostly white figures in coveralls and helmets are softly dancing. Extraordinary claims require extraordinary evidence concept of the number one not a sunrise but a galaxyrise are creatures of the cosmos two ghostly white figures in coveralls and helmets are softly dancing white dwarf and billions upon billions upon billions upon billions upon billions upon billions upon billions.[abstract=dfrn]Abstract DFRN at the end of the text[/abstract]', + 'addon' => 'dfrn', ], 'bug-11445-code-abstract' => [ 'expected' => '', - 'text' => '[code][abstract]This should not be converted[/abstract][/code]', - 'addon' => '', + 'text' => '[code][abstract]This should not be converted[/abstract][/code]', + 'addon' => '', ], 'bug-11445-noparse-abstract' => [ 'expected' => '', - 'text' => '[noparse][abstract]This should not be converted[/abstract][/noparse]', - 'addon' => '', + 'text' => '[noparse][abstract]This should not be converted[/abstract][/noparse]', + 'addon' => '', ], 'bug-11445-nobb-abstract' => [ 'expected' => '', - 'text' => '[nobb][abstract]This should not be converted[/abstract][/nobb]', - 'addon' => '', + 'text' => '[nobb][abstract]This should not be converted[/abstract][/nobb]', + 'addon' => '', ], 'bug-11445-pre-abstract' => [ 'expected' => '', - 'text' => '[pre][abstract]This should not be converted[/abstract][/pre]', - 'addon' => '', + 'text' => '[pre][abstract]This should not be converted[/abstract][/pre]', + 'addon' => '', ], ]; } @@ -452,35 +499,35 @@ Karl Marx - Die ursprüngliche Akkumulation return [ 'no-abstract' => [ 'expected' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'text' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'text' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', ], 'abstract' => [ 'expected' => ' A very small stage in a vast cosmic arena the ash of stellar alchemy rich in heavy atoms a still more glorious dawn awaits are creatures of the cosmos Orion\'s sword. Brain is the seed of intelligence dream of the mind\'s eye inconspicuous motes of rock and gas extraordinary claims require extraordinary evidence vastness is bearable only through love quasar? Made in the interiors of collapsing stars the carbon in our apple pies cosmic ocean citizens of distant epochs paroxysm of global death dispassionate extraterrestrial observer and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract]A very small stage in a vast cosmic arena the ash of stellar alchemy rich in heavy atoms a still more glorious dawn awaits are creatures of the cosmos Orion\'s sword. Brain is the seed of intelligence dream of the mind\'s eye inconspicuous motes of rock and gas extraordinary claims require extraordinary evidence vastness is bearable only through love quasar? Made in the interiors of collapsing stars the carbon in our apple pies cosmic ocean citizens of distant epochs paroxysm of global death dispassionate extraterrestrial observer and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract]A very small stage in a vast cosmic arena the ash of stellar alchemy rich in heavy atoms a still more glorious dawn awaits are creatures of the cosmos Orion\'s sword. Brain is the seed of intelligence dream of the mind\'s eye inconspicuous motes of rock and gas extraordinary claims require extraordinary evidence vastness is bearable only through love quasar? Made in the interiors of collapsing stars the carbon in our apple pies cosmic ocean citizens of distant epochs paroxysm of global death dispassionate extraterrestrial observer and billions upon billions upon billions upon billions upon billions upon billions upon billions.', ], 'abstract-addon' => [ 'expected' => ' Kindling the energy hidden in matter hydrogen atoms at the edge of forever vanquish the impossible ship of the imagination take root and flourish. Tingling of the spine white dwarf as a patch of light the sky calls to us Drake Equation citizens of distant epochs. Concept of the number one dispassionate extraterrestrial observer citizens of distant epochs descended from astronomers extraordinary claims require extraordinary evidence something incredible is waiting to be known and billions upon billions upon billions upon billions upon billions upon billions upon billions.', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=dfrn]Abstract DFRN in the middle of the text[/abstract]Kindling the energy hidden in matter hydrogen atoms at the edge of forever vanquish the impossible ship of the imagination take root and flourish. Tingling of the spine white dwarf as a patch of light the sky calls to us Drake Equation citizens of distant epochs. Concept of the number one dispassionate extraterrestrial observer citizens of distant epochs descended from astronomers extraordinary claims require extraordinary evidence something incredible is waiting to be known and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=dfrn]Abstract DFRN in the middle of the text[/abstract]Kindling the energy hidden in matter hydrogen atoms at the edge of forever vanquish the impossible ship of the imagination take root and flourish. Tingling of the spine white dwarf as a patch of light the sky calls to us Drake Equation citizens of distant epochs. Concept of the number one dispassionate extraterrestrial observer citizens of distant epochs descended from astronomers extraordinary claims require extraordinary evidence something incredible is waiting to be known and billions upon billions upon billions upon billions upon billions upon billions upon billions.', ], 'abstract-multiple-addon-present' => [ 'expected' => ' Cambrian explosion rich in heavy atoms take root and flourish radio telescope light years cosmic fugue. Dispassionate extraterrestrial observer white dwarf the sky calls to us another world courage of our questions two ghostly white figures in coveralls and helmets are softly dancing. Extraordinary claims require extraordinary evidence concept of the number one not a sunrise but a galaxyrise are creatures of the cosmos two ghostly white figures in coveralls and helmets are softly dancing white dwarf and billions upon billions upon billions upon billions upon billions upon billions upon billions. ', - 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=ap]Abstract AP in the middle of the text[/abstract]Cambrian explosion rich in heavy atoms take root and flourish radio telescope light years cosmic fugue. Dispassionate extraterrestrial observer white dwarf the sky calls to us another world courage of our questions two ghostly white figures in coveralls and helmets are softly dancing. Extraordinary claims require extraordinary evidence concept of the number one not a sunrise but a galaxyrise are creatures of the cosmos two ghostly white figures in coveralls and helmets are softly dancing white dwarf and billions upon billions upon billions upon billions upon billions upon billions upon billions.[abstract=dfrn]Abstract DFRN at the end of the text[/abstract]', + 'text' => '[abstract]Abstract at the beginning of the text[/abstract][abstract=ap]Abstract AP in the middle of the text[/abstract]Cambrian explosion rich in heavy atoms take root and flourish radio telescope light years cosmic fugue. Dispassionate extraterrestrial observer white dwarf the sky calls to us another world courage of our questions two ghostly white figures in coveralls and helmets are softly dancing. Extraordinary claims require extraordinary evidence concept of the number one not a sunrise but a galaxyrise are creatures of the cosmos two ghostly white figures in coveralls and helmets are softly dancing white dwarf and billions upon billions upon billions upon billions upon billions upon billions upon billions.[abstract=dfrn]Abstract DFRN at the end of the text[/abstract]', ], 'bug-11445-code-abstract' => [ 'expected' => '[code][abstract]This should not be converted[/abstract][/code]', - 'text' => '[code][abstract]This should not be converted[/abstract][/code]', + 'text' => '[code][abstract]This should not be converted[/abstract][/code]', ], 'bug-11445-noparse-abstract' => [ 'expected' => '[noparse][abstract]This should not be converted[/abstract][/noparse]', - 'text' => '[noparse][abstract]This should not be converted[/abstract][/noparse]', + 'text' => '[noparse][abstract]This should not be converted[/abstract][/noparse]', ], 'bug-11445-nobb-abstract' => [ 'expected' => '[nobb][abstract]This should not be converted[/abstract][/nobb]', - 'text' => '[nobb][abstract]This should not be converted[/abstract][/nobb]', + 'text' => '[nobb][abstract]This should not be converted[/abstract][/nobb]', ], 'bug-11445-pre-abstract' => [ 'expected' => '[pre][abstract]This should not be converted[/abstract][/pre]', - 'text' => '[pre][abstract]This should not be converted[/abstract][/pre]', + 'text' => '[pre][abstract]This should not be converted[/abstract][/pre]', ], ]; } @@ -503,52 +550,52 @@ Karl Marx - Die ursprüngliche Akkumulation return [ 'no-tag' => [ 'expected' => [], - 'text' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', + 'text' => 'Venture the only home we\'ve ever known laws of physics tendrils of gossamer clouds a still more glorious dawn awaits Sea of Tranquility. With pretty stories for which there\'s little good evidence the ash of stellar alchemy corpus callosum preserve and cherish that pale blue dot descended from astronomers preserve and cherish that pale blue dot. A mote of dust suspended in a sunbeam paroxysm of global death two ghostly white figures in coveralls and helmets are softly dancing descended from astronomers star stuff harvesting star light gathered by gravity and billions upon billions upon billions upon billions upon billions upon billions upon billions.', ], 'just-open' => [ 'expected' => [], - 'text' => '[share]', + 'text' => '[share]', ], 'empty-tag' => [ 'expected' => [ - 'author' => '', - 'profile' => '', - 'avatar' => '', - 'link' => '', - 'posted' => '', - 'guid' => '', + 'author' => '', + 'profile' => '', + 'avatar' => '', + 'link' => '', + 'posted' => '', + 'guid' => '', 'message_id' => '', - 'comment' => '', - 'shared' => '', + 'comment' => '', + 'shared' => '', ], 'text' => '[share][/share]', ], 'comment-shared' => [ 'expected' => [ - 'author' => '', - 'profile' => '', - 'avatar' => '', - 'link' => '', - 'posted' => '', - 'guid' => '', + 'author' => '', + 'profile' => '', + 'avatar' => '', + 'link' => '', + 'posted' => '', + 'guid' => '', 'message_id' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'comment' => 'comment', - 'shared' => '', + 'comment' => 'comment', + 'shared' => '', ], 'text' => ' comment [share]https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243[/share]', ], 'all-attributes' => [ 'expected' => [ - 'author' => 'Hypolite Petovan', - 'profile' => 'https://friendica.mrpetovan.com/profile/hypolite', - 'avatar' => 'https://friendica.mrpetovan.com/photo/20682437145daa4e85f019a278584494-5.png', - 'link' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'posted' => '2022-06-16 12:34:10', - 'guid' => '735a2029-1062-ab23-42e4-f9c631220243', + 'author' => 'Hypolite Petovan', + 'profile' => 'https://friendica.mrpetovan.com/profile/hypolite', + 'avatar' => 'https://friendica.mrpetovan.com/photo/20682437145daa4e85f019a278584494-5.png', + 'link' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', + 'posted' => '2022-06-16 12:34:10', + 'guid' => '735a2029-1062-ab23-42e4-f9c631220243', 'message_id' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'comment' => '', - 'shared' => 'George Lucas: I made a science-fiction universe with a straightforward anti-authoritarianism plot where even the libertarian joins the rebellion. + 'comment' => '', + 'shared' => 'George Lucas: I made a science-fiction universe with a straightforward anti-authoritarianism plot where even the libertarian joins the rebellion. Disney: So a morally grey “choose your side” story, right? Lucas: For the right price, yes.', ], @@ -566,15 +613,15 @@ Lucas: For the right price, yes.[/share]", ], 'optional-attributes' => [ 'expected' => [ - 'author' => 'Hypolite Petovan', - 'profile' => 'https://friendica.mrpetovan.com/profile/hypolite', - 'avatar' => 'https://friendica.mrpetovan.com/photo/20682437145daa4e85f019a278584494-5.png', - 'link' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'posted' => '2022-06-16 12:34:10', - 'guid' => '', + 'author' => 'Hypolite Petovan', + 'profile' => 'https://friendica.mrpetovan.com/profile/hypolite', + 'avatar' => 'https://friendica.mrpetovan.com/photo/20682437145daa4e85f019a278584494-5.png', + 'link' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', + 'posted' => '2022-06-16 12:34:10', + 'guid' => '', 'message_id' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'comment' => '', - 'shared' => 'George Lucas: I made a science-fiction universe with a straightforward anti-authoritarianism plot where even the libertarian joins the rebellion. + 'comment' => '', + 'shared' => 'George Lucas: I made a science-fiction universe with a straightforward anti-authoritarianism plot where even the libertarian joins the rebellion. Disney: So a morally grey “choose your side” story, right? Lucas: For the right price, yes.', ], @@ -591,15 +638,15 @@ Lucas: For the right price, yes.[/share]", ], 'double-quotes' => [ 'expected' => [ - 'author' => 'Hypolite Petovan', - 'profile' => 'https://friendica.mrpetovan.com/profile/hypolite', - 'avatar' => 'https://friendica.mrpetovan.com/photo/20682437145daa4e85f019a278584494-5.png', - 'link' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'posted' => '2022-06-16 12:34:10', - 'guid' => '', + 'author' => 'Hypolite Petovan', + 'profile' => 'https://friendica.mrpetovan.com/profile/hypolite', + 'avatar' => 'https://friendica.mrpetovan.com/photo/20682437145daa4e85f019a278584494-5.png', + 'link' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', + 'posted' => '2022-06-16 12:34:10', + 'guid' => '', 'message_id' => 'https://friendica.mrpetovan.com/display/735a2029-1062-ab23-42e4-f9c631220243', - 'comment' => '', - 'shared' => 'George Lucas: I made a science-fiction universe with a straightforward anti-authoritarianism plot where even the libertarian joins the rebellion. + 'comment' => '', + 'shared' => 'George Lucas: I made a science-fiction universe with a straightforward anti-authoritarianism plot where even the libertarian joins the rebellion. Disney: So a morally grey “choose your side” story, right? Lucas: For the right price, yes.', ], @@ -629,4 +676,27 @@ Lucas: For the right price, yes.[/share]', self::assertEquals($expected, $actual); } + + public function dataProfileLink(): array + { + return [ + 'mention' => [ + 'expected' => 'Test 1: @Remote contact', + 'text' => 'Test 1: @[url=https://domain.tld/profile/remotecontact]Remote contact[/url]', + ], + ]; + } + + /** + * @dataProvider dataProfileLink + * + * @param string $expected Expected BBCode output + * @param string $text Input text + */ + public function testProfileLink(string $expected, string $text) + { + $actual = BBCode::convertForUriId(0, $text); + + self::assertEquals($expected, $actual); + } } diff --git a/tests/src/Content/Text/HTMLTest.php b/tests/src/Content/Text/HTMLTest.php index c94f180f84..374f74111f 100644 --- a/tests/src/Content/Text/HTMLTest.php +++ b/tests/src/Content/Text/HTMLTest.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content\Text; use Exception; use Friendica\Content\Text\HTML; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; -class HTMLTest extends FixtureTest +class HTMLTest extends FixtureTestCase { public function dataHTML() { diff --git a/tests/src/Content/Text/MarkdownTest.php b/tests/src/Content/Text/MarkdownTest.php index 296c7da259..0e4552dcbe 100644 --- a/tests/src/Content/Text/MarkdownTest.php +++ b/tests/src/Content/Text/MarkdownTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Content\Text; use Exception; use Friendica\Content\Text\Markdown; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class MarkdownTest extends FixtureTest +class MarkdownTest extends FixtureTestCase { public function dataMarkdown() { @@ -65,7 +51,49 @@ class MarkdownTest extends FixtureTest return [ 'bug-8358-double-decode' => [ 'expectedBBCode' => 'with the and tag', - 'markdown' => 'with the <sup> and </sup> tag', + 'markdown' => 'with the <sup> and </sup> tag', + ], + /** @see https://github.com/friendica/friendica/pull/14940 */ + 'task-14940-youtube-watch-with-www' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://www.youtube.com/watch?v=hfwbmTzBFT0]https://www.youtube.com/watch?v=hfwbmTzBFT0[/url]', + ], + 'task-14940-youtube-watch-without-www' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://youtube.com/watch?v=hfwbmTzBFT0]https://youtube.com/watch?v=hfwbmTzBFT0[/url]', + ], + 'task-14940-youtube-shorts-with-www' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://www.youtube.com/shorts/hfwbmTzBFT0]https://www.youtube.com/shorts/hfwbmTzBFT0[/url]', + ], + 'task-14940-youtube-shorts-without-www' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://youtube.com/shorts/hfwbmTzBFT0]https://youtube.com/shorts/hfwbmTzBFT0[/url]', + ], + 'task-14940-youtube-embed-with-www' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://www.youtube.com/embed/hfwbmTzBFT0]https://www.youtube.com/embed/hfwbmTzBFT0[/url]', + ], + 'task-14940-youtube-embed-without-www' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://youtube.com/embed/hfwbmTzBFT0]https://youtube.com/embed/hfwbmTzBFT0[/url]', + ], + 'task-14940-youtube-mobile' => [ + 'expectedBBCode' => '[youtube]hfwbmTzBFT0[/youtube]', + 'markdown' => '[url=https://m.youtube.com/watch?v=hfwbmTzBFT0]https://m.youtube.com/watch?v=hfwbmTzBFT0[/url]', + ], + // @todo - should we really ignore the URL content in favor of parsing the link of the body? + 'task-14940-vimeo-custom-url' => [ + 'expectedBBCode' => '[vimeo]2345345[/vimeo]', + 'markdown' => '[url=https://no.thing]https://vimeo.com/2345345[/url]', + ], + 'task-14940-vimeo-custom-text' => [ + 'expectedBBCode' => '[vimeo]2345345[/vimeo]', + 'markdown' => '[url=https://vimeo.com/2345345]CustomText[/url]', + ], + 'task-14940-player-vimeo' => [ + 'expectedBBCode' => '[vimeo]2345345[/vimeo]', + 'markdown' => '[url=https://player.vimeo.com/video/2345345]https://player.vimeo.com/video/2345345[/url]', ], ]; } diff --git a/tests/src/Content/Text/PlaintextTest.php b/tests/src/Content/Text/PlaintextTest.php new file mode 100644 index 0000000000..814e842c72 --- /dev/null +++ b/tests/src/Content/Text/PlaintextTest.php @@ -0,0 +1,72 @@ + [ + 'data' => "Ich habe mein Profil so eingestellt, dass ich alle Folgeanfragen manuell bestätigen muss, was langsam aber sicher richtig in Arbeit ausartet 😉\n\nIch schaue mir immer die anderen Profile an und schaue, was sie so gepostet haben. Wenn die Person noch nichts gepostet hat, ignoriere ich die Anfragen und schaue ggf. nach einiger Zeit wieder nach, ob jetzt was gepostet wurde! Wenn die Posts in eine Richtung gehen, die ich nicht mag, lehne ich die Anfragen ab.\n\nIch ignoriere auch Anfragen, wenn sie von Accounts kommen, die ggf. tausenden von anderen Accounts folgen, da ich davon ausgehe, dass da niemand ernsthaft so vielen Accounts folgen kann.", + 'expected' => [ + 'Ich habe mein Profil so eingestellt, dass ich alle Folgeanfragen manuell bestätigen muss, was langsam aber sicher richtig in Arbeit ausartet 😉 (1/6)', + 'Ich schaue mir immer die anderen Profile an und schaue, was sie so gepostet haben. (2/6)', + 'Wenn die Person noch nichts gepostet hat, ignoriere ich die Anfragen und schaue ggf. nach einiger Zeit wieder nach, ob jetzt was gepostet wurde! (3/6)', + 'Wenn die Posts in eine Richtung gehen, die ich nicht mag, lehne ich die Anfragen ab. (4/6)', + 'Ich ignoriere auch Anfragen, wenn sie von Accounts kommen, die ggf. tausenden von anderen Accounts folgen, da ich davon ausgehe, (5/6)', + 'dass da niemand ernsthaft so vielen Accounts folgen kann. (6/6)' + ], + ], + 'test-2' => [ + 'data' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'expected' => [ + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, (1/6)', + 'sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. (2/6)', + 'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. (3/6)', + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, (4/6)', + 'sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. (5/6)', + 'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. (6/6)' + ], + ], + ]; + } + + /** + * Test split long texts + * + * @dataProvider dataMessage + * + * @param string $text Test string + * @param array $expected Expected result + * + * @throws InternalServerErrorException + */ + public function testSplitMessage(string $text, array $expected) + { + $item = [ + 'uri-id' => -1, + 'uid' => 0, + 'title' => '', + 'plink' => '', + 'body' => $text, + ]; + $output = Plaintext::getPost($item, 160, false, BBCode::BLUESKY); + self::assertEquals($expected, $output['parts']); + } +} diff --git a/tests/src/Core/ACLTest.php b/tests/src/Core/ACLTest.php index 14c7a20c3c..49d791ac77 100644 --- a/tests/src/Core/ACLTest.php +++ b/tests/src/Core/ACLTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core; use Friendica\Core\ACL; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class ACLTest extends FixtureTest +class ACLTest extends FixtureTestCase { /** * Test the ACL::isValidContact() function. diff --git a/tests/src/Core/Addon/Model/AddonLoaderTest.php b/tests/src/Core/Addon/Model/AddonLoaderTest.php index 7482291c34..1c119fb680 100644 --- a/tests/src/Core/Addon/Model/AddonLoaderTest.php +++ b/tests/src/Core/Addon/Model/AddonLoaderTest.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Addon\Model; use Friendica\Core\Addon\Exception\AddonInvalidConfigFileException; use Friendica\Core\Addon\Model\AddonLoader; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\VFSTrait; use org\bovigo\vfs\vfsStream; -class AddonLoaderTest extends MockedTest +class AddonLoaderTest extends MockedTestCase { use VFSTrait; diff --git a/tests/src/Core/Cache/APCuCacheTest.php b/tests/src/Core/Cache/APCuCacheTest.php index 03a3fa10a4..47e660b26a 100644 --- a/tests/src/Core/Cache/APCuCacheTest.php +++ b/tests/src/Core/Cache/APCuCacheTest.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Cache; use Friendica\Core\Cache\Type\APCuCache; +use Friendica\Test\MemoryCacheTestCase; /** * @group APCU */ -class APCuCacheTest extends MemoryCacheTest +class APCuCacheTest extends MemoryCacheTestCase { protected function setUp(): void { @@ -48,4 +35,18 @@ class APCuCacheTest extends MemoryCacheTest $this->cache->clear(false); parent::tearDown(); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['entries']); + self::assertNotNull($stats['used_memory']); + self::assertNotNull($stats['hits']); + self::assertNotNull($stats['misses']); + self::assertNotNull($stats['avail_mem']); + } } diff --git a/tests/src/Core/Cache/ArrayCacheTest.php b/tests/src/Core/Cache/ArrayCacheTest.php index e1f491d3bd..50226b0907 100644 --- a/tests/src/Core/Cache/ArrayCacheTest.php +++ b/tests/src/Core/Cache/ArrayCacheTest.php @@ -1,29 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Cache; use Friendica\Core\Cache\Type\ArrayCache; +use Friendica\Test\MemoryCacheTestCase; -class ArrayCacheTest extends MemoryCacheTest +class ArrayCacheTest extends MemoryCacheTestCase { protected function getInstance() { @@ -46,4 +33,12 @@ class ArrayCacheTest extends MemoryCacheTest self::markTestSkipped("Array Cache doesn't support TTL"); return true; } + + /** + * @small + */ + public function testGetStats() + { + self::assertEmpty($this->cache->getStats()); + } } diff --git a/tests/src/Core/Cache/DatabaseCacheTest.php b/tests/src/Core/Cache/DatabaseCacheTest.php index 63cc7e4452..7b9f404ae1 100644 --- a/tests/src/Core/Cache/DatabaseCacheTest.php +++ b/tests/src/Core/Cache/DatabaseCacheTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Cache; -use Friendica\App\BaseURL; -use Friendica\Core\Cache; +use Friendica\Core\Cache\Type\DatabaseCache; +use Friendica\Test\CacheTestCase; use Friendica\Test\DatabaseTestTrait; use Friendica\Test\Util\CreateDatabaseTrait; use Friendica\Test\Util\VFSTrait; -class DatabaseCacheTest extends CacheTest +class DatabaseCacheTest extends CacheTestCase { use DatabaseTestTrait; use CreateDatabaseTrait; @@ -44,7 +30,7 @@ class DatabaseCacheTest extends CacheTest protected function getInstance() { - $this->cache = new Cache\Type\DatabaseCache('database', $this->getDbInstance()); + $this->cache = new DatabaseCache('database', $this->getDbInstance()); return $this->cache; } diff --git a/tests/src/Core/Cache/MemcacheCacheTest.php b/tests/src/Core/Cache/MemcacheCacheTest.php index b924b10455..c622f22216 100644 --- a/tests/src/Core/Cache/MemcacheCacheTest.php +++ b/tests/src/Core/Cache/MemcacheCacheTest.php @@ -1,36 +1,23 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Cache; use Exception; use Friendica\Core\Cache\Type\MemcacheCache; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Test\MemoryCacheTestCase; use Mockery; /** * @requires extension memcache * @group MEMCACHE */ -class MemcacheCacheTest extends MemoryCacheTest +class MemcacheCacheTest extends MemoryCacheTestCase { protected function getInstance() { @@ -72,4 +59,21 @@ class MemcacheCacheTest extends MemoryCacheTest { static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround'); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['version']); + self::assertIsNumeric($stats['hits']); + self::assertIsNumeric($stats['misses']); + self::assertIsNumeric($stats['evictions']); + self::assertIsNumeric($stats['entries']); + self::assertIsNumeric($stats['used_memory']); + self::assertGreaterThan(0, $stats['connected_clients']); + self::assertGreaterThan(0, $stats['uptime']); + } } diff --git a/tests/src/Core/Cache/MemcachedCacheTest.php b/tests/src/Core/Cache/MemcachedCacheTest.php index 80aca63d91..a1c3653f1b 100644 --- a/tests/src/Core/Cache/MemcachedCacheTest.php +++ b/tests/src/Core/Cache/MemcachedCacheTest.php @@ -1,29 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Cache; use Exception; use Friendica\Core\Cache\Type\MemcachedCache; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Test\MemoryCacheTestCase; use Mockery; use Psr\Log\NullLogger; @@ -31,7 +18,7 @@ use Psr\Log\NullLogger; * @requires extension memcached * @group MEMCACHED */ -class MemcachedCacheTest extends MemoryCacheTest +class MemcachedCacheTest extends MemoryCacheTestCase { protected function getInstance() { @@ -71,4 +58,21 @@ class MemcachedCacheTest extends MemoryCacheTest { static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround'); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['version']); + self::assertIsNumeric($stats['hits']); + self::assertIsNumeric($stats['misses']); + self::assertIsNumeric($stats['evictions']); + self::assertIsNumeric($stats['entries']); + self::assertIsNumeric($stats['used_memory']); + self::assertGreaterThan(0, $stats['connected_clients']); + self::assertGreaterThan(0, $stats['uptime']); + } } diff --git a/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php b/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php new file mode 100644 index 0000000000..3f44bcd0bf --- /dev/null +++ b/tests/src/Core/Cache/ProfilerCacheDecoratorTest.php @@ -0,0 +1,56 @@ +shouldReceive('get')->with('system', 'profiler')->once()->andReturn(false); + $config->shouldReceive('get')->with('rendertime', 'callstack')->once()->andReturn(false); + + $this->cache = new ProfilerCacheDecorator(new ArrayCache('localhost'), new Profiler($config)); + return $this->cache; + } + + protected function tearDown(): void + { + $this->cache->clear(false); + parent::tearDown(); + } + + /** + * @doesNotPerformAssertions + */ + public function testTTL() + { + // Array Cache doesn't support TTL + self::markTestSkipped("Array Cache doesn't support TTL"); + return true; + } + + /** + * @small + */ + public function testGetStats() + { + self::assertEmpty($this->cache->getStats()); + } + + public function testGetName() + { + self::assertStringEndsWith(' (with profiler)', $this->instance->getName()); + } +} diff --git a/tests/src/Core/Cache/RedisCacheTest.php b/tests/src/Core/Cache/RedisCacheTest.php index 427fbf69f9..d16bf5a64c 100644 --- a/tests/src/Core/Cache/RedisCacheTest.php +++ b/tests/src/Core/Cache/RedisCacheTest.php @@ -1,36 +1,23 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Cache; use Exception; use Friendica\Core\Cache\Type\RedisCache; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Test\MemoryCacheTestCase; use Mockery; /** * @requires extension redis * @group REDIS */ -class RedisCacheTest extends MemoryCacheTest +class RedisCacheTest extends MemoryCacheTestCase { protected function getInstance() { @@ -58,7 +45,7 @@ class RedisCacheTest extends MemoryCacheTest ->andReturn(null); try { - $this->cache = new \Friendica\Core\Cache\Type\RedisCache($host, $configMock); + $this->cache = new RedisCache($host, $configMock); } catch (Exception $e) { static::markTestSkipped('Redis is not available. Failure: ' . $e->getMessage()); } @@ -70,4 +57,21 @@ class RedisCacheTest extends MemoryCacheTest $this->cache->clear(false); parent::tearDown(); } + + /** + * @small + */ + public function testStats() + { + $stats = $this->instance->getStats(); + + self::assertNotNull($stats['version']); + self::assertIsNumeric($stats['hits']); + self::assertIsNumeric($stats['misses']); + self::assertIsNumeric($stats['evictions']); + self::assertIsNumeric($stats['entries']); + self::assertIsNumeric($stats['used_memory']); + self::assertGreaterThan(0, $stats['connected_clients']); + self::assertGreaterThan(0, $stats['uptime']); + } } diff --git a/tests/src/Core/Config/Cache/CacheTest.php b/tests/src/Core/Config/Cache/CacheTest.php index de73763dd7..fddf621bd0 100644 --- a/tests/src/Core/Config/Cache/CacheTest.php +++ b/tests/src/Core/Config/Cache/CacheTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Config\Cache; use Friendica\Core\Config\ValueObject\Cache; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use ParagonIE\HiddenString\HiddenString; use stdClass; -class CacheTest extends MockedTest +class CacheTest extends MockedTestCase { public function dataTests() { diff --git a/tests/src/Core/Config/Cache/ConfigFileManagerTest.php b/tests/src/Core/Config/Cache/ConfigFileManagerTest.php index 1385c8b19a..6ccfc9733c 100644 --- a/tests/src/Core/Config/Cache/ConfigFileManagerTest.php +++ b/tests/src/Core/Config/Cache/ConfigFileManagerTest.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Config\Cache; use Friendica\Core\Config\Factory\Config; use Friendica\Core\Config\ValueObject\Cache; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\VFSTrait; use Friendica\Core\Config\Util\ConfigFileManager; use org\bovigo\vfs\vfsStream; -class ConfigFileManagerTest extends MockedTest +class ConfigFileManagerTest extends MockedTestCase { use VFSTrait; @@ -48,6 +34,7 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); @@ -75,10 +62,11 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); - $configCache = new Cache(); + $configCache = new Cache(); $configFileLoader->setupCache($configCache); } @@ -104,10 +92,11 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); - $configCache = new Cache(); + $configCache = new Cache(); $configFileLoader->setupCache($configCache); @@ -141,10 +130,11 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); - $configCache = new Cache(); + $configCache = new Cache(); $configFileLoader->setupCache($configCache); @@ -177,10 +167,11 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); - $configCache = new Cache(); + $configCache = new Cache(); $configFileLoader->setupCache($configCache); @@ -231,6 +222,7 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); @@ -268,10 +260,11 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); - $configCache = new Cache(); + $configCache = new Cache(); $configFileLoader->setupCache($configCache); @@ -302,10 +295,11 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); - $configCache = new Cache(); + $configCache = new Cache(); $configFileLoader->setupCache($configCache); @@ -336,6 +330,7 @@ class ConfigFileManagerTest extends MockedTest $configFileLoader = new ConfigFileManager( $this->root->url(), + $this->root->url() . DIRECTORY_SEPARATOR . 'addon', $this->root->url() . DIRECTORY_SEPARATOR . Config::CONFIG_DIR, $this->root->url() . DIRECTORY_SEPARATOR . Config::STATIC_DIR ); @@ -355,8 +350,12 @@ class ConfigFileManagerTest extends MockedTest { $this->delConfigFile('local.config.php'); - $configFileManager = (new Config())->createConfigFileManager($this->root->url(), ['FRIENDICA_CONFIG_DIR' => '/a/wrong/dir/']); - $configCache = new Cache(); + $configFileManager = (new Config())->createConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + ['FRIENDICA_CONFIG_DIR' => '/a/wrong/dir/'], + ); + $configCache = new Cache(); $configFileManager->setupCache($configCache); @@ -381,11 +380,12 @@ class ConfigFileManagerTest extends MockedTest ->at($this->root->getChild('config2')) ->setContent(file_get_contents($fileDir . 'B.config.php')); - $configFileManager = (new Config())->createConfigFileManager($this->root->url(), - [ - 'FRIENDICA_CONFIG_DIR' => $this->root->getChild('config2')->url(), - ]); - $configCache = new Cache(); + $configFileManager = (new Config())->createConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + ['FRIENDICA_CONFIG_DIR' => $this->root->getChild('config2')->url()], + ); + $configCache = new Cache(); $configFileManager->setupCache($configCache); @@ -403,11 +403,12 @@ class ConfigFileManagerTest extends MockedTest ->at($this->root->getChild('config')) ->setContent(''); - $configFileManager = (new Config())->createConfigFileManager($this->root->url()); - $configCache = new Cache(); + $configFileManager = (new Config())->createConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + ); + $configCache = new Cache(); $configFileManager->setupCache($configCache); - - self::assertEquals(1,1); } } diff --git a/tests/src/Core/Config/ConfigTest.php b/tests/src/Core/Config/ConfigTest.php index 99c2141b8a..fc2c41a164 100644 --- a/tests/src/Core/Config/ConfigTest.php +++ b/tests/src/Core/Config/ConfigTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Config; @@ -27,12 +13,11 @@ use Friendica\Core\Config\Model\DatabaseConfig; use Friendica\Core\Config\Model\ReadOnlyFileConfig; use Friendica\Core\Config\Util\ConfigFileManager; use Friendica\Core\Config\ValueObject\Cache; -use Friendica\Test\DatabaseTest; +use Friendica\Test\DatabaseTestCase; use Friendica\Test\Util\CreateDatabaseTrait; use Friendica\Test\Util\VFSTrait; -use org\bovigo\vfs\vfsStream; -class ConfigTest extends DatabaseTest +class ConfigTest extends DatabaseTestCase { use ArraySubsetAsserts; use VFSTrait; @@ -69,8 +54,13 @@ class ConfigTest extends DatabaseTest parent::setUp(); - $this->configCache = new Cache(); - $this->configFileManager = new ConfigFileManager($this->root->url(), $this->root->url() . '/config/', $this->root->url() . '/static/'); + $this->configCache = new Cache(); + $this->configFileManager = new ConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + $this->root->url() . '/config', + $this->root->url() . '/static' + ); } /** @@ -108,7 +98,7 @@ class ConfigTest extends DatabaseTest 'key1' => 'value1a', 'key4' => 'value4', ], - 'other' => [ + 'other' => [ 'key5' => 'value5', 'key6' => 'value6', ], @@ -122,18 +112,18 @@ class ConfigTest extends DatabaseTest 'config', 'other' ], - 'load' => [ + 'load' => [ 'system', ], ], - 'other' => [ + 'other' => [ 'data' => $data, 'possibleCats' => [ 'system', 'config', 'other' ], - 'load' => [ + 'load' => [ 'other', ], ], @@ -144,18 +134,18 @@ class ConfigTest extends DatabaseTest 'config', 'other' ], - 'load' => [ + 'load' => [ 'config', ], ], - 'all' => [ + 'all' => [ 'data' => $data, 'possibleCats' => [ 'system', 'config', 'other' ], - 'load' => [ + 'load' => [ 'system', 'config', 'other' @@ -187,7 +177,7 @@ class ConfigTest extends DatabaseTest */ public function testSetUp(array $data) { - $this->loadDirectFixture($this->configToDbArray($data) , $this->getDbInstance()); + $this->loadDirectFixture($this->configToDbArray($data), $this->getDbInstance()); $this->testedConfig = $this->getInstance(); self::assertInstanceOf(Cache::class, $this->testedConfig->getCache()); @@ -223,13 +213,13 @@ class ConfigTest extends DatabaseTest { return [ 'config' => [ - 'data1' => [ + 'data1' => [ 'config' => [ 'key1' => 'value1', 'key2' => 'value2', ], ], - 'data2' => [ + 'data2' => [ 'config' => [ 'key1' => 'overwritten!', 'key3' => 'value3', @@ -244,19 +234,19 @@ class ConfigTest extends DatabaseTest ], ], ], - 'other' => [ - 'data1' => [ + 'other' => [ + 'data1' => [ 'config' => [ 'key12' => 'data4', 'key45' => 7, ], - 'other' => [ + 'other' => [ 'key1' => 'value1', 'key2' => 'value2', ], ], - 'data2' => [ - 'other' => [ + 'data2' => [ + 'other' => [ 'key1' => 'overwritten!', 'key3' => 'value3', ], @@ -266,7 +256,7 @@ class ConfigTest extends DatabaseTest ] ], 'expect' => [ - 'other' => [ + 'other' => [ // load should overwrite values everytime! 'key1' => 'overwritten!', 'key2' => 'value2', @@ -413,26 +403,26 @@ class ConfigTest extends DatabaseTest public function dataTestCat() { return [ - 'test_with_hashmap' => [ - 'data' => [ + 'test_with_hashmap' => [ + 'data' => [ 'test_with_hashmap' => [ 'notifyall' => [ 'last_update' => 1671051565, 'admin' => true, ], - 'blockbot' => [ + 'blockbot' => [ 'last_update' => 1658952852, 'admin' => true, ], ], - 'config' => [ + 'config' => [ 'register_policy' => 2, 'register_text' => '', 'sitename' => 'Friendica Social Network23', 'hostname' => 'friendica.local', 'private_addons' => false, ], - 'system' => [ + 'system' => [ 'dbclean_expire_conversation' => 90, ], ], @@ -442,14 +432,14 @@ class ConfigTest extends DatabaseTest 'last_update' => 1671051565, 'admin' => true, ], - 'blockbot' => [ + 'blockbot' => [ 'last_update' => 1658952852, 'admin' => true, ], ], ], - 'test_with_keys' => [ - 'data' => [ + 'test_with_keys' => [ + 'data' => [ 'test_with_keys' => [ [ 'last_update' => 1671051565, @@ -460,14 +450,14 @@ class ConfigTest extends DatabaseTest 'admin' => true, ], ], - 'config' => [ + 'config' => [ 'register_policy' => 2, 'register_text' => '', 'sitename' => 'Friendica Social Network23', 'hostname' => 'friendica.local', 'private_addons' => false, ], - 'system' => [ + 'system' => [ 'dbclean_expire_conversation' => 90, ], ], @@ -484,7 +474,7 @@ class ConfigTest extends DatabaseTest ], ], 'test_with_inner_array' => [ - 'data' => [ + 'data' => [ 'test_with_inner_array' => [ 'notifyall' => [ 'last_update' => 1671051565, @@ -493,19 +483,19 @@ class ConfigTest extends DatabaseTest 'no' => 1.5, ], ], - 'blogbot' => [ + 'blogbot' => [ 'last_update' => 1658952852, 'admin' => true, ], ], - 'config' => [ + 'config' => [ 'register_policy' => 2, 'register_text' => '', 'sitename' => 'Friendica Social Network23', 'hostname' => 'friendica.local', 'private_addons' => false, ], - 'system' => [ + 'system' => [ 'dbclean_expire_conversation' => 90, ], ], @@ -518,7 +508,7 @@ class ConfigTest extends DatabaseTest 'no' => 1.5, ], ], - 'blogbot' => [ + 'blogbot' => [ 'last_update' => 1658952852, 'admin' => true, ], @@ -533,7 +523,7 @@ class ConfigTest extends DatabaseTest public function testGetCategory(array $data, string $category, array $assertion) { $this->configCache = new Cache($data); - $config = new ReadOnlyFileConfig($this->configCache); + $config = new ReadOnlyFileConfig($this->configCache); self::assertEquals($assertion, $config->get($category)); } @@ -542,15 +532,15 @@ class ConfigTest extends DatabaseTest { return [ 'default' => [ - 'value' => ['test' => ['array']], + 'value' => ['test' => ['array']], 'assertion' => ['test' => ['array']], ], 'issue-12803' => [ - 'value' => 's:48:"s:40:"s:32:"https://punkrock-underground.com";";";', + 'value' => 's:48:"s:40:"s:32:"https://punkrock-underground.com";";";', 'assertion' => 'https://punkrock-underground.com', ], 'double-serialized-array' => [ - 'value' => 's:53:"a:1:{s:9:"testArray";a:1:{s:4:"with";s:7:"entries";}}";', + 'value' => 's:53:"a:1:{s:9:"testArray";a:1:{s:4:"with";s:7:"entries";}}";', 'assertion' => ['testArray' => ['with' => 'entries']], ], ]; @@ -572,33 +562,33 @@ class ConfigTest extends DatabaseTest $data = [ 'config' => [ 'admin_email' => 'value1', - 'timezone' => 'value2', - 'language' => 'value3', - 'sitename' => 'value', + 'timezone' => 'value2', + 'language' => 'value3', + 'sitename' => 'value', ], 'system' => [ - 'url' => 'value1a', + 'url' => 'value1a', 'debugging' => true, - 'logfile' => 'value4', - 'loglevel' => 'notice', - 'proflier' => true, + 'logfile' => 'value4', + 'loglevel' => 'notice', + 'proflier' => true, ], - 'proxy' => [ + 'proxy' => [ 'trusted_proxies' => 'value5', ], ]; return [ 'empty' => [ - 'data' => $data, - 'server' => [], + 'data' => $data, + 'server' => [], 'assertDisabled' => [], ], 'mixed' => [ 'data' => $data, 'server' => [ 'FRIENDICA_ADMIN_MAIL' => 'test@friendica.local', - 'FRIENDICA_DEBUGGING' => true, + 'FRIENDICA_DEBUGGING' => true, ], 'assertDisabled' => [ 'config' => [ @@ -622,7 +612,13 @@ class ConfigTest extends DatabaseTest $this->setConfigFile('static' . DIRECTORY_SEPARATOR . 'env.config.php', true); $this->loadDirectFixture($this->configToDbArray($data), $this->getDbInstance()); - $configFileManager = new ConfigFileManager($this->root->url(), $this->root->url() . '/config/', $this->root->url() . '/static/', $server); + $configFileManager = new ConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + $this->root->url() . '/config', + $this->root->url() . '/static', + $server + ); $configFileManager->setupCache($this->configCache); $config = new DatabaseConfig($this->getDbInstance(), $this->configCache); diff --git a/tests/src/Core/Config/ConfigTransactionTest.php b/tests/src/Core/Config/ConfigTransactionTest.php index 30e6d9885a..42ec830ce6 100644 --- a/tests/src/Core/Config/ConfigTransactionTest.php +++ b/tests/src/Core/Config/ConfigTransactionTest.php @@ -1,41 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Config; use Friendica\Core\Config\Capability\ISetConfigValuesTransactionally; use Friendica\Core\Config\Model\DatabaseConfig; -use Friendica\Core\Config\Model\ReadOnlyFileConfig; use Friendica\Core\Config\Model\ConfigTransaction; use Friendica\Core\Config\Util\ConfigFileManager; use Friendica\Core\Config\ValueObject\Cache; use Friendica\Database\Database; -use Friendica\Test\DatabaseTest; -use Friendica\Test\FixtureTest; -use Friendica\Test\MockedTest; -use Friendica\Test\Util\Database\StaticDatabase; -use Friendica\Test\Util\VFSTrait; +use Friendica\Test\FixtureTestCase; use Mockery\Exception\InvalidCountException; -class ConfigTransactionTest extends FixtureTest +class ConfigTransactionTest extends FixtureTestCase { /** @var ConfigFileManager */ protected $configFileManager; @@ -44,7 +25,12 @@ class ConfigTransactionTest extends FixtureTest { parent::setUp(); - $this->configFileManager = new ConfigFileManager($this->root->url(), $this->root->url() . '/config/', $this->root->url() . '/static/'); + $this->configFileManager = new ConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + $this->root->url() . '/config', + $this->root->url() . '/static' + ); } public function dataTests(): array @@ -110,7 +96,7 @@ class ConfigTransactionTest extends FixtureTest { $this->configFileManager = \Mockery::spy(ConfigFileManager::class); - $config = new DatabaseConfig($this->dice->create(Database::class), new Cache()); + $config = new DatabaseConfig($this->dice->create(Database::class), new Cache()); $configTransaction = new ConfigTransaction($config); // commit empty transaction diff --git a/tests/src/Core/Hooks/Model/InstanceManagerTest.php b/tests/src/Core/Hooks/Model/InstanceManagerTest.php index 7c5bea8029..ef22218945 100644 --- a/tests/src/Core/Hooks/Model/InstanceManagerTest.php +++ b/tests/src/Core/Hooks/Model/InstanceManagerTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Hooks\Model; @@ -26,13 +12,13 @@ use Friendica\Core\Hooks\Exceptions\HookInstanceException; use Friendica\Core\Hooks\Exceptions\HookRegisterArgumentException; use Friendica\Core\Hooks\Model\DiceInstanceManager; use Friendica\Core\Hooks\Util\StrategiesFileManager; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\Hooks\InstanceMocks\FakeInstance; use Friendica\Test\Util\Hooks\InstanceMocks\FakeInstanceDecorator; use Friendica\Test\Util\Hooks\InstanceMocks\IAmADecoratedInterface; use Mockery\MockInterface; -class InstanceManagerTest extends MockedTest +class InstanceManagerTest extends MockedTestCase { /** @var StrategiesFileManager|MockInterface */ protected $hookFileManager; diff --git a/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php b/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php index b70d3d8964..633b636701 100644 --- a/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php +++ b/tests/src/Core/Hooks/Util/StrategiesFileManagerTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Hooks\Util; @@ -25,13 +11,13 @@ use Friendica\Core\Addon\Capability\ICanLoadAddons; use Friendica\Core\Hooks\Capability\ICanRegisterStrategies; use Friendica\Core\Hooks\Exceptions\HookConfigException; use Friendica\Core\Hooks\Util\StrategiesFileManager; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\VFSTrait; use org\bovigo\vfs\vfsStream; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -class StrategiesFileManagerTest extends MockedTest +class StrategiesFileManagerTest extends MockedTestCase { use VFSTrait; diff --git a/tests/src/Core/InstallerTest.php b/tests/src/Core/InstallerTest.php index a446364dac..2f5873dfe6 100644 --- a/tests/src/Core/InstallerTest.php +++ b/tests/src/Core/InstallerTest.php @@ -1,49 +1,38 @@ . - * - */ -/// @todo this is in the same namespace as Install for mocking 'function_exists' -namespace Friendica\Core; +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Friendica\Test\src\Core; use Dice\Dice; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use Friendica\Core\Config\ValueObject\Cache; +use Friendica\Core\Installer; +use Friendica\Core\L10n; use Friendica\DI; use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; use Friendica\Network\HTTPClient\Capability\ICanSendHttpRequests; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\VFSTrait; use Mockery; use Mockery\MockInterface; +use phpmock\phpunit\PHPMock; -class InstallerTest extends MockedTest +class InstallerTest extends MockedTestCase { use VFSTrait; use ArraySubsetAsserts; + use PHPMock; /** * @var L10n|MockInterface */ private $l10nMock; /** - * @var Dice|MockInterface + * @var Dice&MockInterface */ private $dice; @@ -55,7 +44,7 @@ class InstallerTest extends MockedTest $this->l10nMock = Mockery::mock(L10n::class); - /** @var Dice|MockInterface $dice */ + /** @var Dice&MockInterface $dice */ $this->dice = Mockery::mock(Dice::class)->makePartial(); $this->dice = $this->dice->addRules(include __DIR__ . '/../../../static/dependencies.config.php'); @@ -110,6 +99,8 @@ class InstallerTest extends MockedTest $this->mockL10nT('Error: File Information PHP module required but not installed.', 1); $this->mockL10nT('GNU Multiple Precision PHP module', 1); $this->mockL10nT('Error: GNU Multiple Precision PHP module required but not installed.', 1); + $this->mockL10nT('IDN Functions PHP module', 1); + $this->mockL10nT('Error: IDN Functions PHP module required but not installed.', 1); $this->mockL10nT('Program execution functions', 1); $this->mockL10nT('Error: Program execution functions (proc_open) required but not enabled.', 1); } @@ -127,65 +118,50 @@ class InstallerTest extends MockedTest self::assertArraySubset($subSet, $assertionArray, false, "expected subset: " . PHP_EOL . print_r($subSet, true) . PHP_EOL . "current subset: " . print_r($assertionArray, true)); } - /** - * Replaces function_exists results with given mocks - * - * @param array $functions a list from function names and their result - */ - private function setFunctions(array $functions) + public static function getCheckKeysData(): array { - global $phpMock; - $phpMock['function_exists'] = function($function) use ($functions) { - foreach ($functions as $name => $value) { - if ($function == $name) { - return $value; - } - } - return '__phpunit_continue__'; - }; - } - - /** - * Replaces class_exist results with given mocks - * - * @param array $classes a list from class names and their results - */ - private function setClasses(array $classes) - { - global $phpMock; - $phpMock['class_exists'] = function($class) use ($classes) { - foreach ($classes as $name => $value) { - if ($class == $name) { - return $value; - } - } - return '__phpunit_continue__'; - }; + return [ + 'openssl_pkey_new does not exist' => ['openssl_pkey_new', false], + 'openssl_pkey_new does exists' => ['openssl_pkey_new', true], + ]; } /** * @small + * + * @dataProvider getCheckKeysData */ - public function testCheckKeys() + public function testCheckKeys($function, $expected) { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) use ($function, $expected) { + if ($function_name === $function) { + return $expected; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); - $this->setFunctions(['openssl_pkey_new' => false]); $install = new Installer(); - self::assertFalse($install->checkKeys()); - - $this->setFunctions(['openssl_pkey_new' => true]); - $install = new Installer(); - self::assertTrue($install->checkKeys()); + self::assertSame($expected, $install->checkKeys()); } /** * @small */ - public function testCheckFunctions() + public function testCheckFunctionsWithoutIntlChar() { + $class_exists = $this->getFunctionMock('Friendica\Core', 'class_exists'); + $class_exists->expects($this->any())->willReturnCallback(function($class_name) { + if ($class_name === 'IntlChar') { + return false; + } + return call_user_func_array('\class_exists', func_get_args()); + }); + $this->mockFunctionL10TCalls(); - $this->setClasses(['IntlChar' => false]); + $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(2, @@ -194,9 +170,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutCurlInit() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'curl_init') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['curl_init' => false, 'imagecreatefromjpeg' => true]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(4, @@ -205,9 +195,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutImagecreateformjpeg() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'imagecreatefromjpeg') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['imagecreatefromjpeg' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(5, @@ -216,9 +220,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutOpensslpublicencrypt() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'openssl_public_encrypt') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['openssl_public_encrypt' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(6, @@ -227,9 +245,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutMbStrlen() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'mb_strlen') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['mb_strlen' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(7, @@ -238,9 +270,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutIconvStrlen() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'iconv_strlen') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['iconv_strlen' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(8, @@ -249,9 +295,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutPosixkill() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'posix_kill') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['posix_kill' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(9, @@ -260,9 +320,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutProcOpen() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'proc_open') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['proc_open' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(10, @@ -271,8 +345,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['json_encode' => false]); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutJsonEncode() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'json_encode') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); + $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(11, @@ -281,9 +370,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutFinfoOpen() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'finfo_open') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['finfo_open' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(12, @@ -292,9 +395,23 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctionsWithoutGmpStrval() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'gmp_strval') { + return false; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + + $this->mockFunctionL10TCalls(true); - $this->mockFunctionL10TCalls(); - $this->setFunctions(['gmp_strval' => false]); $install = new Installer(); self::assertFalse($install->checkFunctions()); self::assertCheckExist(13, @@ -303,20 +420,36 @@ class InstallerTest extends MockedTest false, true, $install->getChecks()); + } + + /** + * @small + */ + public function testCheckFunctions() + { + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if (in_array( + $function_name, + [ + 'curl_init', + 'imagecreatefromjpeg', + 'openssl_public_encrypt', + 'mb_strlen', + 'iconv_strlen', + 'posix_kill', + 'json_encode', + 'finfo_open', + 'gmp_strval', + ] + )) { + return true; + } + return call_user_func_array('\function_exists', func_get_args()); + }); $this->mockFunctionL10TCalls(true); - $this->setFunctions([ - 'curl_init' => true, - 'imagecreatefromjpeg' => true, - 'openssl_public_encrypt' => true, - 'mb_strlen' => true, - 'iconv_strlen' => true, - 'posix_kill' => true, - 'json_encode' => true, - 'finfo_open' => true, - 'gmp_strval' => true, - ]); - $this->setClasses(['IntlChar' => true]); + $install = new Installer(); self::assertTrue($install->checkFunctions()); } @@ -343,11 +476,18 @@ class InstallerTest extends MockedTest /** * @small - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function testCheckHtAccessFail() { + // Mocking that we can use CURL + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'curl_init') { + return true; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); // Mocking the CURL Response @@ -365,11 +505,11 @@ class InstallerTest extends MockedTest // Mocking the CURL Request $networkMock = Mockery::mock(ICanSendHttpRequests::class); $networkMock - ->shouldReceive('fetchFull') + ->shouldReceive('get') ->with('https://test/install/testrewrite') ->andReturn($IHTTPResult); $networkMock - ->shouldReceive('fetchFull') + ->shouldReceive('get') ->with('http://test/install/testrewrite') ->andReturn($IHTTPResult); @@ -379,9 +519,6 @@ class InstallerTest extends MockedTest DI::init($this->dice, true); - // Mocking that we can use CURL - $this->setFunctions(['curl_init' => true]); - $install = new Installer(); self::assertFalse($install->checkHtAccess('https://test')); @@ -390,11 +527,18 @@ class InstallerTest extends MockedTest /** * @small - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function testCheckHtAccessWork() { + // Mocking that we can use CURL + $function_exists = $this->getFunctionMock('Friendica\Core', 'function_exists'); + $function_exists->expects($this->any())->willReturnCallback(function($function_name) { + if ($function_name === 'curl_init') { + return true; + } + return call_user_func_array('\function_exists', func_get_args()); + }); + $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); // Mocking the failed CURL Response @@ -412,11 +556,11 @@ class InstallerTest extends MockedTest // Mocking the CURL Request $networkMock = Mockery::mock(ICanSendHttpRequests::class); $networkMock - ->shouldReceive('fetchFull') + ->shouldReceive('get') ->with('https://test/install/testrewrite') ->andReturn($IHTTPResultF); $networkMock - ->shouldReceive('fetchFull') + ->shouldReceive('get') ->with('http://test/install/testrewrite') ->andReturn($IHTTPResultW); @@ -426,68 +570,20 @@ class InstallerTest extends MockedTest DI::init($this->dice, true); - // Mocking that we can use CURL - $this->setFunctions(['curl_init' => true]); - $install = new Installer(); self::assertTrue($install->checkHtAccess('https://test')); } - /** - * @small - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function testImagick() - { - static::markTestIncomplete('needs adapted class_exists() mock'); - - $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); - - $this->setClasses(['Imagick' => true]); - - $install = new Installer(); - - // even there is no supported type, Imagick should return true (because it is not required) - self::assertTrue($install->checkImagick()); - - self::assertCheckExist(1, - $this->l10nMock->t('ImageMagick supports GIF'), - '', - true, - false, - $install->getChecks()); - } - - /** - * @small - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function testImagickNotFound() - { - static::markTestIncomplete('Disabled due not working/difficult mocking global functions - needs more care!'); - - $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); - - $this->setClasses(['Imagick' => true]); - - $install = new Installer(); - - // even there is no supported type, Imagick should return true (because it is not required) - self::assertTrue($install->checkImagick()); - self::assertCheckExist(1, - $this->l10nMock->t('ImageMagick supports GIF'), - '', - false, - false, - $install->getChecks()); - } - public function testImagickNotInstalled() { - $this->setClasses(['Imagick' => false]); + $class_exists = $this->getFunctionMock('Friendica\Core', 'class_exists'); + $class_exists->expects($this->any())->willReturnCallback(function($class_name) { + if ($class_name === 'Imagick') { + return false; + } + return call_user_func_array('\class_exists', func_get_args()); + }); $this->mockL10nT('ImageMagick PHP extension is not installed'); $install = new Installer(); @@ -518,34 +614,3 @@ class InstallerTest extends MockedTest $install->setUpCache($configCache, '/test/'); } } - -/** - * A workaround to replace the PHP native function_exists with a mocked function - * - * @param string $function_name the Name of the function - * - * @return bool true or false - */ -function function_exists(string $function_name) -{ - global $phpMock; - if (isset($phpMock['function_exists'])) { - $result = call_user_func_array($phpMock['function_exists'], func_get_args()); - if ($result !== '__phpunit_continue__') { - return $result; - } - } - return call_user_func_array('\function_exists', func_get_args()); -} - -function class_exists($class_name) -{ - global $phpMock; - if (isset($phpMock['class_exists'])) { - $result = call_user_func_array($phpMock['class_exists'], func_get_args()); - if ($result !== '__phpunit_continue__') { - return $result; - } - } - return call_user_func_array('\class_exists', func_get_args()); -} diff --git a/tests/src/Core/KeyValueStorage/DBKeyValueStorageTest.php b/tests/src/Core/KeyValueStorage/DBKeyValueStorageTest.php index 3a3b255365..73699cd84e 100644 --- a/tests/src/Core/KeyValueStorage/DBKeyValueStorageTest.php +++ b/tests/src/Core/KeyValueStorage/DBKeyValueStorageTest.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\KeyValueStorage; use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs; use Friendica\Core\KeyValueStorage\Type\DBKeyValueStorage; use Friendica\Database\Database; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\CreateDatabaseTrait; -class DBKeyValueStorageTest extends KeyValueStorageTest +class DBKeyValueStorageTest extends MockedTestCase { use CreateDatabaseTrait; @@ -82,4 +69,79 @@ class DBKeyValueStorageTest extends KeyValueStorageTest self::assertGreaterThanOrEqual($updateAt, $updateAtAfter); } + + public function testInstance() + { + $instance = $this->getInstance(); + + self::assertInstanceOf(IManageKeyValuePairs::class, $instance); + } + + public function dataTests(): array + { + return [ + 'string' => ['k' => 'data', 'v' => 'it'], + 'boolTrue' => ['k' => 'data', 'v' => true], + 'boolFalse' => ['k' => 'data', 'v' => false], + 'integer' => ['k' => 'data', 'v' => 235], + 'decimal' => ['k' => 'data', 'v' => 2.456], + 'array' => ['k' => 'data', 'v' => ['1', 2, '3', true, false]], + 'boolIntTrue' => ['k' => 'data', 'v' => 1], + 'boolIntFalse' => ['k' => 'data', 'v' => 0], + ]; + } + + /** + * @dataProvider dataTests + */ + public function testGetSetDelete($k, $v) + { + $instance = $this->getInstance(); + + $instance->set($k, $v); + + self::assertEquals($v, $instance->get($k)); + self::assertEquals($v, $instance[$k]); + + $instance->delete($k); + + self::assertNull($instance->get($k)); + self::assertNull($instance[$k]); + } + + /** + * @dataProvider dataTests + */ + public function testSetOverride($k, $v) + { + $instance = $this->getInstance(); + + $instance->set($k, $v); + + self::assertEquals($v, $instance->get($k)); + self::assertEquals($v, $instance[$k]); + + $instance->set($k, 'another_value'); + + self::assertEquals('another_value', $instance->get($k)); + self::assertEquals('another_value', $instance[$k]); + } + + /** + * @dataProvider dataTests + */ + public function testOffsetSetDelete($k, $v) + { + $instance = $this->getInstance(); + + $instance[$k] = $v; + + self::assertEquals($v, $instance->get($k)); + self::assertEquals($v, $instance[$k]); + + unset($instance[$k]); + + self::assertNull($instance->get($k)); + self::assertNull($instance[$k]); + } } diff --git a/tests/src/Core/KeyValueStorage/KeyValueStorageTest.php b/tests/src/Core/KeyValueStorage/KeyValueStorageTest.php deleted file mode 100644 index ddb0acc4dc..0000000000 --- a/tests/src/Core/KeyValueStorage/KeyValueStorageTest.php +++ /dev/null @@ -1,105 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Core\KeyValueStorage; - -use Friendica\Core\KeyValueStorage\Capability\IManageKeyValuePairs; -use Friendica\Test\MockedTest; - -abstract class KeyValueStorageTest extends MockedTest -{ - abstract public function getInstance(): IManageKeyValuePairs; - - public function testInstance() - { - $instance = $this->getInstance(); - - self::assertInstanceOf(IManageKeyValuePairs::class, $instance); - } - - public function dataTests(): array - { - return [ - 'string' => ['k' => 'data', 'v' => 'it'], - 'boolTrue' => ['k' => 'data', 'v' => true], - 'boolFalse' => ['k' => 'data', 'v' => false], - 'integer' => ['k' => 'data', 'v' => 235], - 'decimal' => ['k' => 'data', 'v' => 2.456], - 'array' => ['k' => 'data', 'v' => ['1', 2, '3', true, false]], - 'boolIntTrue' => ['k' => 'data', 'v' => 1], - 'boolIntFalse' => ['k' => 'data', 'v' => 0], - ]; - } - - /** - * @dataProvider dataTests - */ - public function testGetSetDelete($k, $v) - { - $instance = $this->getInstance(); - - $instance->set($k, $v); - - self::assertEquals($v, $instance->get($k)); - self::assertEquals($v, $instance[$k]); - - $instance->delete($k); - - self::assertNull($instance->get($k)); - self::assertNull($instance[$k]); - } - - /** - * @dataProvider dataTests - */ - public function testSetOverride($k, $v) - { - $instance = $this->getInstance(); - - $instance->set($k, $v); - - self::assertEquals($v, $instance->get($k)); - self::assertEquals($v, $instance[$k]); - - $instance->set($k, 'another_value'); - - self::assertEquals('another_value', $instance->get($k)); - self::assertEquals('another_value', $instance[$k]); - } - - /** - * @dataProvider dataTests - */ - public function testOffsetSetDelete($k, $v) - { - $instance = $this->getInstance(); - - $instance[$k] = $v; - - self::assertEquals($v, $instance->get($k)); - self::assertEquals($v, $instance[$k]); - - unset($instance[$k]); - - self::assertNull($instance->get($k)); - self::assertNull($instance[$k]); - } -} diff --git a/tests/src/Core/L10nTest.php b/tests/src/Core/L10nTest.php index 9ae0c26e5c..01dc301d78 100644 --- a/tests/src/Core/L10nTest.php +++ b/tests/src/Core/L10nTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core; use Friendica\Core\L10n; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class L10nTest extends MockedTest +class L10nTest extends MockedTestCase { public function dataDetectLanguage() { diff --git a/tests/src/Core/Lock/APCuCacheLockTest.php b/tests/src/Core/Lock/APCuCacheLockTest.php index b5a2704303..3b6c7904b4 100644 --- a/tests/src/Core/Lock/APCuCacheLockTest.php +++ b/tests/src/Core/Lock/APCuCacheLockTest.php @@ -1,45 +1,45 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\APCuCache; +use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Core\Lock\Type\CacheLock; +use Friendica\Test\CacheLockTestCase; /** * @group APCU */ -class APCuCacheLockTest extends LockTest +class APCuCacheLockTest extends CacheLockTestCase { + private APCuCache $cache; + private ICanLock $lock; + protected function setUp(): void { if (!APCuCache::isAvailable()) { static::markTestSkipped('APCu is not available'); } + $this->cache = new APCuCache('localhost'); + $this->lock = new CacheLock($this->cache); + parent::setUp(); } - protected function getInstance() + protected function getInstance(): CacheLock { - return new \Friendica\Core\Lock\Type\CacheLock(new APCuCache('localhost')); + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } } diff --git a/tests/src/Core/Lock/ArrayCacheLockTest.php b/tests/src/Core/Lock/ArrayCacheLockTest.php index 5a92de870b..07cd88dd1c 100644 --- a/tests/src/Core/Lock/ArrayCacheLockTest.php +++ b/tests/src/Core/Lock/ArrayCacheLockTest.php @@ -1,34 +1,38 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\ArrayCache; use Friendica\Core\Lock\Type\CacheLock; +use Friendica\Test\CacheLockTestCase; -class ArrayCacheLockTest extends LockTest +class ArrayCacheLockTest extends CacheLockTestCase { - protected function getInstance() + private CacheLock $lock; + private ArrayCache $cache; + + protected function setUp(): void { - return new \Friendica\Core\Lock\Type\CacheLock(new ArrayCache('localhost')); + $this->cache = new ArrayCache('localhost'); + $this->lock = new CacheLock($this->cache); + + parent::setUp(); + } + + protected function getInstance(): CacheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } /** diff --git a/tests/src/Core/Lock/DatabaseLockDriverTest.php b/tests/src/Core/Lock/DatabaseLockDriverTest.php index 3e8a917ad2..fbfe61762e 100644 --- a/tests/src/Core/Lock/DatabaseLockDriverTest.php +++ b/tests/src/Core/Lock/DatabaseLockDriverTest.php @@ -1,30 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; +use Friendica\Core\Lock\Capability\ICanLock; use Friendica\Core\Lock\Type\DatabaseLock; +use Friendica\Test\LockTestCase; use Friendica\Test\Util\CreateDatabaseTrait; -class DatabaseLockDriverTest extends LockTest +class DatabaseLockDriverTest extends LockTestCase { use CreateDatabaseTrait; @@ -39,7 +27,7 @@ class DatabaseLockDriverTest extends LockTest parent::setUp(); } - protected function getInstance() + protected function getInstance(): ICanLock { return new DatabaseLock($this->getDbInstance(), $this->pid); } diff --git a/tests/src/Core/Lock/MemcacheCacheLockTest.php b/tests/src/Core/Lock/MemcacheCacheLockTest.php index fee3195536..8915e6d37c 100644 --- a/tests/src/Core/Lock/MemcacheCacheLockTest.php +++ b/tests/src/Core/Lock/MemcacheCacheLockTest.php @@ -1,39 +1,30 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; use Exception; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\MemcacheCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Type\CacheLock; +use Friendica\Test\CacheLockTestCase; use Mockery; /** * @requires extension Memcache * @group MEMCACHE */ -class MemcacheCacheLockTest extends LockTest +class MemcacheCacheLockTest extends CacheLockTestCase { - protected function getInstance() + private CacheLock $lock; + private MemcacheCache $cache; + + protected function setUp(): void { $configMock = Mockery::mock(IManageConfigValues::class); @@ -49,16 +40,24 @@ class MemcacheCacheLockTest extends LockTest ->with('system', 'memcache_port') ->andReturn($port); - $lock = null; - try { - $cache = new MemcacheCache($host, $configMock); - $lock = new \Friendica\Core\Lock\Type\CacheLock($cache); + $this->cache = new MemcacheCache($host, $configMock); + $this->lock = new CacheLock($this->cache); } catch (Exception $e) { static::markTestSkipped('Memcache is not available'); } - return $lock; + parent::setUp(); + } + + protected function getInstance(): CacheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } /** diff --git a/tests/src/Core/Lock/MemcachedCacheLockTest.php b/tests/src/Core/Lock/MemcachedCacheLockTest.php index b2ce4ceeb4..522f60c64a 100644 --- a/tests/src/Core/Lock/MemcachedCacheLockTest.php +++ b/tests/src/Core/Lock/MemcachedCacheLockTest.php @@ -1,30 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; use Exception; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\MemcachedCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Type\CacheLock; +use Friendica\Test\CacheLockTestCase; use Mockery; use Psr\Log\NullLogger; @@ -32,9 +20,12 @@ use Psr\Log\NullLogger; * @requires extension memcached * @group MEMCACHED */ -class MemcachedCacheLockTest extends LockTest +class MemcachedCacheLockTest extends CacheLockTestCase { - protected function getInstance() + private MemcachedCache $cache; + private CacheLock $lock; + + protected function setUp(): void { $configMock = Mockery::mock(IManageConfigValues::class); @@ -48,16 +39,24 @@ class MemcachedCacheLockTest extends LockTest $logger = new NullLogger(); - $lock = null; - try { - $cache = new MemcachedCache($host, $configMock, $logger); - $lock = new CacheLock($cache); + $this->cache = new MemcachedCache($host, $configMock, $logger); + $this->lock = new CacheLock($this->cache); } catch (Exception $e) { static::markTestSkipped('Memcached is not available'); } - return $lock; + parent::setUp(); + } + + protected function getInstance(): CacheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } /** diff --git a/tests/src/Core/Lock/RedisCacheLockTest.php b/tests/src/Core/Lock/RedisCacheLockTest.php index adda4526c0..1136b80c4b 100644 --- a/tests/src/Core/Lock/RedisCacheLockTest.php +++ b/tests/src/Core/Lock/RedisCacheLockTest.php @@ -1,39 +1,27 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; use Exception; +use Friendica\Core\Cache\Capability\ICanCacheInMemory; use Friendica\Core\Cache\Type\RedisCache; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Lock\Type\CacheLock; +use Friendica\Test\CacheLockTestCase; use Mockery; /** * @requires extension redis * @group REDIS */ -class RedisCacheLockTest extends LockTest +class RedisCacheLockTest extends CacheLockTestCase { - protected function getInstance() + protected function setUp(): void { $configMock = Mockery::mock(IManageConfigValues::class); @@ -58,15 +46,23 @@ class RedisCacheLockTest extends LockTest ->with('system', 'redis_password') ->andReturn(null); - $lock = null; - try { - $cache = new RedisCache($host, $configMock); - $lock = new \Friendica\Core\Lock\Type\CacheLock($cache); + $this->cache = new RedisCache($host, $configMock); + $this->lock = new CacheLock($this->cache); } catch (Exception $e) { static::markTestSkipped('Redis is not available. Error: ' . $e->getMessage()); } - return $lock; + parent::setUp(); + } + + protected function getInstance(): CAcheLock + { + return $this->lock; + } + + protected function getCache(): ICanCacheInMemory + { + return $this->cache; } } diff --git a/tests/src/Core/Lock/SemaphoreLockTest.php b/tests/src/Core/Lock/SemaphoreLockTest.php index 41e73891ee..30152ac427 100644 --- a/tests/src/Core/Lock/SemaphoreLockTest.php +++ b/tests/src/Core/Lock/SemaphoreLockTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Lock; @@ -26,12 +12,15 @@ use Friendica\App; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Model\ReadOnlyFileConfig; use Friendica\Core\Config\ValueObject\Cache; +use Friendica\Core\Lock\Capability\ICanLock; +use Friendica\Core\Lock\Type\SemaphoreLock; use Friendica\Core\System; use Friendica\DI; +use Friendica\Test\LockTestCase; use Mockery; use Mockery\MockInterface; -class SemaphoreLockTest extends LockTest +class SemaphoreLockTest extends LockTestCase { protected function setUp(): void { @@ -43,7 +32,7 @@ class SemaphoreLockTest extends LockTest $dice->shouldReceive('create')->with(App::class)->andReturn($app); $configCache = new Cache(['system' => ['temppath' => '/tmp']]); - $configMock = new ReadOnlyFileConfig($configCache); + $configMock = new ReadOnlyFileConfig($configCache); $dice->shouldReceive('create')->with(IManageConfigValues::class)->andReturn($configMock); // @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject @@ -52,9 +41,9 @@ class SemaphoreLockTest extends LockTest parent::setUp(); } - protected function getInstance() + protected function getInstance(): ICanLock { - return new \Friendica\Core\Lock\Type\SemaphoreLock(); + return new SemaphoreLock(); } /** diff --git a/tests/src/Core/Logger/ProfilerLoggerTest.php b/tests/src/Core/Logger/ProfilerLoggerTest.php index 3784c1189f..f137e8c7fa 100644 --- a/tests/src/Core/Logger/ProfilerLoggerTest.php +++ b/tests/src/Core/Logger/ProfilerLoggerTest.php @@ -1,34 +1,21 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Logger; -use Friendica\Test\MockedTest; use Friendica\Core\Logger\Type\ProfilerLogger; +use Friendica\Test\LoggerDataTrait; +use Friendica\Test\MockedTestCase; use Friendica\Util\Profiler; use Mockery\MockInterface; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; -class ProfilerLoggerTest extends MockedTest +class ProfilerLoggerTest extends MockedTestCase { use LoggerDataTrait; diff --git a/tests/src/Core/Logger/StreamLoggerTest.php b/tests/src/Core/Logger/StreamLoggerTest.php index a1e0af5bbb..5d6dc84c44 100644 --- a/tests/src/Core/Logger/StreamLoggerTest.php +++ b/tests/src/Core/Logger/StreamLoggerTest.php @@ -1,35 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Logger; use Friendica\Core\Logger\Exception\LoggerArgumentException; use Friendica\Core\Logger\Exception\LogLevelException; +use Friendica\Test\LoggerTestCase; use Friendica\Test\Util\VFSTrait; use Friendica\Core\Logger\Type\StreamLogger; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamFile; use Psr\Log\LogLevel; -class StreamLoggerTest extends AbstractLoggerTest +class StreamLoggerTest extends LoggerTestCase { use VFSTrait; diff --git a/tests/src/Core/Logger/SyslogLoggerFactoryWrapper.php b/tests/src/Core/Logger/SyslogLoggerFactoryWrapper.php index 63209cad31..6bc30bc439 100644 --- a/tests/src/Core/Logger/SyslogLoggerFactoryWrapper.php +++ b/tests/src/Core/Logger/SyslogLoggerFactoryWrapper.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Logger; @@ -31,16 +17,16 @@ class SyslogLoggerFactoryWrapper extends SyslogLogger { public function create(IManageConfigValues $config): LoggerInterface { - $logOpts = $config->get('system', 'syslog_flags') ?? SyslogLoggerClass::DEFAULT_FLAGS; - $logFacility = $config->get('system', 'syslog_facility') ?? SyslogLoggerClass::DEFAULT_FACILITY; + $logOpts = (int) $config->get('system', 'syslog_flags') ?? SyslogLoggerClass::DEFAULT_FLAGS; + $logFacility = (int) $config->get('system', 'syslog_facility') ?? SyslogLoggerClass::DEFAULT_FACILITY; $loglevel = SyslogLogger::mapLegacyConfigDebugLevel($config->get('system', 'loglevel')); - if (array_key_exists($loglevel, SyslogLoggerClass::logLevels)) { - $loglevel = SyslogLoggerClass::logLevels[$loglevel]; - } else { + if (!array_key_exists($loglevel, SyslogLoggerClass::logLevels)) { throw new LogLevelException(sprintf('The level "%s" is not valid.', $loglevel)); } + $loglevel = SyslogLoggerClass::logLevels[$loglevel]; + return new SyslogLoggerWrapper($this->channel, $this->introspection, $loglevel, $logOpts, $logFacility); } } diff --git a/tests/src/Core/Logger/SyslogLoggerTest.php b/tests/src/Core/Logger/SyslogLoggerTest.php index c22aecc10b..ecdaf19e27 100644 --- a/tests/src/Core/Logger/SyslogLoggerTest.php +++ b/tests/src/Core/Logger/SyslogLoggerTest.php @@ -1,31 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Logger; use Friendica\Core\Logger\Exception\LogLevelException; use Friendica\Core\Logger\Type\SyslogLogger; +use Friendica\Test\LoggerTestCase; use Psr\Log\LogLevel; -class SyslogLoggerTest extends AbstractLoggerTest +class SyslogLoggerTest extends LoggerTestCase { /** * @var SyslogLoggerWrapper diff --git a/tests/src/Core/Logger/SyslogLoggerWrapper.php b/tests/src/Core/Logger/SyslogLoggerWrapper.php index dce28b164c..df2944c4c5 100644 --- a/tests/src/Core/Logger/SyslogLoggerWrapper.php +++ b/tests/src/Core/Logger/SyslogLoggerWrapper.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Logger; @@ -31,7 +17,7 @@ class SyslogLoggerWrapper extends SyslogLogger { private $content; - public function __construct(string $channel, IHaveCallIntrospections $introspection, string $logLevel, string $logOptions, string $logFacility) + public function __construct(string $channel, IHaveCallIntrospections $introspection, int $logLevel, int $logOptions, int $logFacility) { parent::__construct($channel, $introspection, $logLevel, $logOptions, $logFacility); diff --git a/tests/src/Core/Logger/WorkerLoggerTest.php b/tests/src/Core/Logger/WorkerLoggerTest.php index 5ac2dc6a2f..ab3e41d9d1 100644 --- a/tests/src/Core/Logger/WorkerLoggerTest.php +++ b/tests/src/Core/Logger/WorkerLoggerTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Logger; use Friendica\Core\Logger\Type\WorkerLogger; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Psr\Log\LoggerInterface; -class WorkerLoggerTest extends MockedTest +class WorkerLoggerTest extends MockedTestCase { private function assertUid($uid) { diff --git a/tests/src/Core/PConfig/Cache/CacheTest.php b/tests/src/Core/PConfig/Cache/CacheTest.php index 752c0de363..a4d968730b 100644 --- a/tests/src/Core/PConfig/Cache/CacheTest.php +++ b/tests/src/Core/PConfig/Cache/CacheTest.php @@ -1,30 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\PConfig\Cache; -use Friendica\Core\PConfig\Cache; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class CacheTest extends MockedTest +class CacheTest extends MockedTestCase { public function dataTests() { diff --git a/tests/src/Core/PConfig/JitPConfigTest.php b/tests/src/Core/PConfig/JitPConfigTest.php index 2d4411e519..e452ffe92c 100644 --- a/tests/src/Core/PConfig/JitPConfigTest.php +++ b/tests/src/Core/PConfig/JitPConfigTest.php @@ -1,29 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\PConfig; use Friendica\Core\PConfig\Type\JitPConfig; +use Friendica\Test\PConfigTestCase; -class JitPConfigTest extends PConfigTest +class JitPConfigTest extends PConfigTestCase { public function getInstance() { diff --git a/tests/src/Core/PConfig/PreloadPConfigTest.php b/tests/src/Core/PConfig/PreloadPConfigTest.php index 01fcf43f64..a7cba04045 100644 --- a/tests/src/Core/PConfig/PreloadPConfigTest.php +++ b/tests/src/Core/PConfig/PreloadPConfigTest.php @@ -1,33 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\PConfig; use Friendica\Core\PConfig\Type\PreloadPConfig; +use Friendica\Test\PConfigTestCase; -class PreloadPConfigTest extends PConfigTest +class PreloadPConfigTest extends PConfigTestCase { public function getInstance() { - return new \Friendica\Core\PConfig\Type\PreloadPConfig($this->configCache, $this->configModel); + return new PreloadPConfig($this->configCache, $this->configModel); } /** diff --git a/tests/src/Core/Session/UserSessionTest.php b/tests/src/Core/Session/UserSessionTest.php index 8d46098089..cecae1d90c 100644 --- a/tests/src/Core/Session/UserSessionTest.php +++ b/tests/src/Core/Session/UserSessionTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Session; use Friendica\Core\Session\Model\UserSession; use Friendica\Core\Session\Type\ArraySession; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class UserSessionTest extends MockedTest +class UserSessionTest extends MockedTestCase { public function dataLocalUserId() { @@ -166,13 +152,13 @@ class UserSessionTest extends MockedTest 'data' => [ 'remote' => ['3' => '21'], ], - 'expected' => false, + 'expected' => 0, ], 'empty' => [ 'cid' => 21, 'data' => [ ], - 'expected' => false, + 'expected' => 0, ], ]; } @@ -181,7 +167,7 @@ class UserSessionTest extends MockedTest public function testGetUserIdForVisitorContactID(int $cid, array $data, $expected) { $userSession = new UserSession(new ArraySession($data)); - $this->assertEquals($expected, $userSession->getUserIDForVisitorContactID($cid)); + $this->assertSame($expected, $userSession->getUserIDForVisitorContactID($cid)); } public function dataAuthenticated() @@ -190,6 +176,7 @@ class UserSessionTest extends MockedTest 'authenticated' => [ 'data' => [ 'authenticated' => true, + 'uid' => 21, ], 'expected' => true, ], @@ -199,6 +186,13 @@ class UserSessionTest extends MockedTest ], 'expected' => false, ], + 'remote_visitor' => [ + 'data' => [ + 'authenticated' => true, + 'visitor_id' => 21, + ], + 'expected' => false, + ], 'missing' => [ 'data' => [ ], @@ -215,4 +209,104 @@ class UserSessionTest extends MockedTest $userSession = new UserSession(new ArraySession($data)); $this->assertEquals($expected, $userSession->isAuthenticated()); } + + public function dataIsVisitor() + { + return [ + 'local_user' => [ + 'data' => [ + 'authenticated' => true, + 'uid' => 21, + ], + 'expected' => false, + ], + 'not_authenticated' => [ + 'data' => [ + 'authenticated' => false, + ], + 'expected' => false, + ], + 'remote_visitor' => [ + 'data' => [ + 'authenticated' => true, + 'visitor_id' => 21, + ], + 'expected' => true, + ], + 'remote_unauthenticated_visitor' => [ + 'data' => [ + 'authenticated' => false, + 'visitor_id' => 21, + ], + 'expected' => false, + ], + 'missing' => [ + 'data' => [ + ], + 'expected' => false, + ], + ]; + } + + /** + * @dataProvider dataIsVisitor + */ + public function testIsVisitor(array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->isVisitor()); + } + + public function dataIsUnauthenticated() + { + return [ + 'local_user' => [ + 'data' => [ + 'authenticated' => true, + 'uid' => 21, + ], + 'expected' => false, + ], + 'not_authenticated' => [ + 'data' => [ + 'authenticated' => false, + ], + 'expected' => true, + ], + 'authenticated' => [ + 'data' => [ + 'authenticated' => true, + ], + 'expected' => false, + ], + 'remote_visitor' => [ + 'data' => [ + 'authenticated' => true, + 'visitor_id' => 21, + ], + 'expected' => false, + ], + 'remote_unauthenticated_visitor' => [ + 'data' => [ + 'authenticated' => false, + 'visitor_id' => 21, + ], + 'expected' => true, + ], + 'missing' => [ + 'data' => [ + ], + 'expected' => true, + ], + ]; + } + + /** + * @dataProvider dataIsUnauthenticated + */ + public function testIsUnauthenticated(array $data, $expected) + { + $userSession = new UserSession(new ArraySession($data)); + $this->assertEquals($expected, $userSession->isUnauthenticated()); + } } diff --git a/tests/src/Core/Storage/DatabaseStorageTest.php b/tests/src/Core/Storage/DatabaseStorageTest.php index 093a2353ee..ab3c514b1d 100644 --- a/tests/src/Core/Storage/DatabaseStorageTest.php +++ b/tests/src/Core/Storage/DatabaseStorageTest.php @@ -1,30 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Storage; use Friendica\Core\Storage\Type\Database; +use Friendica\Test\StorageTestCase; use Friendica\Test\Util\CreateDatabaseTrait; -class DatabaseStorageTest extends StorageTest +class DatabaseStorageTest extends StorageTestCase { use CreateDatabaseTrait; diff --git a/tests/src/Core/Storage/FilesystemStorageConfigTest.php b/tests/src/Core/Storage/FilesystemStorageConfigTest.php index b7fbc63f91..390266f565 100644 --- a/tests/src/Core/Storage/FilesystemStorageConfigTest.php +++ b/tests/src/Core/Storage/FilesystemStorageConfigTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Storage; @@ -25,11 +11,12 @@ use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Core\Storage\Capability\ICanConfigureStorage; use Friendica\Core\Storage\Type\FilesystemConfig; +use Friendica\Test\StorageConfigTestCase; use Friendica\Test\Util\VFSTrait; use Mockery\MockInterface; use org\bovigo\vfs\vfsStream; -class FilesystemStorageConfigTest extends StorageConfigTest +class FilesystemStorageConfigTest extends StorageConfigTestCase { use VFSTrait; diff --git a/tests/src/Core/Storage/FilesystemStorageTest.php b/tests/src/Core/Storage/FilesystemStorageTest.php index c3a66eb0f4..2d2cfa8d3a 100644 --- a/tests/src/Core/Storage/FilesystemStorageTest.php +++ b/tests/src/Core/Storage/FilesystemStorageTest.php @@ -1,33 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Storage; use Friendica\Core\Storage\Exception\StorageException; use Friendica\Core\Storage\Type\Filesystem; use Friendica\Core\Storage\Type\FilesystemConfig; +use Friendica\Test\StorageTestCase; use Friendica\Test\Util\VFSTrait; use org\bovigo\vfs\vfsStream; -class FilesystemStorageTest extends StorageTest +class FilesystemStorageTest extends StorageTestCase { use VFSTrait; diff --git a/tests/src/Core/Storage/Repository/StorageManagerTest.php b/tests/src/Core/Storage/Repository/StorageManagerTest.php index 446e93a7aa..8af22c4b78 100644 --- a/tests/src/Core/Storage/Repository/StorageManagerTest.php +++ b/tests/src/Core/Storage/Repository/StorageManagerTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Storage\Repository; @@ -35,29 +21,24 @@ use Friendica\Core\Storage\Repository\StorageManager; use Friendica\Core\Storage\Type\Filesystem; use Friendica\Core\Storage\Type\SystemResource; use Friendica\Database\Database; -use Friendica\Database\Definition\DbaDefinition; -use Friendica\Database\Definition\ViewDefinition; use Friendica\DI; use Friendica\Core\Config\Factory\Config; +use Friendica\Core\Hooks\HookEventBridge; use Friendica\Core\Storage\Type; -use Friendica\Test\DatabaseTest; +use Friendica\Test\DatabaseTestCase; use Friendica\Test\Util\CreateDatabaseTrait; use Friendica\Test\Util\Database\StaticDatabase; -use Friendica\Test\Util\VFSTrait; -use Friendica\Util\Profiler; +use Friendica\Test\Util\FakeEventDispatcher; use org\bovigo\vfs\vfsStream; -use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Friendica\Test\Util\SampleStorageBackend; -class StorageManagerTest extends DatabaseTest +class StorageManagerTest extends DatabaseTestCase { use CreateDatabaseTrait; /** @var IManageConfigValues */ private $config; - /** @var LoggerInterface */ - private $logger; /** @var L10n */ private $l10n; @@ -74,12 +55,14 @@ class StorageManagerTest extends DatabaseTest vfsStream::newDirectory(Type\FilesystemConfig::DEFAULT_BASE_FOLDER, 0777)->at($this->root); - $this->logger = new NullLogger(); $this->database = $this->getDbInstance(); $configFactory = new Config(); - $configFileManager = $configFactory->createConfigFileManager($this->root->url()); - $configCache = $configFactory->createCache($configFileManager); + $configFileManager = $configFactory->createConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + ); + $configCache = $configFactory->createCache($configFileManager); $this->config = new \Friendica\Core\Config\Model\DatabaseConfig($this->database, $configCache); $this->config->set('storage', 'name', 'Database'); @@ -102,7 +85,14 @@ class StorageManagerTest extends DatabaseTest */ public function testInstance() { - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); self::assertInstanceOf(StorageManager::class, $storageManager); } @@ -110,21 +100,21 @@ class StorageManagerTest extends DatabaseTest public function dataStorages() { return [ - 'empty' => [ + 'empty' => [ 'name' => '', 'valid' => false, 'interface' => ICanReadFromStorage::class, 'assert' => null, 'assertName' => '', ], - 'database' => [ + 'database' => [ 'name' => Type\Database::NAME, 'valid' => true, 'interface' => ICanWriteToStorage::class, 'assert' => Type\Database::class, 'assertName' => Type\Database::NAME, ], - 'filesystem' => [ + 'filesystem' => [ 'name' => Filesystem::NAME, 'valid' => true, 'interface' => ICanWriteToStorage::class, @@ -138,7 +128,7 @@ class StorageManagerTest extends DatabaseTest 'assert' => SystemResource::class, 'assertName' => SystemResource::NAME, ], - 'invalid' => [ + 'invalid' => [ 'name' => 'invalid', 'valid' => false, 'interface' => null, @@ -164,7 +154,14 @@ class StorageManagerTest extends DatabaseTest $this->config->set('storage', 'name', $name); } - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); if ($interface === ICanWriteToStorage::class) { $storage = $storageManager->getWritableStorageByName($name); @@ -184,7 +181,14 @@ class StorageManagerTest extends DatabaseTest */ public function testIsValidBackend($name, $valid, $interface, $assert, $assertName) { - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); // true in every of the backends self::assertEquals(!empty($assertName), $storageManager->isValidBackend($name)); @@ -198,7 +202,14 @@ class StorageManagerTest extends DatabaseTest */ public function testListBackends() { - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); self::assertEquals(StorageManager::DEFAULT_BACKENDS, $storageManager->listBackends()); } @@ -214,7 +225,14 @@ class StorageManagerTest extends DatabaseTest static::markTestSkipped('only works for ICanWriteToStorage'); } - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); $selBackend = $storageManager->getWritableStorageByName($name); $storageManager->setBackend($selBackend); @@ -234,7 +252,14 @@ class StorageManagerTest extends DatabaseTest $this->expectException(InvalidClassStorageException::class); } - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); self::assertInstanceOf($assert, $storageManager->getBackend()); } @@ -255,7 +280,14 @@ class StorageManagerTest extends DatabaseTest ->addRule(IHandleSessions::class, ['instanceOf' => Memory::class, 'shared' => true, 'call' => null]); DI::init($dice); - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); self::assertTrue($storageManager->register(SampleStorageBackend::class)); @@ -283,7 +315,21 @@ class StorageManagerTest extends DatabaseTest ->addRule(IHandleSessions::class, ['instanceOf' => Memory::class, 'shared' => true, 'call' => null]); DI::init($dice); - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = DI::eventDispatcher(); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } + + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + $eventDispatcher, + $this->l10n, + false + ); self::assertTrue($storageManager->register(SampleStorageBackend::class)); @@ -322,8 +368,15 @@ class StorageManagerTest extends DatabaseTest $this->loadFixture(__DIR__ . '/../../../../datasets/storage/database.fixture.php', $this->database); - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); - $storage = $storageManager->getWritableStorageByName($name); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); + $storage = $storageManager->getWritableStorageByName($name); $storageManager->move($storage); $photos = $this->database->select('photo', ['backend-ref', 'backend-class', 'id', 'data']); @@ -346,8 +399,15 @@ class StorageManagerTest extends DatabaseTest $this->expectException(InvalidClassStorageException::class); $this->expectExceptionMessage('Backend SystemResource is not valid'); - $storageManager = new StorageManager($this->database, $this->config, $this->logger, $this->l10n, false); - $storage = $storageManager->getWritableStorageByName(SystemResource::getName()); + $storageManager = new StorageManager( + $this->database, + $this->config, + new NullLogger(), + new FakeEventDispatcher(), + $this->l10n, + false + ); + $storage = $storageManager->getWritableStorageByName(SystemResource::getName()); $storageManager->move($storage); } } diff --git a/tests/src/Core/Storage/StorageConfigTest.php b/tests/src/Core/Storage/StorageConfigTest.php deleted file mode 100644 index 7da0b09e83..0000000000 --- a/tests/src/Core/Storage/StorageConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Core\Storage; - -use Friendica\Core\Storage\Capability\ICanConfigureStorage; -use Friendica\Test\MockedTest; - -abstract class StorageConfigTest extends MockedTest -{ - /** @return ICanConfigureStorage */ - abstract protected function getInstance(); - - abstract protected function assertOption(ICanConfigureStorage $storage); - - /** - * Test if the "getOption" is asserted - */ - public function testGetOptions() - { - $instance = $this->getInstance(); - - $this->assertOption($instance); - } -} diff --git a/tests/src/Core/SystemTest.php b/tests/src/Core/SystemTest.php index c47539c79f..e0359db6a2 100644 --- a/tests/src/Core/SystemTest.php +++ b/tests/src/Core/SystemTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core; diff --git a/tests/src/Core/Worker/Repository/ProcessTest.php b/tests/src/Core/Worker/Repository/ProcessTest.php index f39851f64a..eb31556e74 100644 --- a/tests/src/Core/Worker/Repository/ProcessTest.php +++ b/tests/src/Core/Worker/Repository/ProcessTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Core\Worker\Repository; use Friendica\Core\Worker\Factory; use Friendica\Core\Worker\Repository; use Friendica\DI; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Psr\Log\NullLogger; -class ProcessTest extends FixtureTest +class ProcessTest extends FixtureTestCase { public function testStandardProcess() { diff --git a/tests/src/Database/DBATest.php b/tests/src/Database/DBATest.php index 0c3c5e1604..49d3a6a5e6 100644 --- a/tests/src/Database/DBATest.php +++ b/tests/src/Database/DBATest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Database; @@ -25,10 +11,10 @@ use Dice\Dice; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Test\DatabaseTest; +use Friendica\Test\DatabaseTestCase; use Friendica\Test\Util\Database\StaticDatabase; -class DBATest extends DatabaseTest +class DBATest extends DatabaseTestCase { protected function setUp(): void { diff --git a/tests/src/Database/DBStructureTest.php b/tests/src/Database/DBStructureTest.php index 7bb9baf948..9ce3475ee7 100644 --- a/tests/src/Database/DBStructureTest.php +++ b/tests/src/Database/DBStructureTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Database; @@ -25,10 +11,10 @@ use Dice\Dice; use Friendica\Database\Database; use Friendica\Database\DBStructure; use Friendica\DI; -use Friendica\Test\DatabaseTest; +use Friendica\Test\DatabaseTestCase; use Friendica\Test\Util\Database\StaticDatabase; -class DBStructureTest extends DatabaseTest +class DBStructureTest extends DatabaseTestCase { protected function setUp(): void { diff --git a/tests/src/Database/DatabaseTest.php b/tests/src/Database/DatabaseTest.php index c431a5294d..c9b6dd9528 100644 --- a/tests/src/Database/DatabaseTest.php +++ b/tests/src/Database/DatabaseTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Database; use Friendica\Core\Config\Util\ConfigFileManager; use Friendica\Core\Config\ValueObject\Cache; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Friendica\Test\Util\CreateDatabaseTrait; -class DatabaseTest extends FixtureTest +class DatabaseTest extends FixtureTestCase { use CreateDatabaseTrait; @@ -46,7 +32,12 @@ class DatabaseTest extends FixtureTest parent::setUp(); $this->configCache = new Cache(); - $this->configFileManager = new ConfigFileManager($this->root->url(), $this->root->url() . '/config/', $this->root->url() . '/static/'); + $this->configFileManager = new ConfigFileManager( + $this->root->url(), + $this->root->url() . '/addon', + $this->root->url() . '/config', + $this->root->url() . '/static' + ); } /** @@ -101,7 +92,7 @@ class DatabaseTest extends FixtureTest self::assertTrue($db->update('gserver', ['active-week-users' => 0, 'registered-users' => 0], ['nurl' => 'http://friendica.local'])); - $fields = ["`registered-users` = `registered-users` + 1"]; + $fields = ["`registered-users` = `registered-users` + 1"]; $fields[] = "`active-week-users` = `active-week-users` + 2"; self::assertTrue($db->update('gserver', $fields, ['nurl' => 'http://friendica.local'])); diff --git a/tests/src/Factory/Api/Mastodon/EmojiTest.php b/tests/src/Factory/Api/Mastodon/EmojiTest.php index da67ea1639..24dba047e6 100644 --- a/tests/src/Factory/Api/Mastodon/EmojiTest.php +++ b/tests/src/Factory/Api/Mastodon/EmojiTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Factory\Api\Mastodon; use Friendica\Content\Smilies; use Friendica\DI; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class EmojiTest extends FixtureTest +class EmojiTest extends FixtureTestCase { protected function setUp(): void { diff --git a/tests/src/Factory/Api/Mastodon/StatusTest.php b/tests/src/Factory/Api/Mastodon/StatusTest.php index df702fac85..030c1605d4 100644 --- a/tests/src/Factory/Api/Mastodon/StatusTest.php +++ b/tests/src/Factory/Api/Mastodon/StatusTest.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Factory\Api\Mastodon; use Friendica\Core\Hook; +use Friendica\Core\Hooks\HookEventBridge; use Friendica\DI; use Friendica\Model\Post; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class StatusTest extends FixtureTest +class StatusTest extends FixtureTestCase { protected $status; @@ -35,13 +22,21 @@ class StatusTest extends FixtureTest parent::setUp(); DI::config()->set('system', 'no_smilies', false); + $this->status = DI::mstdnStatus(); + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = DI::eventDispatcher(); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } + Hook::register('smilie', 'tests/Util/SmileyWhitespaceAddon.php', 'add_test_unicode_smilies'); Hook::loadHooks(); } - public function testSimpleStatus() + public function testSimpleStatus(): void { $post = Post::selectFirst([], ['id' => 13]); $this->assertNotNull($post); @@ -49,7 +44,7 @@ class StatusTest extends FixtureTest $this->assertNotNull($result); } - public function testSimpleEmojiStatus() + public function testSimpleEmojiStatus(): void { $post = Post::selectFirst([], ['id' => 14]); $this->assertNotNull($post); diff --git a/tests/src/Factory/Api/Twitter/ActivitiesTest.php b/tests/src/Factory/Api/Twitter/ActivitiesTest.php index f76ced98fd..a00f1d7e94 100644 --- a/tests/src/Factory/Api/Twitter/ActivitiesTest.php +++ b/tests/src/Factory/Api/Twitter/ActivitiesTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Factory\Api\Twitter; use Friendica\DI; use Friendica\Factory\Api\Friendica\Activities; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class ActivitiesTest extends FixtureTest +class ActivitiesTest extends FixtureTestCase { /** * Test the api_format_items_activities() function. diff --git a/tests/src/Factory/Api/Twitter/DirectMessageTest.php b/tests/src/Factory/Api/Twitter/DirectMessageTest.php index 216c4051fa..2f73faf353 100644 --- a/tests/src/Factory/Api/Twitter/DirectMessageTest.php +++ b/tests/src/Factory/Api/Twitter/DirectMessageTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Factory\Api\Twitter; use Friendica\DI; use Friendica\Factory\Api\Twitter\DirectMessage; -use Friendica\Test\FixtureTest; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; +use Friendica\Test\FixtureTestCase; -class DirectMessageTest extends FixtureTest +class DirectMessageTest extends FixtureTestCase { /** * Test the api_format_messages() function. @@ -40,7 +26,7 @@ class DirectMessageTest extends FixtureTest $id = $ids[0]['id']; $directMessage = (new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser())) - ->createFromMailId($id, ApiTest::SELF_USER['id']) + ->createFromMailId($id, ApiTestCase::SELF_USER['id']) ->toArray(); self::assertEquals('item_title' . "\n" . 'item_body', $directMessage['text']); @@ -63,11 +49,11 @@ class DirectMessageTest extends FixtureTest $id = $ids[0]['id']; $directMessage = (new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser())) - ->createFromMailId($id, ApiTest::SELF_USER['id'], 'html') + ->createFromMailId($id, ApiTestCase::SELF_USER['id'], 'html') ->toArray(); self::assertEquals('item_title', $directMessage['title']); - self::assertEquals('item_body', $directMessage['text']); + self::assertEquals('item_body', $directMessage['text']); } /** @@ -82,7 +68,7 @@ class DirectMessageTest extends FixtureTest $id = $ids[0]['id']; $directMessage = (new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser())) - ->createFromMailId($id, ApiTest::SELF_USER['id'], 'plain') + ->createFromMailId($id, ApiTestCase::SELF_USER['id'], 'plain') ->toArray(); self::assertEquals('item_title', $directMessage['title']); @@ -104,7 +90,7 @@ class DirectMessageTest extends FixtureTest $id = $ids[0]['id']; $directMessage = (new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser())) - ->createFromMailId($id, ApiTest::SELF_USER['id'], 'plain', $$GETUSEROBJECTS$$) + ->createFromMailId($id, ApiTestCase::SELF_USER['id'], 'plain', $$GETUSEROBJECTS$$) ->toArray(); self::assertTrue(!isset($directMessage['sender'])); diff --git a/tests/src/Factory/Api/Twitter/StatusTest.php b/tests/src/Factory/Api/Twitter/StatusTest.php index e79e545bfd..5a00d5177f 100644 --- a/tests/src/Factory/Api/Twitter/StatusTest.php +++ b/tests/src/Factory/Api/Twitter/StatusTest.php @@ -1,26 +1,13 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Factory\Api\Twitter; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Factory\Api\Friendica\Activities; use Friendica\Factory\Api\Twitter\Attachment; @@ -29,10 +16,10 @@ use Friendica\Factory\Api\Twitter\Media; use Friendica\Factory\Api\Twitter\Mention; use Friendica\Factory\Api\Twitter\Status; use Friendica\Factory\Api\Twitter\Url; -use Friendica\Test\FixtureTest; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; +use Friendica\Test\FixtureTestCase; -class StatusTest extends FixtureTest +class StatusTest extends FixtureTestCase { protected $statusFactory; @@ -60,7 +47,7 @@ class StatusTest extends FixtureTest public function testApiConvertItem() { $status = $this->statusFactory - ->createFromItemId(13, ApiTest::SELF_USER['id']) + ->createFromItemId(13, ApiTestCase::SELF_USER['id']) ->toArray(); self::assertStringStartsWith('item_title', $status['text']); @@ -121,7 +108,7 @@ class StatusTest extends FixtureTest public function testApiGetEntitiesWithIncludeEntities() { $status = $this->statusFactory - ->createFromItemId(13, ApiTest::SELF_USER['id'], true) + ->createFromItemId(13, ApiTestCase::SELF_USER['id'], true) ->toArray(); self::assertIsArray($status['entities']); @@ -137,10 +124,13 @@ class StatusTest extends FixtureTest */ public function testApiFormatItems() { + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + $posts = DI::dba()->selectToArray('post-view', ['uri-id']); foreach ($posts as $item) { $status = $this->statusFactory - ->createFromUriId($item['uri-id'], ApiTest::SELF_USER['id']) + ->createFromUriId($item['uri-id'], ApiTestCase::SELF_USER['id']) ->toArray(); self::assertIsInt($status['id']); diff --git a/tests/src/Factory/Api/Twitter/UserTest.php b/tests/src/Factory/Api/Twitter/UserTest.php index efc87b4912..7e44a20a0f 100644 --- a/tests/src/Factory/Api/Twitter/UserTest.php +++ b/tests/src/Factory/Api/Twitter/UserTest.php @@ -1,32 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Factory\Api\Twitter; use Friendica\DI; use Friendica\Factory\Api\Twitter\User; -use Friendica\Test\FixtureTest; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Network\HTTPException\NotFoundException; +use Friendica\Test\ApiTestCase; +use Friendica\Test\FixtureTestCase; -class UserTest extends FixtureTest +class UserTest extends FixtureTestCase { /** * Assert that an user array contains expected keys. @@ -35,11 +22,11 @@ class UserTest extends FixtureTest */ protected function assertSelfUser(array $user) { - self::assertEquals(ApiTest::SELF_USER['id'], $user['uid']); - self::assertEquals(ApiTest::SELF_USER['id'], $user['cid']); + self::assertEquals(ApiTestCase::SELF_USER['id'], $user['uid']); + self::assertEquals(ApiTestCase::SELF_USER['id'], $user['cid']); self::assertEquals('DFRN', $user['location']); - self::assertEquals(ApiTest::SELF_USER['name'], $user['name']); - self::assertEquals(ApiTest::SELF_USER['nick'], $user['screen_name']); + self::assertEquals(ApiTestCase::SELF_USER['name'], $user['name']); + self::assertEquals(ApiTestCase::SELF_USER['nick'], $user['screen_name']); self::assertTrue($user['verified']); } @@ -51,7 +38,7 @@ class UserTest extends FixtureTest public function testApiGetUser() { $user = (new User(DI::logger(), DI::twitterStatus())) - ->createFromUserId(ApiTest::SELF_USER['id']) + ->createFromUserId(ApiTestCase::SELF_USER['id']) ->toArray(); $this->assertSelfUser($user); @@ -67,7 +54,7 @@ class UserTest extends FixtureTest $this->markTestIncomplete('Needs missing fields for profile colors at API User object first.'); /* - DI::pConfig()->set(ApiTest::SELF_USER['id'], 'frio', 'schema', 'red'); + DI::pConfig()->set(ApiTestCase::SELF_USER['id'], 'frio', 'schema', 'red'); $userFactory = new User(DI::logger(), DI::twitterStatus()); $user = $userFactory->createFromUserId(42); @@ -89,7 +76,7 @@ class UserTest extends FixtureTest $this->markTestIncomplete('Needs missing fields for profile colors at API User object first.'); /* - DI::pConfig()->set(ApiTest::SELF_USER['id'], 'frio', 'schema', '---'); + DI::pConfig()->set(ApiTestCase::SELF_USER['id'], 'frio', 'schema', '---'); $userFactory = new User(DI::logger(), DI::twitterStatus()); $user = $userFactory->createFromUserId(42); @@ -111,10 +98,10 @@ class UserTest extends FixtureTest $this->markTestIncomplete('Needs missing fields for profile colors at API User object first.'); /* - DI::pConfig()->set(ApiTest::SELF_USER['id'], 'frio', 'schema', '---'); - DI::pConfig()->set(ApiTest::SELF_USER['id'], 'frio', 'nav_bg', '#123456'); - DI::pConfig()->set(ApiTest::SELF_USER['id'], 'frio', 'link_color', '#123456'); - DI::pConfig()->set(ApiTest::SELF_USER['id'], 'frio', 'background_color', '#123456'); + DI::pConfig()->set(ApiTestCase::SELF_USER['id'], 'frio', 'schema', '---'); + DI::pConfig()->set(ApiTestCase::SELF_USER['id'], 'frio', 'nav_bg', '#123456'); + DI::pConfig()->set(ApiTestCase::SELF_USER['id'], 'frio', 'link_color', '#123456'); + DI::pConfig()->set(ApiTestCase::SELF_USER['id'], 'frio', 'background_color', '#123456'); $userFactory = new User(DI::logger(), DI::twitterStatus()); $user = $userFactory->createFromUserId(42); @@ -133,15 +120,11 @@ class UserTest extends FixtureTest */ public function testApiGetUserWithWrongGetId() { + $this->expectException(NotFoundException::class); + $user = (new User(DI::logger(), DI::twitterStatus())) ->createFromUserId(-1) ->toArray(); - - self::assertEquals(0, $user['id']); - self::assertEquals(0, $user['uid']); - self::assertEquals(0, $user['cid']); - self::assertEquals(0, $user['pid']); - self::assertEmpty($user['name']); } /** diff --git a/tests/src/Model/FileTagTest.php b/tests/src/Model/FileTagTest.php index 47eb15e3ce..63464c8d52 100644 --- a/tests/src/Model/FileTagTest.php +++ b/tests/src/Model/FileTagTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model; diff --git a/tests/src/Model/GServerTest.php b/tests/src/Model/GServerTest.php index a56f4ed6ff..6e4dff8c42 100644 --- a/tests/src/Model/GServerTest.php +++ b/tests/src/Model/GServerTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model; diff --git a/tests/src/Model/Log/ParsedLogIteratorTest.php b/tests/src/Model/Log/ParsedLogIteratorTest.php index eb650a9dc0..49f92d69d6 100644 --- a/tests/src/Model/Log/ParsedLogIteratorTest.php +++ b/tests/src/Model/Log/ParsedLogIteratorTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model\Log; diff --git a/tests/src/Model/Post/MediaTest.php b/tests/src/Model/Post/MediaTest.php index ee6d89ad3c..d063399b13 100644 --- a/tests/src/Model/Post/MediaTest.php +++ b/tests/src/Model/Post/MediaTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model\Post; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class MediaTest extends MockedTest +class MediaTest extends MockedTestCase { /** * Test the api_get_attachments() function. diff --git a/tests/src/Model/TagTest.php b/tests/src/Model/TagTest.php index 62003734b1..ab8cb3dfd6 100644 --- a/tests/src/Model/TagTest.php +++ b/tests/src/Model/TagTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model; diff --git a/tests/src/Model/User/CookieTest.php b/tests/src/Model/User/CookieTest.php index ebe78764ba..3c56a89668 100644 --- a/tests/src/Model/User/CookieTest.php +++ b/tests/src/Model/User/CookieTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model\User; @@ -25,11 +11,11 @@ use Friendica\App\BaseURL; use Friendica\App\Request; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Model\User\Cookie; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\StaticCookie; use Mockery\MockInterface; -class CookieTest extends MockedTest +class CookieTest extends MockedTestCase { /** @var MockInterface|IManageConfigValues */ private $config; diff --git a/tests/src/Model/UserTest.php b/tests/src/Model/UserTest.php index 89c3b9d230..f58f34995f 100644 --- a/tests/src/Model/UserTest.php +++ b/tests/src/Model/UserTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Model; @@ -25,10 +11,10 @@ use Dice\Dice; use Friendica\Database\Database; use Friendica\DI; use Friendica\Model\User; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Mockery\MockInterface; -class UserTest extends MockedTest +class UserTest extends MockedTestCase { private $parent; private $child; diff --git a/tests/src/Moderation/Factory/ReportTest.php b/tests/src/Moderation/Factory/ReportTest.php index 35e3a50506..d488648e92 100644 --- a/tests/src/Moderation/Factory/ReportTest.php +++ b/tests/src/Moderation/Factory/ReportTest.php @@ -1,36 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Moderation\Factory; use Friendica\Moderation\Collection; use Friendica\Moderation\Factory; use Friendica\Moderation\Entity; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\Clock\FrozenClock; use Friendica\Util\DateTimeFormat; use Psr\Clock\ClockInterface; use Psr\Log\NullLogger; -class ReportTest extends MockedTest +class ReportTest extends MockedTestCase { public function dataCreateFromTableRow(): array { diff --git a/tests/src/Module/Api/ApiResponseTest.php b/tests/src/Module/Api/ApiResponseTest.php index c92b9ae235..23cc4bf702 100644 --- a/tests/src/Module/Api/ApiResponseTest.php +++ b/tests/src/Module/Api/ApiResponseTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api; @@ -26,10 +12,10 @@ use Friendica\App\BaseURL; use Friendica\Core\L10n; use Friendica\Factory\Api\Twitter\User; use Friendica\Module\Api\ApiResponse; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Psr\Log\NullLogger; -class ApiResponseTest extends MockedTest +class ApiResponseTest extends MockedTestCase { public function testErrorWithJson() { diff --git a/tests/src/Module/Api/Friendica/DirectMessages/SearchTest.php b/tests/src/Module/Api/Friendica/DirectMessages/SearchTest.php index e17afb9be3..27ea3e327d 100644 --- a/tests/src/Module/Api/Friendica/DirectMessages/SearchTest.php +++ b/tests/src/Module/Api/Friendica/DirectMessages/SearchTest.php @@ -1,40 +1,25 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Friendica\DirectMessages; -use Friendica\App\Router; use Friendica\DI; use Friendica\Factory\Api\Twitter\DirectMessage; use Friendica\Module\Api\Friendica\DirectMessages\Search; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; use Psr\Log\NullLogger; -class SearchTest extends ApiTest +class SearchTest extends ApiTestCase { public function testEmpty() { $directMessage = new DirectMessage(new NullLogger(), DI::dba(), DI::twitterUser()); - $response = (new Search($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Search($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); @@ -52,7 +37,7 @@ class SearchTest extends ApiTest $directMessage = new DirectMessage(new NullLogger(), DI::dba(), DI::twitterUser()); - $response = (new Search($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Search($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'searchstring' => 'item_body' ]); @@ -73,7 +58,7 @@ class SearchTest extends ApiTest { $directMessage = new DirectMessage(new NullLogger(), DI::dba(), DI::twitterUser()); - $response = (new Search($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Search($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'searchstring' => 'test' ]); diff --git a/tests/src/Module/Api/Friendica/NotificationTest.php b/tests/src/Module/Api/Friendica/NotificationTest.php index 06191e3d32..c31b43d55d 100644 --- a/tests/src/Module/Api/Friendica/NotificationTest.php +++ b/tests/src/Module/Api/Friendica/NotificationTest.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Friendica; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\Friendica\Notification; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; use Friendica\Util\DateTimeFormat; use Friendica\Util\Temporal; -class NotificationTest extends ApiTest +class NotificationTest extends ApiTestCase { public function testEmpty() { @@ -62,11 +48,11 @@ class NotificationTest extends ApiTest $assertXml = << - + XML; - $response = (new Notification(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'xml'])) + $response = (new Notification(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'xml'])) ->run($this->httpExceptionMock); self::assertXmlStringEqualsXmlString($assertXml, (string)$response->getBody()); @@ -78,7 +64,7 @@ XML; public function testWithJsonResult() { - $response = (new Notification(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Notification(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Friendica/Photo/DeleteTest.php b/tests/src/Module/Api/Friendica/Photo/DeleteTest.php index 7e30095598..19d440cd70 100644 --- a/tests/src/Module/Api/Friendica/Photo/DeleteTest.php +++ b/tests/src/Module/Api/Friendica/Photo/DeleteTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Friendica\Photo; @@ -25,9 +11,9 @@ use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Friendica\Photo\Delete; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class DeleteTest extends ApiTest +class DeleteTest extends ApiTestCase { protected function setUp(): void { @@ -39,7 +25,7 @@ class DeleteTest extends ApiTest public function testEmpty() { $this->expectException(BadRequestException::class); - (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), []))->run($this->httpExceptionMock); + (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), []))->run($this->httpExceptionMock); } public function testWithoutAuthenticatedUser() @@ -50,14 +36,14 @@ class DeleteTest extends ApiTest public function testWrong() { $this->expectException(BadRequestException::class); - (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), []))->run($this->httpExceptionMock, ['photo_id' => 1]); + (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), []))->run($this->httpExceptionMock, ['photo_id' => 1]); } public function testValidWithPost() { $this->loadFixture(__DIR__ . '/../../../../../datasets/photo/photo.fixture.php', DI::dba()); - $response = (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'photo_id' => '709057080661a283a6aa598501504178' ]); @@ -72,7 +58,7 @@ class DeleteTest extends ApiTest { $this->loadFixture(__DIR__ . '/../../../../../datasets/photo/photo.fixture.php', DI::dba()); - $response = (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'photo_id' => '709057080661a283a6aa598501504178' ]); diff --git a/tests/src/Module/Api/Friendica/Photoalbum/DeleteTest.php b/tests/src/Module/Api/Friendica/Photoalbum/DeleteTest.php index 7d483f1fd0..727ba63a2b 100644 --- a/tests/src/Module/Api/Friendica/Photoalbum/DeleteTest.php +++ b/tests/src/Module/Api/Friendica/Photoalbum/DeleteTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Friendica\Photoalbum; @@ -25,9 +11,9 @@ use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Friendica\Photoalbum\Delete; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class DeleteTest extends ApiTest +class DeleteTest extends ApiTestCase { protected function setUp(): void { @@ -39,7 +25,7 @@ class DeleteTest extends ApiTest public function testEmpty() { $this->expectException(BadRequestException::class); - (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -47,7 +33,7 @@ class DeleteTest extends ApiTest public function testWrong() { $this->expectException(BadRequestException::class); - (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'album' => 'album_name' ]); @@ -57,7 +43,7 @@ class DeleteTest extends ApiTest { $this->loadFixture(__DIR__ . '/../../../../../datasets/photo/photo.fixture.php', DI::dba()); - $response = (new Delete(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Delete(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'album' => 'test_album'] ); diff --git a/tests/src/Module/Api/Friendica/Photoalbum/UpdateTest.php b/tests/src/Module/Api/Friendica/Photoalbum/UpdateTest.php index 4d8c7ff7fa..6ac54c392b 100644 --- a/tests/src/Module/Api/Friendica/Photoalbum/UpdateTest.php +++ b/tests/src/Module/Api/Friendica/Photoalbum/UpdateTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Friendica\Photoalbum; @@ -25,9 +11,9 @@ use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Friendica\Photoalbum\Update; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class UpdateTest extends ApiTest +class UpdateTest extends ApiTestCase { protected function setUp(): void { @@ -39,14 +25,14 @@ class UpdateTest extends ApiTest public function testEmpty() { $this->expectException(BadRequestException::class); - (new Update(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Update(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } public function testTooFewArgs() { $this->expectException(BadRequestException::class); - (new Update(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Update(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'album' => 'album_name' ]); @@ -55,7 +41,7 @@ class UpdateTest extends ApiTest public function testWrongUpdate() { $this->expectException(BadRequestException::class); - (new Update(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Update(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'album' => 'album_name', 'album_new' => 'album_name' @@ -71,7 +57,7 @@ class UpdateTest extends ApiTest { $this->loadFixture(__DIR__ . '/../../../../../datasets/photo/photo.fixture.php', DI::dba()); - $response = (new Update(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Update(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'album' => 'test_album', 'album_new' => 'test_album_2' diff --git a/tests/src/Module/Api/GnuSocial/GnuSocial/ConfigTest.php b/tests/src/Module/Api/GnuSocial/GnuSocial/ConfigTest.php index db16070065..c38b045171 100644 --- a/tests/src/Module/Api/GnuSocial/GnuSocial/ConfigTest.php +++ b/tests/src/Module/Api/GnuSocial/GnuSocial/ConfigTest.php @@ -1,38 +1,24 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\GnuSocial\GnuSocial; use Friendica\DI; use Friendica\Module\Api\GNUSocial\GNUSocial\Config; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ConfigTest extends ApiTest +class ConfigTest extends ApiTestCase { /** * Test the api_statusnet_config() function. */ public function testApiStatusnetConfig() { - $response = (new Config(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Config(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/GnuSocial/GnuSocial/VersionTest.php b/tests/src/Module/Api/GnuSocial/GnuSocial/VersionTest.php index 11fa444efb..2f8f8c0508 100644 --- a/tests/src/Module/Api/GnuSocial/GnuSocial/VersionTest.php +++ b/tests/src/Module/Api/GnuSocial/GnuSocial/VersionTest.php @@ -1,36 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\GnuSocial\GnuSocial; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\GNUSocial\GNUSocial\Version; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class VersionTest extends ApiTest +class VersionTest extends ApiTestCase { public function test() { - $response = (new Version(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Version(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); self::assertEquals([ diff --git a/tests/src/Module/Api/GnuSocial/Help/TestTest.php b/tests/src/Module/Api/GnuSocial/Help/TestTest.php index 452211bb0d..b1bbd68aa0 100644 --- a/tests/src/Module/Api/GnuSocial/Help/TestTest.php +++ b/tests/src/Module/Api/GnuSocial/Help/TestTest.php @@ -1,36 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\GnuSocial\Help; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\GNUSocial\Help\Test; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class TestTest extends ApiTest +class TestTest extends ApiTestCase { public function testJson() { - $response = (new Test(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Test(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); $json = $this->toJson($response); @@ -44,7 +30,7 @@ class TestTest extends ApiTest public function testXml() { - $response = (new Test(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'xml'])) + $response = (new Test(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'xml'])) ->run($this->httpExceptionMock); self::assertEquals([ diff --git a/tests/src/Module/Api/Mastodon/Accounts/StatusesTest.php b/tests/src/Module/Api/Mastodon/Accounts/StatusesTest.php index 0523d04c9e..401c1a4b8f 100644 --- a/tests/src/Module/Api/Mastodon/Accounts/StatusesTest.php +++ b/tests/src/Module/Api/Mastodon/Accounts/StatusesTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon\Accounts; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class StatusesTest extends ApiTest +class StatusesTest extends ApiTestCase { /** * Test the api_status_show() function. diff --git a/tests/src/Module/Api/Mastodon/Accounts/VerifyCredentialsTest.php b/tests/src/Module/Api/Mastodon/Accounts/VerifyCredentialsTest.php index 06a3b8614b..10702b8e4a 100644 --- a/tests/src/Module/Api/Mastodon/Accounts/VerifyCredentialsTest.php +++ b/tests/src/Module/Api/Mastodon/Accounts/VerifyCredentialsTest.php @@ -1,32 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon\Accounts; -use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Mastodon\Accounts\VerifyCredentials; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class VerifyCredentialsTest extends ApiTest +class VerifyCredentialsTest extends ApiTestCase { /** * Test the api_account_verify_credentials() function. @@ -35,7 +20,7 @@ class VerifyCredentialsTest extends ApiTest */ public function testApiAccountVerifyCredentials() { - $response = (new VerifyCredentials(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new VerifyCredentials(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Mastodon/ConversationsTest.php b/tests/src/Module/Api/Mastodon/ConversationsTest.php index 7b85ebb6d9..38736b64f8 100644 --- a/tests/src/Module/Api/Mastodon/ConversationsTest.php +++ b/tests/src/Module/Api/Mastodon/ConversationsTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ConversationsTest extends ApiTest +class ConversationsTest extends ApiTestCase { /** * Test the api_conversation_show() function. diff --git a/tests/src/Module/Api/Mastodon/PushSubscriptionTest.php b/tests/src/Module/Api/Mastodon/PushSubscriptionTest.php index e7c968ea28..489bdd7b4a 100644 --- a/tests/src/Module/Api/Mastodon/PushSubscriptionTest.php +++ b/tests/src/Module/Api/Mastodon/PushSubscriptionTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class PushSubscriptionTest extends ApiTest +class PushSubscriptionTest extends ApiTestCase { /** * Test the api_account_verify_credentials() function. @@ -36,7 +22,7 @@ class PushSubscriptionTest extends ApiTest // $this->useHttpMethod(Router::POST); // - // $response = (new PushSubscription(DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), DI::mstdnSubscription(), DI::mstdnError(), [])) + // $response = (new PushSubscription(DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), DI::mstdnSubscription(), DI::mstdnError(), [])) // ->run(); // // $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Mastodon/SearchTest.php b/tests/src/Module/Api/Mastodon/SearchTest.php index 1a625d9a15..5b04cf01af 100644 --- a/tests/src/Module/Api/Mastodon/SearchTest.php +++ b/tests/src/Module/Api/Mastodon/SearchTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class SearchTest extends ApiTest +class SearchTest extends ApiTestCase { /** * Test the api_search() function. diff --git a/tests/src/Module/Api/Mastodon/Timelines/HomeTest.php b/tests/src/Module/Api/Mastodon/Timelines/HomeTest.php index b58bf380db..b5bbd7bccc 100644 --- a/tests/src/Module/Api/Mastodon/Timelines/HomeTest.php +++ b/tests/src/Module/Api/Mastodon/Timelines/HomeTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon\Timelines; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class HomeTest extends ApiTest +class HomeTest extends ApiTestCase { /** * Test the api_statuses_home_timeline() function. diff --git a/tests/src/Module/Api/Mastodon/Timelines/PublicTimelineTest.php b/tests/src/Module/Api/Mastodon/Timelines/PublicTimelineTest.php index 1652982ef1..017d1fb62a 100644 --- a/tests/src/Module/Api/Mastodon/Timelines/PublicTimelineTest.php +++ b/tests/src/Module/Api/Mastodon/Timelines/PublicTimelineTest.php @@ -1,29 +1,15 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Mastodon\Timelines; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class PublicTimelineTest extends ApiTest +class PublicTimelineTest extends ApiTestCase { /** * Test the api_statuses_public_timeline() function. diff --git a/tests/src/Module/Api/Twitter/Account/RateLimitStatusTest.php b/tests/src/Module/Api/Twitter/Account/RateLimitStatusTest.php index 2a4cc0e406..36b4162c4a 100644 --- a/tests/src/Module/Api/Twitter/Account/RateLimitStatusTest.php +++ b/tests/src/Module/Api/Twitter/Account/RateLimitStatusTest.php @@ -1,37 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Account; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\Twitter\Account\RateLimitStatus; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class RateLimitStatusTest extends ApiTest +class RateLimitStatusTest extends ApiTestCase { public function testWithJson() { - $response = (new RateLimitStatus(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new RateLimitStatus(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); $result = $this->toJson($response); @@ -47,7 +32,7 @@ class RateLimitStatusTest extends ApiTest public function testWithXml() { - $response = (new RateLimitStatus(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'xml'])) + $response = (new RateLimitStatus(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'xml'])) ->run($this->httpExceptionMock); self::assertEquals([ diff --git a/tests/src/Module/Api/Twitter/Account/UpdateProfileTest.php b/tests/src/Module/Api/Twitter/Account/UpdateProfileTest.php index 71817022dc..ccfcb8075f 100644 --- a/tests/src/Module/Api/Twitter/Account/UpdateProfileTest.php +++ b/tests/src/Module/Api/Twitter/Account/UpdateProfileTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Account; use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Account\UpdateProfile; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class UpdateProfileTest extends ApiTest +class UpdateProfileTest extends ApiTestCase { /** * Test the api_account_update_profile() function. @@ -35,7 +21,7 @@ class UpdateProfileTest extends ApiTest { $this->useHttpMethod(Router::POST); - $response = (new UpdateProfile(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new UpdateProfile(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'name' => 'new_name', 'description' => 'new_description' diff --git a/tests/src/Module/Api/Twitter/Blocks/ListsTest.php b/tests/src/Module/Api/Twitter/Blocks/ListsTest.php index 2476315a07..d98e490303 100644 --- a/tests/src/Module/Api/Twitter/Blocks/ListsTest.php +++ b/tests/src/Module/Api/Twitter/Blocks/ListsTest.php @@ -1,39 +1,24 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Blocks; -use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Blocks\Lists; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ListsTest extends ApiTest +class ListsTest extends ApiTestCase { /** * Test the api_statuses_f() function. */ public function testApiStatusesFWithBlocks() { - $response = (new Lists(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Lists(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/ContactEndpointMock.php b/tests/src/Module/Api/Twitter/ContactEndpointMock.php index cf3e0fef1c..88385a48df 100644 --- a/tests/src/Module/Api/Twitter/ContactEndpointMock.php +++ b/tests/src/Module/Api/Twitter/ContactEndpointMock.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter; diff --git a/tests/src/Module/Api/Twitter/ContactEndpointTest.php b/tests/src/Module/Api/Twitter/ContactEndpointTest.php index 15f56e97ce..bdb5b96d3c 100644 --- a/tests/src/Module/Api/Twitter/ContactEndpointTest.php +++ b/tests/src/Module/Api/Twitter/ContactEndpointTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter; @@ -26,9 +12,9 @@ use Friendica\Module\Api\Twitter\ContactEndpoint; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\NotFoundException; use Friendica\Object\Api\Twitter\User; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class ContactEndpointTest extends FixtureTest +class ContactEndpointTest extends FixtureTestCase { public function testIds() { diff --git a/tests/src/Module/Api/Twitter/DirectMessages/AllTest.php b/tests/src/Module/Api/Twitter/DirectMessages/AllTest.php index c04097cd8b..8f180ff306 100644 --- a/tests/src/Module/Api/Twitter/DirectMessages/AllTest.php +++ b/tests/src/Module/Api/Twitter/DirectMessages/AllTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\DirectMessages; use Friendica\DI; use Friendica\Module\Api\Twitter\DirectMessages\All; use Friendica\Factory\Api\Twitter\DirectMessage; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class AllTest extends ApiTest +class AllTest extends ApiTestCase { /** * Test the api_direct_messages_box() function. @@ -39,7 +25,7 @@ class AllTest extends ApiTest $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new All($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new All($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/DirectMessages/ConversationTest.php b/tests/src/Module/Api/Twitter/DirectMessages/ConversationTest.php index 0dc9ba8e8f..1681ef0e51 100644 --- a/tests/src/Module/Api/Twitter/DirectMessages/ConversationTest.php +++ b/tests/src/Module/Api/Twitter/DirectMessages/ConversationTest.php @@ -1,33 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\DirectMessages; -use Friendica\App\Router; use Friendica\DI; use Friendica\Factory\Api\Twitter\DirectMessage; use Friendica\Module\Api\Twitter\DirectMessages\Conversation; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ConversationTest extends ApiTest +class ConversationTest extends ApiTestCase { /** * Test the api_direct_messages_box() function. @@ -38,7 +23,7 @@ class ConversationTest extends ApiTest { $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new Conversation($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Conversation($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'friendica_verbose' => true, ]); diff --git a/tests/src/Module/Api/Twitter/DirectMessages/DestroyTest.php b/tests/src/Module/Api/Twitter/DirectMessages/DestroyTest.php index d699fb7131..03a232ecbc 100644 --- a/tests/src/Module/Api/Twitter/DirectMessages/DestroyTest.php +++ b/tests/src/Module/Api/Twitter/DirectMessages/DestroyTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\DirectMessages; @@ -25,10 +11,17 @@ use Friendica\App\Router; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Module\Api\Twitter\DirectMessages\Destroy; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class DestroyTest extends ApiTest +class DestroyTest extends ApiTestCase { + protected function setUp(): void + { + parent::setUp(); + + $this->useHttpMethod(Router::POST); + } + /** * Test the api_direct_messages_destroy() function. * @@ -37,7 +30,8 @@ class DestroyTest extends ApiTest public function testApiDirectMessagesDestroy() { $this->expectException(\Friendica\Network\HTTPException\BadRequestException::class); - (new Destroy(DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + + (new Destroy(DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); } @@ -48,7 +42,7 @@ class DestroyTest extends ApiTest */ public function testApiDirectMessagesDestroyWithVerbose() { - $response = (new Destroy(DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Destroy(DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'friendica_verbose' => true, ]); @@ -84,7 +78,7 @@ class DestroyTest extends ApiTest public function testApiDirectMessagesDestroyWithId() { $this->expectException(\Friendica\Network\HTTPException\BadRequestException::class); - (new Destroy(DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + (new Destroy(DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'id' => 1 ]); @@ -97,7 +91,7 @@ class DestroyTest extends ApiTest */ public function testApiDirectMessagesDestroyWithIdAndVerbose() { - $response = (new Destroy(DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Destroy(DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'id' => 1, 'friendica_parenturi' => 'parent_uri', @@ -121,7 +115,7 @@ class DestroyTest extends ApiTest $ids = DBA::selectToArray('mail', ['id']); $id = $ids[0]['id']; - $response = (new Destroy(DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Destroy(DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'id' => $id, 'friendica_verbose' => true, diff --git a/tests/src/Module/Api/Twitter/DirectMessages/InboxTest.php b/tests/src/Module/Api/Twitter/DirectMessages/InboxTest.php index 6aa9db5a44..efed6f38c4 100644 --- a/tests/src/Module/Api/Twitter/DirectMessages/InboxTest.php +++ b/tests/src/Module/Api/Twitter/DirectMessages/InboxTest.php @@ -1,33 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\DirectMessages; -use Friendica\App\Router; use Friendica\DI; use Friendica\Factory\Api\Twitter\DirectMessage; use Friendica\Module\Api\Twitter\DirectMessages\Inbox; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class InboxTest extends ApiTest +class InboxTest extends ApiTestCase { /** * Test the api_direct_messages_box() function. @@ -40,7 +25,7 @@ class InboxTest extends ApiTest $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new Inbox($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Inbox($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/DirectMessages/NewDMTest.php b/tests/src/Module/Api/Twitter/DirectMessages/NewDMTest.php index e0c9b070d5..d338d42685 100644 --- a/tests/src/Module/Api/Twitter/DirectMessages/NewDMTest.php +++ b/tests/src/Module/Api/Twitter/DirectMessages/NewDMTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\DirectMessages; @@ -25,10 +11,17 @@ use Friendica\App\Router; use Friendica\DI; use Friendica\Factory\Api\Twitter\DirectMessage; use Friendica\Module\Api\Twitter\DirectMessages\NewDM; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class NewDMTest extends ApiTest +class NewDMTest extends ApiTestCase { + protected function setUp(): void + { + parent::setUp(); + + $this->useHttpMethod(Router::POST); + } + /** * Test the api_direct_messages_new() function. * @@ -38,7 +31,7 @@ class NewDMTest extends ApiTest { $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); self::assertEmpty((string)$response->getBody()); @@ -70,7 +63,7 @@ class NewDMTest extends ApiTest { $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'text' => 'message_text', 'user_id' => 43 @@ -92,7 +85,7 @@ class NewDMTest extends ApiTest $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'text' => 'message_text', 'user_id' => 44 @@ -116,7 +109,7 @@ class NewDMTest extends ApiTest $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'text' => 'message_text', 'user_id' => 44, @@ -142,7 +135,7 @@ class NewDMTest extends ApiTest $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'rss'])) + $response = (new NewDM($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'rss'])) ->run($this->httpExceptionMock, [ 'text' => 'message_text', 'user_id' => 44, diff --git a/tests/src/Module/Api/Twitter/DirectMessages/SentTest.php b/tests/src/Module/Api/Twitter/DirectMessages/SentTest.php index a2355bca99..fa82a3645b 100644 --- a/tests/src/Module/Api/Twitter/DirectMessages/SentTest.php +++ b/tests/src/Module/Api/Twitter/DirectMessages/SentTest.php @@ -1,33 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\DirectMessages; -use Friendica\App\Router; use Friendica\DI; use Friendica\Factory\Api\Twitter\DirectMessage; use Friendica\Module\Api\Twitter\DirectMessages\Sent; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class SentTest extends ApiTest +class SentTest extends ApiTestCase { /** * Test the api_direct_messages_box() function. @@ -38,7 +23,7 @@ class SentTest extends ApiTest { $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new Sent($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new Sent($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock, [ 'friendica_verbose' => true, ]); @@ -58,7 +43,7 @@ class SentTest extends ApiTest { $directMessage = new DirectMessage(DI::logger(), DI::dba(), DI::twitterUser()); - $response = (new Sent($directMessage, DI::dba(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'rss'])) + $response = (new Sent($directMessage, DI::dba(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'rss'])) ->run($this->httpExceptionMock); self::assertXml((string)$response->getBody(), 'direct-messages'); diff --git a/tests/src/Module/Api/Twitter/Favorites/CreateTest.php b/tests/src/Module/Api/Twitter/Favorites/CreateTest.php index 74f64e65ea..5e7d43be82 100644 --- a/tests/src/Module/Api/Twitter/Favorites/CreateTest.php +++ b/tests/src/Module/Api/Twitter/Favorites/CreateTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Favorites; @@ -26,9 +12,9 @@ use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\Twitter\Favorites\Create; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class CreateTest extends ApiTest +class CreateTest extends ApiTestCase { protected function setUp(): void { @@ -46,7 +32,7 @@ class CreateTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Create(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Create(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -57,7 +43,7 @@ class CreateTest extends ApiTest */ public function testApiFavoritesCreateDestroyWithCreateAction() { - $response = (new Create(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Create(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 3 ]); @@ -74,7 +60,7 @@ class CreateTest extends ApiTest */ public function testApiFavoritesCreateDestroyWithCreateActionAndRss() { - $response = (new Create(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => ICanCreateResponses::TYPE_RSS])) + $response = (new Create(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => ICanCreateResponses::TYPE_RSS])) ->run($this->httpExceptionMock, [ 'id' => 3 ]); diff --git a/tests/src/Module/Api/Twitter/Favorites/DestroyTest.php b/tests/src/Module/Api/Twitter/Favorites/DestroyTest.php index 94c363abc9..a99f195d2a 100644 --- a/tests/src/Module/Api/Twitter/Favorites/DestroyTest.php +++ b/tests/src/Module/Api/Twitter/Favorites/DestroyTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Favorites; @@ -25,9 +11,9 @@ use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Favorites\Destroy; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class DestroyTest extends ApiTest +class DestroyTest extends ApiTestCase { protected function setUp(): void { @@ -45,7 +31,7 @@ class DestroyTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Destroy(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Destroy(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -56,7 +42,7 @@ class DestroyTest extends ApiTest */ public function testApiFavoritesCreateDestroyWithDestroyAction() { - $response = (new Destroy(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Destroy(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 3 ]); diff --git a/tests/src/Module/Api/Twitter/FavoritesTest.php b/tests/src/Module/Api/Twitter/FavoritesTest.php index 34c344f5fc..7734a6c629 100644 --- a/tests/src/Module/Api/Twitter/FavoritesTest.php +++ b/tests/src/Module/Api/Twitter/FavoritesTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Favorites; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class FavoritesTest extends ApiTest +class FavoritesTest extends ApiTestCase { /** * Test the api_favorites() function. @@ -36,7 +22,10 @@ class FavoritesTest extends ApiTest */ public function testApiFavorites() { - $response = (new Favorites(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Favorites(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'page' => -1, 'max_id' => 10, @@ -56,7 +45,10 @@ class FavoritesTest extends ApiTest */ public function testApiFavoritesWithRss() { - $response = (new Favorites(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Favorites(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ 'extension' => ICanCreateResponses::TYPE_RSS ]))->run($this->httpExceptionMock); diff --git a/tests/src/Module/Api/Twitter/Followers/ListsTest.php b/tests/src/Module/Api/Twitter/Followers/ListsTest.php index e44c27005e..ceb3578253 100644 --- a/tests/src/Module/Api/Twitter/Followers/ListsTest.php +++ b/tests/src/Module/Api/Twitter/Followers/ListsTest.php @@ -1,39 +1,24 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Followers; -use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Followers\Lists; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ListsTest extends ApiTest +class ListsTest extends ApiTestCase { /** * Test the api_statuses_f() function. */ public function testApiStatusesFWithFollowers() { - $response = (new Lists(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Lists(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/Friends/ListsTest.php b/tests/src/Module/Api/Twitter/Friends/ListsTest.php index 0b2f4cd4ee..ca03f08d06 100644 --- a/tests/src/Module/Api/Twitter/Friends/ListsTest.php +++ b/tests/src/Module/Api/Twitter/Friends/ListsTest.php @@ -1,32 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Friends; -use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Friends\Lists; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ListsTest extends ApiTest +class ListsTest extends ApiTestCase { /** * Test the api_statuses_f() function. @@ -35,7 +20,7 @@ class ListsTest extends ApiTest */ public function testApiStatusesFWithFriends() { - $response = (new Lists(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Lists(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/Friendships/IncomingTest.php b/tests/src/Module/Api/Twitter/Friendships/IncomingTest.php index 0b8d069803..8419cd072a 100644 --- a/tests/src/Module/Api/Twitter/Friendships/IncomingTest.php +++ b/tests/src/Module/Api/Twitter/Friendships/IncomingTest.php @@ -1,32 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Friendships; -use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Friendships\Incoming; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class IncomingTest extends ApiTest +class IncomingTest extends ApiTestCase { /** * Test the api_friendships_incoming() function. @@ -35,7 +20,7 @@ class IncomingTest extends ApiTest */ public function testApiFriendshipsIncoming() { - $response = (new Incoming(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Incoming(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/Lists/StatusesTest.php b/tests/src/Module/Api/Twitter/Lists/StatusesTest.php index 8b6b9ed756..add9bbe6d2 100644 --- a/tests/src/Module/Api/Twitter/Lists/StatusesTest.php +++ b/tests/src/Module/Api/Twitter/Lists/StatusesTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Lists; -use Friendica\App\Router; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Lists\Statuses; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class StatusesTest extends ApiTest +class StatusesTest extends ApiTestCase { /** * Test the api_lists_statuses() function. @@ -38,7 +24,7 @@ class StatusesTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Statuses(DI::dba(), DI::twitterStatus(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Statuses(DI::dba(), DI::twitterStatus(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -47,7 +33,10 @@ class StatusesTest extends ApiTest */ public function testApiListsStatusesWithListId() { - $response = (new Statuses(DI::dba(), DI::twitterStatus(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Statuses(DI::dba(), DI::twitterStatus(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'list_id' => 1, 'page' => -1, @@ -67,7 +56,10 @@ class StatusesTest extends ApiTest */ public function testApiListsStatusesWithListIdAndRss() { - $response = (new Statuses(DI::dba(), DI::twitterStatus(), DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'rss'])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Statuses(DI::dba(), DI::twitterStatus(), DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'rss'])) ->run($this->httpExceptionMock, [ 'list_id' => 1 ]); diff --git a/tests/src/Module/Api/Twitter/Media/UploadTest.php b/tests/src/Module/Api/Twitter/Media/UploadTest.php index 7ab2bcc207..408edbf7f9 100644 --- a/tests/src/Module/Api/Twitter/Media/UploadTest.php +++ b/tests/src/Module/Api/Twitter/Media/UploadTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Media; @@ -27,10 +13,10 @@ use Friendica\Module\Api\Twitter\Media\Upload; use Friendica\Network\HTTPException\BadRequestException; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Network\HTTPException\UnauthorizedException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; use Friendica\Test\Util\AuthTestConfig; -class UploadTest extends ApiTest +class UploadTest extends ApiTestCase { protected function setUp(): void { @@ -46,7 +32,7 @@ class UploadTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Upload(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Upload(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -60,7 +46,7 @@ class UploadTest extends ApiTest $this->expectException(UnauthorizedException::class); AuthTestConfig::$authenticated = false; - (new Upload(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Upload(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -79,7 +65,7 @@ class UploadTest extends ApiTest ] ]; - (new Upload(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Upload(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -102,7 +88,7 @@ class UploadTest extends ApiTest ] ]; - $response = (new Upload(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Upload(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $media = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/SavedSearchesTest.php b/tests/src/Module/Api/Twitter/SavedSearchesTest.php index c92d949ea3..db2e55213b 100644 --- a/tests/src/Module/Api/Twitter/SavedSearchesTest.php +++ b/tests/src/Module/Api/Twitter/SavedSearchesTest.php @@ -1,36 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\Twitter\SavedSearches; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class SavedSearchesTest extends ApiTest +class SavedSearchesTest extends ApiTestCase { public function test() { - $response = (new SavedSearches(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) + $response = (new SavedSearches(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => 'json'])) ->run($this->httpExceptionMock); $result = $this->toJson($response); diff --git a/tests/src/Module/Api/Twitter/Statuses/DestroyTest.php b/tests/src/Module/Api/Twitter/Statuses/DestroyTest.php index a08e2e5d86..4b82299b4f 100644 --- a/tests/src/Module/Api/Twitter/Statuses/DestroyTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/DestroyTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; @@ -25,9 +11,9 @@ use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\Destroy; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class DestroyTest extends ApiTest +class DestroyTest extends ApiTestCase { protected function setUp(): void { @@ -45,7 +31,7 @@ class DestroyTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Destroy(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Destroy(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -71,7 +57,7 @@ class DestroyTest extends ApiTest */ public function testApiStatusesDestroyWithId() { - $response = (new Destroy(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Destroy(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 1 ]); diff --git a/tests/src/Module/Api/Twitter/Statuses/MentionsTest.php b/tests/src/Module/Api/Twitter/Statuses/MentionsTest.php index 41a1fcd4cc..2e31ae382b 100644 --- a/tests/src/Module/Api/Twitter/Statuses/MentionsTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/MentionsTest.php @@ -1,33 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\Mentions; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class MentionsTest extends ApiTest +class MentionsTest extends ApiTestCase { /** * Test the api_statuses_mentions() function. @@ -36,7 +21,7 @@ class MentionsTest extends ApiTest */ public function testApiStatusesMentions() { - $response = (new Mentions(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Mentions(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'max_id' => 10 ]); @@ -54,7 +39,7 @@ class MentionsTest extends ApiTest */ public function testApiStatusesMentionsWithNegativePage() { - $response = (new Mentions(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Mentions(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'page' => -2 ]); @@ -86,7 +71,7 @@ class MentionsTest extends ApiTest */ public function testApiStatusesMentionsWithRss() { - $response = (new Mentions(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => ICanCreateResponses::TYPE_RSS])) + $response = (new Mentions(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], ['extension' => ICanCreateResponses::TYPE_RSS])) ->run($this->httpExceptionMock, [ 'page' => -2 ]); diff --git a/tests/src/Module/Api/Twitter/Statuses/NetworkPublicTimelineTest.php b/tests/src/Module/Api/Twitter/Statuses/NetworkPublicTimelineTest.php index 7903ac4819..52db7b76de 100644 --- a/tests/src/Module/Api/Twitter/Statuses/NetworkPublicTimelineTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/NetworkPublicTimelineTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\NetworkPublicTimeline; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class NetworkPublicTimelineTest extends ApiTest +class NetworkPublicTimelineTest extends ApiTestCase { /** * Test the api_statuses_networkpublic_timeline() function. @@ -36,7 +22,10 @@ class NetworkPublicTimelineTest extends ApiTest */ public function testApiStatusesNetworkpublicTimeline() { - $response = (new NetworkPublicTimeline(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new NetworkPublicTimeline(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'max_id' => 10 ]); @@ -58,7 +47,10 @@ class NetworkPublicTimelineTest extends ApiTest */ public function testApiStatusesNetworkpublicTimelineWithNegativePage() { - $response = (new NetworkPublicTimeline(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new NetworkPublicTimeline(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'page' => -2 ]); @@ -94,7 +86,10 @@ class NetworkPublicTimelineTest extends ApiTest */ public function testApiStatusesNetworkpublicTimelineWithRss() { - $response = (new NetworkPublicTimeline(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new NetworkPublicTimeline(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ 'extension' => ICanCreateResponses::TYPE_RSS ]))->run($this->httpExceptionMock, [ 'page' => -2 diff --git a/tests/src/Module/Api/Twitter/Statuses/RetweetTest.php b/tests/src/Module/Api/Twitter/Statuses/RetweetTest.php index 3de93b096e..b8a7a50428 100644 --- a/tests/src/Module/Api/Twitter/Statuses/RetweetTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/RetweetTest.php @@ -1,33 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; use Friendica\App\Router; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\Retweet; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class RetweetTest extends ApiTest +class RetweetTest extends ApiTestCase { protected function setUp(): void { @@ -45,7 +32,7 @@ class RetweetTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Retweet(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Retweet(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -71,7 +58,7 @@ class RetweetTest extends ApiTest */ public function testApiStatusesRepeatWithId() { - $response = (new Retweet(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Retweet(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 1 ]); @@ -88,7 +75,10 @@ class RetweetTest extends ApiTest */ public function testApiStatusesRepeatWithSharedId() { - $response = (new Retweet(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Retweet(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 5 ]); diff --git a/tests/src/Module/Api/Twitter/Statuses/ShowTest.php b/tests/src/Module/Api/Twitter/Statuses/ShowTest.php index a3ab3d30cc..68df218a32 100644 --- a/tests/src/Module/Api/Twitter/Statuses/ShowTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/ShowTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; -use Friendica\App\Router; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\Show; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ShowTest extends ApiTest +class ShowTest extends ApiTestCase { /** * Test the api_statuses_show() function. @@ -39,7 +25,7 @@ class ShowTest extends ApiTest $this->expectException(BadRequestException::class); - (new Show(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Show(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -50,7 +36,7 @@ class ShowTest extends ApiTest */ public function testApiStatusesShowWithId() { - $response = (new Show(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Show(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 1 ]); @@ -68,7 +54,10 @@ class ShowTest extends ApiTest */ public function testApiStatusesShowWithConversation() { - $response = (new Show(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Show(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'id' => 1, 'conversation' => 1 diff --git a/tests/src/Module/Api/Twitter/Statuses/UpdateTest.php b/tests/src/Module/Api/Twitter/Statuses/UpdateTest.php index 4f1cd37ed3..d044c900dd 100644 --- a/tests/src/Module/Api/Twitter/Statuses/UpdateTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/UpdateTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; use Friendica\App\Router; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\Update; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class UpdateTest extends ApiTest +class UpdateTest extends ApiTestCase { protected function setUp(): void { @@ -54,7 +40,7 @@ class UpdateTest extends ApiTest ] ]; - $response = (new Update(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Update(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'status' => 'Status content #friendica', 'in_reply_to_status_id' => 0, @@ -76,7 +62,7 @@ class UpdateTest extends ApiTest */ public function testApiStatusesUpdateWithHtml() { - $response = (new Update(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Update(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'htmlstatus' => 'Status content', ]); diff --git a/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php b/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php index 77b51bb7e4..aeadede16c 100644 --- a/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php +++ b/tests/src/Module/Api/Twitter/Statuses/UserTimelineTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Statuses; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Statuses\UserTimeline; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class UserTimelineTest extends ApiTest +class UserTimelineTest extends ApiTestCase { /** * Test the api_statuses_user_timeline() function. @@ -36,9 +22,9 @@ class UserTimelineTest extends ApiTest */ public function testApiStatusesUserTimeline() { - $response = (new UserTimeline(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new UserTimeline(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ - 'user_id' => 42, + 'user_id' => 43, // Public contact id 'max_id' => 10, 'exclude_replies' => true, 'conversation_id' => 1, @@ -61,9 +47,12 @@ class UserTimelineTest extends ApiTest */ public function testApiStatusesUserTimelineWithNegativePage() { - $response = (new UserTimeline(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new UserTimeline(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ - 'user_id' => 42, + 'user_id' => 43, // Public contact id 'page' => -2, ]); @@ -84,7 +73,7 @@ class UserTimelineTest extends ApiTest */ public function testApiStatusesUserTimelineWithRss() { - $response = (new UserTimeline(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ + $response = (new UserTimeline(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ 'extension' => ICanCreateResponses::TYPE_RSS ]))->run($this->httpExceptionMock); diff --git a/tests/src/Module/Api/Twitter/Users/LookupTest.php b/tests/src/Module/Api/Twitter/Users/LookupTest.php index 1228d2cbc2..aa53cce5cb 100644 --- a/tests/src/Module/Api/Twitter/Users/LookupTest.php +++ b/tests/src/Module/Api/Twitter/Users/LookupTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Users; -use Friendica\App\Router; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Users\Lookup; use Friendica\Network\HTTPException\NotFoundException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class LookupTest extends ApiTest +class LookupTest extends ApiTestCase { /** * Test the api_users_lookup() function. @@ -38,7 +24,7 @@ class LookupTest extends ApiTest { $this->expectException(NotFoundException::class); - (new Lookup(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Lookup(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } @@ -49,7 +35,10 @@ class LookupTest extends ApiTest */ public function testApiUsersLookupWithUserId() { - $response = (new Lookup(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Lookup(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'user_id' => static::OTHER_USER['id'] ]); diff --git a/tests/src/Module/Api/Twitter/Users/SearchTest.php b/tests/src/Module/Api/Twitter/Users/SearchTest.php index 71c4e1240d..c5996a5345 100644 --- a/tests/src/Module/Api/Twitter/Users/SearchTest.php +++ b/tests/src/Module/Api/Twitter/Users/SearchTest.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Users; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; +use Friendica\Core\Renderer; use Friendica\DI; use Friendica\Module\Api\Twitter\Users\Search; use Friendica\Network\HTTPException\BadRequestException; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class SearchTest extends ApiTest +class SearchTest extends ApiTestCase { /** * Test the api_users_search() function. @@ -37,7 +23,10 @@ class SearchTest extends ApiTest */ public function testApiUsersSearch() { - $response = (new Search(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Search(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock, [ 'q' => static::OTHER_USER['name'] ]); @@ -54,7 +43,10 @@ class SearchTest extends ApiTest */ public function testApiUsersSearchWithXml() { - $response = (new Search(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ + // @todo: This call is needed for this test + Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); + + $response = (new Search(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ 'extension' => ICanCreateResponses::TYPE_XML ]))->run($this->httpExceptionMock, [ 'q' => static::OTHER_USER['name'] @@ -72,7 +64,7 @@ class SearchTest extends ApiTest { $this->expectException(BadRequestException::class); - (new Search(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + (new Search(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); } } diff --git a/tests/src/Module/Api/Twitter/Users/ShowTest.php b/tests/src/Module/Api/Twitter/Users/ShowTest.php index e7fc678a8d..d44db14ef2 100644 --- a/tests/src/Module/Api/Twitter/Users/ShowTest.php +++ b/tests/src/Module/Api/Twitter/Users/ShowTest.php @@ -1,33 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Api\Twitter\Users; -use Friendica\App\Router; use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Api\Twitter\Users\Show; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class ShowTest extends ApiTest +class ShowTest extends ApiTestCase { /** * Test the api_users_show() function. @@ -36,7 +21,7 @@ class ShowTest extends ApiTest */ public function testApiUsersShow() { - $response = (new Show(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) + $response = (new Show(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [])) ->run($this->httpExceptionMock); $json = $this->toJson($response); @@ -56,7 +41,7 @@ class ShowTest extends ApiTest */ public function testApiUsersShowWithXml() { - $response = (new Show(DI::mstdnError(), DI::app(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ + $response = (new Show(DI::mstdnError(), DI::appHelper(), DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], [ 'extension' => ICanCreateResponses::TYPE_XML ]))->run($this->httpExceptionMock); diff --git a/tests/src/Module/BaseApiTest.php b/tests/src/Module/BaseApiTest.php index 08a0794c83..4397913780 100644 --- a/tests/src/Module/BaseApiTest.php +++ b/tests/src/Module/BaseApiTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module; use Friendica\Module\BaseApi; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class BaseApiTest extends ApiTest +class BaseApiTest extends ApiTestCase { public function testWithWrongAuth() { diff --git a/tests/src/Module/NodeInfoTest.php b/tests/src/Module/NodeInfoTest.php index 96790b5a91..21af637508 100644 --- a/tests/src/Module/NodeInfoTest.php +++ b/tests/src/Module/NodeInfoTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module; @@ -28,10 +14,10 @@ use Friendica\Module\NodeInfo110; use Friendica\Module\NodeInfo120; use Friendica\Module\NodeInfo210; use Friendica\Module\Special\HTTPException; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Mockery\MockInterface; -class NodeInfoTest extends FixtureTest +class NodeInfoTest extends FixtureTestCase { /** @var MockInterface|HTTPException */ protected $httpExceptionMock; diff --git a/tests/src/Module/Special/OptionsTest.php b/tests/src/Module/Special/OptionsTest.php index dad0f1e5bc..eb6e40e08a 100644 --- a/tests/src/Module/Special/OptionsTest.php +++ b/tests/src/Module/Special/OptionsTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Module\Special; @@ -26,10 +12,10 @@ use Friendica\Capabilities\ICanCreateResponses; use Friendica\DI; use Friendica\Module\Special\HTTPException; use Friendica\Module\Special\Options; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Mockery\MockInterface; -class OptionsTest extends FixtureTest +class OptionsTest extends FixtureTestCase { /** @var MockInterface|HTTPException */ protected $httpExceptionMock; diff --git a/tests/src/Module/StatsCachingTest.php b/tests/src/Module/StatsCachingTest.php new file mode 100644 index 0000000000..58226769a9 --- /dev/null +++ b/tests/src/Module/StatsCachingTest.php @@ -0,0 +1,203 @@ +httpExceptionMock = \Mockery::mock(HTTPException::class); + $this->config = \Mockery::mock(IManageConfigValues::class); + $this->cache = new ArrayCache('localhost'); + $this->lock = new CacheLock($this->cache); + } + + public function testStatsCachingNotAllowed() + { + $this->httpExceptionMock->shouldReceive('content')->andReturn('failed')->once(); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock); + + self::assertEquals('404', $response->getStatusCode()); + self::assertEquals('Page not found', $response->getReasonPhrase()); + self::assertEquals('failed', $response->getBody()); + } + + public function testStatsCachingWitMinimumCache() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals([ + 'type' => 'array', + 'stats' => [], + ], $json['cache']); + self::assertEquals([ + 'type' => 'array', + 'stats' => [], + ], $json['lock']); + } + + public function testStatsCachingWithDatabase() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals(['enabled' => false], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } + + public function testStatsCachingWithCache() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals(['enabled' => false], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } + + public function testStatsCachingWithOpcacheAndNull() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true); + PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn(false); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals([ + 'enabled' => false, + 'hit_rate' => null, + 'used_memory' => null, + 'free_memory' => null, + 'num_cached_scripts' => null, + ], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } + + public function testStatsCachingWithOpcacheAndValues() + { + $request = [ + 'key' => '12345', + ]; + $this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345'); + + $this->cache = new DatabaseCache('localhost', DI::dba()); + $this->lock = new DatabaseLock(DI::dba()); + PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true); + PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn([ + 'opcache_enabled' => true, + 'opcache_statistics' => [ + 'opcache_hit_rate' => 1, + 'num_cached_scripts' => 2, + ], + 'memory_usage' => [ + 'used_memory' => 3, + 'free_memory' => 4, + ] + ]); + + $response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, [])) + ->run($this->httpExceptionMock, $request); + + self::assertJson($response->getBody()); + self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders()); + + $json = json_decode($response->getBody(), true); + + self::assertEquals([ + 'enabled' => true, + 'hit_rate' => 1, + 'used_memory' => 3, + 'free_memory' => 4, + 'num_cached_scripts' => 2, + ], $json['opcache']); + self::assertEquals(['type' => 'database'], $json['cache']); + self::assertEquals(['type' => 'database'], $json['lock']); + } +} diff --git a/tests/src/Navigation/Notifications/Entity/NotifyTest.php b/tests/src/Navigation/Notifications/Entity/NotifyTest.php index fac8e48293..ff88e36b90 100644 --- a/tests/src/Navigation/Notifications/Entity/NotifyTest.php +++ b/tests/src/Navigation/Notifications/Entity/NotifyTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Navigation\Notifications\Entity; use Friendica\Navigation\Notifications\Entity\Notify; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class NotifyTest extends FixtureTest +class NotifyTest extends FixtureTestCase { public function dataFormatNotify(): array { diff --git a/tests/src/Network/HTTPClient/Client/HTTPClientTest.php b/tests/src/Network/HTTPClient/Client/HTTPClientTest.php index 2e6afc841e..bd15a7120d 100644 --- a/tests/src/Network/HTTPClient/Client/HTTPClientTest.php +++ b/tests/src/Network/HTTPClient/Client/HTTPClientTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Network\HTTPClient\Client; use Friendica\DI; use Friendica\Test\DiceHttpMockHandlerTrait; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Psr7\Response; -class HTTPClientTest extends MockedTest +class HTTPClientTest extends MockedTestCase { use DiceHttpMockHandlerTrait; diff --git a/tests/src/Network/HTTPClient/Response/CurlResultTest.php b/tests/src/Network/HTTPClient/Response/CurlResultTest.php index e5047d6af4..979e59cf46 100644 --- a/tests/src/Network/HTTPClient/Response/CurlResultTest.php +++ b/tests/src/Network/HTTPClient/Response/CurlResultTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Network\HTTPClient\Response; @@ -47,7 +33,7 @@ class CurlResultTest extends TestCase self::assertFalse($curlResult->isTimeout()); self::assertFalse($curlResult->isRedirectUrl()); self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); + self::assertSame($body, $curlResult->getBodyString()); self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); self::assertSame('https://test.local', $curlResult->getUrl()); self::assertSame('https://test.local', $curlResult->getRedirectUrl()); @@ -76,7 +62,7 @@ class CurlResultTest extends TestCase self::assertFalse($curlResult->isTimeout()); self::assertTrue($curlResult->isRedirectUrl()); self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); + self::assertSame($body, $curlResult->getBodyString()); self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); self::assertSame('https://test.local/test/it', $curlResult->getUrl()); self::assertSame('https://test.other/test/it', $curlResult->getRedirectUrl()); @@ -103,7 +89,7 @@ class CurlResultTest extends TestCase self::assertTrue($curlResult->isTimeout()); self::assertFalse($curlResult->isRedirectUrl()); self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); + self::assertSame($body, $curlResult->getBodyString()); self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); self::assertSame('https://test.local/test/it', $curlResult->getRedirectUrl()); self::assertSame('Tested error', $curlResult->getError()); @@ -131,7 +117,7 @@ class CurlResultTest extends TestCase self::assertFalse($curlResult->isTimeout()); self::assertTrue($curlResult->isRedirectUrl()); self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); + self::assertSame($body, $curlResult->getBodyString()); self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); self::assertSame('https://test.local/test/it?key=value', $curlResult->getUrl()); self::assertSame('https://test.other/some/?key=value', $curlResult->getRedirectUrl()); diff --git a/tests/src/Network/MimeTypeTest.php b/tests/src/Network/MimeTypeTest.php index 64a52d8d6b..b6715bf961 100644 --- a/tests/src/Network/MimeTypeTest.php +++ b/tests/src/Network/MimeTypeTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Network; diff --git a/tests/src/Network/ProbeTest.php b/tests/src/Network/ProbeTest.php index d9c0f074ec..653cc0ca62 100644 --- a/tests/src/Network/ProbeTest.php +++ b/tests/src/Network/ProbeTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Network; use Friendica\Network\Probe; use Friendica\Test\DiceHttpMockHandlerTrait; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use GuzzleHttp\Middleware; -class ProbeTest extends MockedTest +class ProbeTest extends MockedTestCase { use DiceHttpMockHandlerTrait; diff --git a/tests/src/Object/Log/ParsedLogLineTest.php b/tests/src/Object/Log/ParsedLogLineTest.php index 1bebd34bd1..08486062b0 100644 --- a/tests/src/Object/Log/ParsedLogLineTest.php +++ b/tests/src/Object/Log/ParsedLogLineTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Object\Log; diff --git a/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php b/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php index 1abcecb79b..e718eebf07 100644 --- a/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php +++ b/tests/src/Profile/ProfileField/Entity/ProfileFieldTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Profile\ProfileField\Entity; @@ -27,13 +13,13 @@ use Friendica\Profile\ProfileField\Exception\UnexpectedPermissionSetException; use Friendica\Profile\ProfileField\Factory\ProfileField as ProfileFieldFactory; use Friendica\Security\PermissionSet\Repository\PermissionSet as PermissionSetRepository; use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\ACLFormatter; use Friendica\Util\DateTimeFormat; use Mockery\MockInterface; use Psr\Log\NullLogger; -class ProfileFieldTest extends MockedTest +class ProfileFieldTest extends MockedTestCase { /** @var MockInterface|PermissionSetRepository */ protected $permissionSetRepository; diff --git a/tests/src/Profile/ProfileField/Repository/ProfileFieldTest.php b/tests/src/Profile/ProfileField/Repository/ProfileFieldTest.php index 145e543195..2f5ab7bce8 100644 --- a/tests/src/Profile/ProfileField/Repository/ProfileFieldTest.php +++ b/tests/src/Profile/ProfileField/Repository/ProfileFieldTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Profile\ProfileField\Repository; @@ -28,10 +14,10 @@ use Friendica\Profile\ProfileField\Factory\ProfileField as ProfileFieldFactory; use Friendica\Security\PermissionSet\Repository\PermissionSet; use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory; use Friendica\Security\PermissionSet\Repository\PermissionSet as PermissionSetRepository; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Friendica\DI; -class ProfileFieldTest extends FixtureTest +class ProfileFieldTest extends FixtureTestCase { /** @var ProfileFieldRepository */ private $depository; diff --git a/tests/src/Protocol/ActivityPub/ProcessorMock.php b/tests/src/Protocol/ActivityPub/ProcessorMock.php index 07dd99ef02..3ce0475f92 100644 --- a/tests/src/Protocol/ActivityPub/ProcessorMock.php +++ b/tests/src/Protocol/ActivityPub/ProcessorMock.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Protocol\ActivityPub; diff --git a/tests/src/Protocol/ActivityPub/ProcessorTest.php b/tests/src/Protocol/ActivityPub/ProcessorTest.php index ee0c71e5e7..636a338332 100644 --- a/tests/src/Protocol/ActivityPub/ProcessorTest.php +++ b/tests/src/Protocol/ActivityPub/ProcessorTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Protocol\ActivityPub; diff --git a/tests/src/Protocol/ActivityPub/TransmitterTest.php b/tests/src/Protocol/ActivityPub/TransmitterTest.php index 3eb9cb020e..f9ef18a1a5 100644 --- a/tests/src/Protocol/ActivityPub/TransmitterTest.php +++ b/tests/src/Protocol/ActivityPub/TransmitterTest.php @@ -1,33 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Protocol\ActivityPub; use Friendica\Core\Hook; +use Friendica\Core\Hooks\HookEventBridge; use Friendica\DI; use Friendica\Model\Post; use Friendica\Protocol\ActivityPub\Transmitter; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; -class TransmitterTest extends FixtureTest +class TransmitterTest extends FixtureTestCase { protected function setUp(): void { @@ -35,11 +22,18 @@ class TransmitterTest extends FixtureTest DI::config()->set('system', 'no_smilies', false); + /** @var \Friendica\Event\EventDispatcher */ + $eventDispatcher = DI::eventDispatcher(); + + foreach (HookEventBridge::getStaticSubscribedEvents() as $eventName => $methodName) { + $eventDispatcher->addListener($eventName, [HookEventBridge::class, $methodName]); + } + Hook::register('smilie', 'tests/Util/SmileyWhitespaceAddon.php', 'add_test_unicode_smilies'); Hook::loadHooks(); } - public function testEmojiPost() + public function testEmojiPost(): void { $post = Post::selectFirst([], ['id' => 14]); $this->assertNotNull($post); diff --git a/tests/src/Protocol/ActivityTest.php b/tests/src/Protocol/ActivityTest.php index b93a0352ee..30359321cf 100644 --- a/tests/src/Protocol/ActivityTest.php +++ b/tests/src/Protocol/ActivityTest.php @@ -1,31 +1,17 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Protocol; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityNamespace; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class ActivityTest extends MockedTest +class ActivityTest extends MockedTestCase { public function dataMatch() { diff --git a/tests/src/Protocol/HTTP/MediaTypeTest.php b/tests/src/Protocol/HTTP/MediaTypeTest.php index e863d75ea0..f926a5bbb5 100644 --- a/tests/src/Protocol/HTTP/MediaTypeTest.php +++ b/tests/src/Protocol/HTTP/MediaTypeTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Protocol\HTTP; diff --git a/tests/src/Protocol/SalmonTest.php b/tests/src/Protocol/SalmonTest.php index 3583a6e85d..aa531d5bd0 100644 --- a/tests/src/Protocol/SalmonTest.php +++ b/tests/src/Protocol/SalmonTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Protocol; diff --git a/tests/src/Protocol/WebFingerUriTest.php b/tests/src/Protocol/WebFingerUriTest.php index 9217ea16a6..c4f0efd841 100644 --- a/tests/src/Protocol/WebFingerUriTest.php +++ b/tests/src/Protocol/WebFingerUriTest.php @@ -1,21 +1,10 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Main database structure configuration file. * diff --git a/tests/src/Security/BasicAuthTest.php b/tests/src/Security/BasicAuthTest.php index be3dacd9b9..72cf9df59e 100644 --- a/tests/src/Security/BasicAuthTest.php +++ b/tests/src/Security/BasicAuthTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Security; use Friendica\Security\BasicAuth; -use Friendica\Test\src\Module\Api\ApiTest; +use Friendica\Test\ApiTestCase; -class BasicAuthTest extends ApiTest +class BasicAuthTest extends ApiTestCase { /** * Test the api_source() function. diff --git a/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php b/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php index 2db2f29f4b..bb456d0445 100644 --- a/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php +++ b/tests/src/Security/PermissionSet/Entity/PermissionSetTest.php @@ -1,30 +1,16 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Security\PermissionSet\Entity; use Friendica\Security\PermissionSet\Entity\PermissionSet; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; -class PermissionSetTest extends MockedTest +class PermissionSetTest extends MockedTestCase { public function dateAllowedContacts() { diff --git a/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php b/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php index 41d07fe93d..25ff93cbbd 100644 --- a/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php +++ b/tests/src/Security/PermissionSet/Factory/PermissionSetTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Security\PermissionSet\Factory; use Friendica\Security\PermissionSet\Factory\PermissionSet; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\ACLFormatter; use Psr\Log\NullLogger; -class PermissionSetTest extends MockedTest +class PermissionSetTest extends MockedTestCase { /** @var PermissionSet */ protected $permissionSet; diff --git a/tests/src/Security/PermissionSet/Repository/PermissionSetTest.php b/tests/src/Security/PermissionSet/Repository/PermissionSetTest.php index 4a742fc303..d92789668d 100644 --- a/tests/src/Security/PermissionSet/Repository/PermissionSetTest.php +++ b/tests/src/Security/PermissionSet/Repository/PermissionSetTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Security\PermissionSet\Repository; @@ -27,10 +13,10 @@ use Friendica\Security\PermissionSet\Exception\PermissionSetNotFoundException; use Friendica\Security\PermissionSet\Repository\PermissionSet as PermissionSetRepository; use Friendica\Security\PermissionSet\Entity\PermissionSet; use Friendica\Security\PermissionSet\Factory\PermissionSet as PermissionSetFactory; -use Friendica\Test\FixtureTest; +use Friendica\Test\FixtureTestCase; use Friendica\DI; -class PermissionSetTest extends FixtureTest +class PermissionSetTest extends FixtureTestCase { /** @var PermissionSetRepository */ private $repository; diff --git a/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php b/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php index cc392ef54e..fdf2722367 100644 --- a/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php +++ b/tests/src/Security/TwoFactor/Factory/TrustedBrowserTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Security\TwoFactor\Factory; use Friendica\Security\TwoFactor\Factory\TrustedBrowser; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; use Psr\Log\NullLogger; -class TrustedBrowserTest extends MockedTest +class TrustedBrowserTest extends MockedTestCase { public function testCreateFromTableRowSuccess() { diff --git a/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php b/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php index 3ab24b1dfa..08f12f562a 100644 --- a/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php +++ b/tests/src/Security/TwoFactor/Model/TrustedBrowserTest.php @@ -1,32 +1,18 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Security\TwoFactor\Model; use Friendica\Security\TwoFactor\Model\TrustedBrowser; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; -class TrustedBrowserTest extends MockedTest +class TrustedBrowserTest extends MockedTestCase { public function test__construct() { diff --git a/tests/src/Util/ACLFormatterTest.php b/tests/src/Util/ACLFormatterTest.php index 93e1767dc5..b2aa364d41 100644 --- a/tests/src/Util/ACLFormatterTest.php +++ b/tests/src/Util/ACLFormatterTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/tests/src/Util/ArraysTest.php b/tests/src/Util/ArraysTest.php index 0c992ef254..2a062774a8 100644 --- a/tests/src/Util/ArraysTest.php +++ b/tests/src/Util/ArraysTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/tests/src/Util/BasePathTest.php b/tests/src/Util/BasePathTest.php deleted file mode 100644 index 844911d32f..0000000000 --- a/tests/src/Util/BasePathTest.php +++ /dev/null @@ -1,96 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Util; - -use Friendica\Test\MockedTest; -use Friendica\Util\BasePath; - -class BasePathTest extends MockedTest -{ - public function dataPaths() - { - return [ - 'fullPath' => [ - 'server' => [], - 'input' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - 'output' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'relative' => [ - 'server' => [], - 'input' => 'config', - 'output' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'document_root' => [ - 'server' => [ - 'DOCUMENT_ROOT' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'input' => '/noooop', - 'output' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'pwd' => [ - 'server' => [ - 'PWD' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'input' => '/noooop', - 'output' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'no_overwrite' => [ - 'server' => [ - 'DOCUMENT_ROOT' => dirname(__DIR__, 3), - 'PWD' => dirname(__DIR__, 3), - ], - 'input' => 'config', - 'output' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'no_overwrite_if_invalid' => [ - 'server' => [ - 'DOCUMENT_ROOT' => '/nopopop', - 'PWD' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ], - 'input' => '/noatgawe22fafa', - 'output' => dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config', - ] - ]; - } - - /** - * Test the basepath determination - * @dataProvider dataPaths - */ - public function testDetermineBasePath(array $server, $input, $output) - { - $basepath = new BasePath($input, $server); - self::assertEquals($output, $basepath->getPath()); - } - - /** - * Test the basepath determination with a complete wrong path - */ - public function testFailedBasePath() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessageMatches("/(.*) is not a valid basepath/"); - - $basepath = new BasePath('/now23452sgfgas', []); - $basepath->getPath(); - } -} diff --git a/tests/src/Util/CryptoTest.php b/tests/src/Util/CryptoTest.php deleted file mode 100644 index 82afc24832..0000000000 --- a/tests/src/Util/CryptoTest.php +++ /dev/null @@ -1,108 +0,0 @@ -. - * - * This is in the same namespace as Crypto for mocking 'rand' and 'random_init' - */ - -/// @todo Use right namespace - needs alternative way of mocking random_int() -namespace Friendica\Util; - -use phpseclib\Crypt\RSA; -use phpseclib\Math\BigInteger; -use PHPUnit\Framework\TestCase; - -class CryptoTest extends TestCase -{ - public static function tearDownAfterClass(): void - { - // Reset mocking - global $phpMock; - $phpMock = []; - - parent::tearDownAfterClass(); - } - - /** - * Replaces random_int results with given mocks - * - */ - private function assertRandomInt($min, $max) - { - global $phpMock; - $phpMock['random_int'] = function ($mMin, $mMax) use ($min, $max) { - self::assertEquals($min, $mMin); - self::assertEquals($max, $mMax); - return 1; - }; - } - - public function testRandomDigitsRandomInt() - { - self::assertRandomInt(0, 9); - - $test = Crypto::randomDigits(1); - self::assertEquals(1, strlen($test)); - self::assertEquals(1, $test); - - $test = Crypto::randomDigits(8); - self::assertEquals(8, strlen($test)); - self::assertEquals(11111111, $test); - } - - public function dataRsa(): array - { - return [ - 'diaspora' => [ - 'key' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/diaspora-public-rsa-base64'), - 'expected' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/diaspora-public-pem'), - ], - ]; - } - - /** - * @dataProvider dataRsa - */ - public function testPubRsaToMe(string $key, string $expected) - { - self::assertEquals($expected, Crypto::rsaToPem(base64_decode($key))); - } - - - public function dataPEM() - { - return [ - 'diaspora' => [ - 'key' => file_get_contents(__DIR__ . '/../../datasets/crypto/rsa/diaspora-public-pem'), - ], - ]; - } -} - -/** - * A workaround to replace the PHP native random_int() (>= 7.0) with a mocked function - * - * @return int - */ -function random_int($min, $max) -{ - global $phpMock; - if (isset($phpMock['random_int'])) { - return call_user_func_array($phpMock['random_int'], func_get_args()); - } -} diff --git a/tests/src/Util/DateTimeFormatTest.php b/tests/src/Util/DateTimeFormatTest.php index 046680de62..6cc506807e 100644 --- a/tests/src/Util/DateTimeFormatTest.php +++ b/tests/src/Util/DateTimeFormatTest.php @@ -1,68 +1,54 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\DateTimeFormat; -class DateTimeFormatTest extends MockedTest +class DateTimeFormatTest extends MockedTestCase { public function dataYearMonth() { return [ 'validNormal' => [ - 'input' => '1990-10', + 'input' => '1990-10', 'assert' => true, ], 'validOneCharMonth' => [ - 'input' => '1990-1', + 'input' => '1990-1', 'assert' => true, ], 'validTwoCharMonth' => [ - 'input' => '1990-01', + 'input' => '1990-01', 'assert' => true, ], 'invalidFormat' => [ - 'input' => '199-11', + 'input' => '199-11', 'assert' => false, ], 'invalidFormat2' => [ - 'input' => '1990-15', + 'input' => '1990-15', 'assert' => false, ], 'invalidFormat3' => [ - 'input' => '99-101', + 'input' => '99-101', 'assert' => false, ], 'invalidFormat4' => [ - 'input' => '11-1990', + 'input' => '11-1990', 'assert' => false, ], 'invalidFuture' => [ - 'input' => '3030-12', + 'input' => '3030-12', 'assert' => false, ], 'invalidYear' => [ - 'input' => '-100-10', + 'input' => '-100-10', 'assert' => false, ], ]; @@ -93,51 +79,55 @@ class DateTimeFormatTest extends MockedTest return [ 'Mo, 19 Sep 2022 14:51:00 +0200' => [ 'expectedDate' => '2022-09-19T14:51:00+02:00', - 'dateString' => 'Mo, 19 Sep 2022 14:51:00 +0200', + 'dateString' => 'Mo, 19 Sep 2022 14:51:00 +0200', ], '2020-11-21T12:00:13.745339ZZ' => [ 'expectedDate' => '2020-11-21T12:00:13+00:00', - 'dateString' => '2020-11-21T12:00:13.745339ZZ', + 'dateString' => '2020-11-21T12:00:13.745339ZZ', ], '2016-09-09T13:32:00ZZ' => [ 'expectedDate' => '2016-09-09T13:32:00+00:00', - 'dateString' => '2016-09-09T13:32:00ZZ', + 'dateString' => '2016-09-09T13:32:00ZZ', ], 'Sun, 10/03/2021 - 12:41' => [ 'expectedDate' => '2021-10-03T12:41:00+00:00', - 'dateString' => 'Sun, 10/03/2021 - 12:41', + 'dateString' => 'Sun, 10/03/2021 - 12:41', ], '4:30 PM, Sep 13, 2022' => [ 'expectedDate' => '2022-09-13T16:30:00+00:00', - 'dateString' => '4:30 PM, Sep 13, 2022', + 'dateString' => '4:30 PM, Sep 13, 2022', ], 'August 27, 2022 - 21:00' => [ 'expectedDate' => '2022-08-27T21:00:00+00:00', - 'dateString' => 'August 27, 2022 - 21:00', + 'dateString' => 'August 27, 2022 - 21:00', ], '2021-09-19T14:06:03+00:00' => [ 'expectedDate' => '2021-09-19T14:06:03+00:00', - 'dateString' => '2021-09-19T14:06:03+00:00', + 'dateString' => '2021-09-19T14:06:03+00:00', ], 'Eastern Time timezone' => [ 'expectedDate' => '2022-09-30T00:00:00-05:00', - 'dateString' => 'September 30, 2022, 12:00 a.m. ET', + 'dateString' => 'September 30, 2022, 12:00 a.m. ET', ], 'German date time string' => [ 'expectedDate' => '2022-10-05T16:34:00+02:00', - 'dateString' => '05 Okt 2022 16:34:00 +0200', + 'dateString' => '05 Okt 2022 16:34:00 +0200', ], '(Coordinated Universal Time)' => [ 'expectedDate' => '2022-12-30T14:29:10+00:00', - 'dateString' => 'Fri Dec 30 2022 14:29:10 GMT+0000 (Coordinated Universal Time)', + 'dateString' => 'Fri Dec 30 2022 14:29:10 GMT+0000 (Coordinated Universal Time)', ], 'Double HTML encode' => [ 'expectedDate' => '2015-05-22T08:48:00+12:00', - 'dateString' => '2015-05-22T08:48:00&#43;12:00' + 'dateString' => '2015-05-22T08:48:00&#43;12:00' ], '2023-04-02\T17:22:42+05:30' => [ 'expectedDate' => '2023-04-02T17:22:42+05:30', - 'dateString' => '2023-04-02\T17:22:42+05:30' + 'dateString' => '2023-04-02\T17:22:42+05:30' + ], + '2025-03-07T08:54:14.341+01:00[Europe/Berlin]' => [ + 'expectedDate' => '2025-03-07T08:54:14+01:00', + 'dateString' => '2025-03-07T08:54:14.341+01:00[Europe/Berlin]' ], ]; } @@ -165,9 +155,87 @@ class DateTimeFormatTest extends MockedTest */ public function testConvertRelative() { - $now = DateTimeFormat::utcNow('U'); + $now = DateTimeFormat::utcNow('U'); $date = DateTimeFormat::utc('now - 3 days', 'U'); $this->assertEquals(259200, $now - $date); } + + public function dataConvert() + { + return [ + 'unix timestamp' => [ + 'expected' => '2025-03-12 16:18:27', + 's' => '1741796307', + ], + 'ATOM' => [ + 'expected' => '2022-06-02 15:58:35', + 's' => '2022-06-02T16:58:35+01:00', + ], + 'COOKIE' => [ + 'expected' => '2022-06-02 14:58:35', + 's' => 'Thursday, 02-Jun-2022 16:58:35 Africa/Cairo', + ], + 'ISO 8601/RFC 3339' => [ + 'expected' => '2022-06-02 13:58:35', + 's' => '2022-06-02T16:58:35+0300', + ], + 'RFC 822/RFC 1036' => [ + 'expected' => '2022-06-02 12:58:35', + 's' => 'Thu, 02 Jun 22 16:58:35 +0400', + ], + 'RFC 850' => [ + 'expected' => '2022-06-02 11:58:35', + 's' => 'Thursday, 02-Jun-22 16:58:35 Indian/Kerguelen', + ], + 'RFC 1123/RFC 2822/RSS' => [ + 'expected' => '2022-06-02 10:58:35', + 's' => 'Thu, 02 Jun 2022 16:58:35 +0600', + ], + 'RFC 3339/W3C' => [ + 'expected' => '2025-03-07 01:54:14', + 's' => '2025-03-07T08:54:14+07:00', + ], + 'RFC 3339 extended' => [ + 'expected' => '2025-03-07 00:54:14', + 's' => '2025-03-07T08:54:14.341+08:00', + ], + 'RFC 7231' => [ + 'expected' => '2022-06-02 07:58:35', + 's' => 'Thu, 02 Jun 2022 16:58:35 Asia/Tokyo', + ], + ]; + } + + /** + * @dataProvider dataConvert + */ + public function testConvert($expected, string $s = 'now', string $tz_to = 'UTC', string $tz_from = 'UTC', string $format = DateTimeFormat::MYSQL) + { + $this->assertSame($expected, DateTimeFormat::convert($s, $tz_to, $tz_from, $format)); + } + + public function dataConvertNow() + { + return [ + 'now missing' => [ + ], + 'now empty' => [ + 's' => '', + ], + 'now now' => [ + 's' => 'now', + ], + ]; + } + + /** + * @dataProvider dataConvertNow + */ + public function testConvertNow(string $s = 'now', string $tz_to = 'UTC', string $tz_from = 'UTC', string $format = DateTimeFormat::MYSQL) + { + $this->assertSame(date(DateTimeFormat::MYSQL), DateTimeFormat::convert($s, $tz_to, $tz_from, $format)); + } + + } diff --git a/tests/src/Util/EMailerTest.php b/tests/src/Util/EMailerTest.php index 6432d58008..21c7fbabc2 100644 --- a/tests/src/Util/EMailerTest.php +++ b/tests/src/Util/EMailerTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; @@ -26,7 +12,7 @@ use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; use Friendica\Object\EMail\IEmail; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\EmailerSpy; use Friendica\Test\Util\HookMockTrait; use Friendica\Test\Util\SampleMailBuilder; @@ -40,7 +26,7 @@ use Psr\Log\NullLogger; * @runTestsInSeparateProcesses * @preserveGlobalState disabled */ -class EMailerTest extends MockedTest +class EMailerTest extends MockedTestCase { use VFSTrait; use HookMockTrait; diff --git a/tests/src/Util/Emailer/MailBuilderTest.php b/tests/src/Util/Emailer/MailBuilderTest.php index ecfed9f084..36095a5d60 100644 --- a/tests/src/Util/Emailer/MailBuilderTest.php +++ b/tests/src/Util/Emailer/MailBuilderTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util\Emailer; @@ -26,7 +12,7 @@ use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Network\HTTPException\UnprocessableEntityException; use Friendica\Object\EMail\IEmail; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\SampleMailBuilder; use Friendica\Test\Util\VFSTrait; use Friendica\Util\EMailer\MailBuilder; @@ -37,7 +23,7 @@ use Psr\Log\NullLogger; * This class tests the "MailBuilder" (@see MailBuilder ) * Since it's an abstract class and every extended class of it has dependencies, we use a "SampleMailBuilder" (@see SampleMailBuilder ) to make this class work */ -class MailBuilderTest extends MockedTest +class MailBuilderTest extends MockedTestCase { use VFSTrait; diff --git a/tests/src/Util/Emailer/SystemMailBuilderTest.php b/tests/src/Util/Emailer/SystemMailBuilderTest.php index fe3b570968..400f6b66c2 100644 --- a/tests/src/Util/Emailer/SystemMailBuilderTest.php +++ b/tests/src/Util/Emailer/SystemMailBuilderTest.php @@ -1,36 +1,22 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util\Emailer; use Friendica\App\BaseURL; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\VFSTrait; use Friendica\Util\EMailer\MailBuilder; use Friendica\Util\EMailer\SystemMailBuilder; use Psr\Log\NullLogger; -class SystemMailBuilderTest extends MockedTest +class SystemMailBuilderTest extends MockedTestCase { use VFSTrait; diff --git a/tests/src/Util/HTTPInputDataTest.php b/tests/src/Util/HTTPInputDataTest.php index 9a302d64b5..680f24ae2e 100644 --- a/tests/src/Util/HTTPInputDataTest.php +++ b/tests/src/Util/HTTPInputDataTest.php @@ -1,36 +1,21 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Test\Util\HTTPInputDataDouble; -use Friendica\Util\HTTPInputData; /** * Testing HTTPInputData * * @see HTTPInputData */ -class HTTPInputDataTest extends MockedTest +class HTTPInputDataTest extends MockedTestCase { /** * Returns the data stream for the unit test diff --git a/tests/src/Util/HTTPSignatureTest.php b/tests/src/Util/HTTPSignatureTest.php index bc54a93184..feb8bebbd9 100644 --- a/tests/src/Util/HTTPSignatureTest.php +++ b/tests/src/Util/HTTPSignatureTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/tests/src/Util/ImagesTest.php b/tests/src/Util/ImagesTest.php index b9caf4ccaf..b8449d3495 100644 --- a/tests/src/Util/ImagesTest.php +++ b/tests/src/Util/ImagesTest.php @@ -1,34 +1,20 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use Friendica\Test\DiceHttpMockHandlerTrait; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\Images; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Psr7\Response; -class ImagesTest extends MockedTest +class ImagesTest extends MockedTestCase { use DiceHttpMockHandlerTrait; use ArraySubsetAsserts; @@ -96,4 +82,104 @@ class ImagesTest extends MockedTest self::assertArraySubset($assertion, Images::getInfoFromURL($url)); } + + public function dataScalingDimensions() + { + return [ + 'landscape' => [ + 'width' => 640, + 'height' => 480, + 'max' => 320, + 'assertion' => [ + 'width' => 320, + 'height' => 240, + ] + ], + 'wide_landscape' => [ + 'width' => 640, + 'height' => 120, + 'max' => 320, + 'assertion' => [ + 'width' => 320, + 'height' => 60, + ] + ], + 'landscape_round_up' => [ + 'width' => 640, + 'height' => 479, + 'max' => 320, + 'assertion' => [ + 'width' => 320, + 'height' => 240, + ] + ], + 'landscape_zero_height' => [ + 'width' => 640, + 'height' => 1, + 'max' => 160, + 'assertion' => [ + 'width' => 160, + 'height' => 1, + ] + ], + 'portrait' => [ + 'width' => 480, + 'height' => 640, + 'max' => 320, + 'assertion' => [ + 'width' => 240, + 'height' => 320, + ] + ], + // For portrait with aspect ratio <= 16:9, constrain height + 'portrait_16_9' => [ + 'width' => 1080, + 'height' => 1920, + 'max' => 320, + 'assertion' => [ + 'width' => 180, + 'height' => 320, + ] + ], + // For portrait with aspect ratio > 16:9, constrain width + 'portrait_over_16_9_too_wide' => [ + 'width' => 1080, + 'height' => 1921, + 'max' => 320, + 'assertion' => [ + 'width' => 320, + 'height' => 570, + ] + ], + // For portrait with aspect ratio > 16:9, constrain width + 'portrait_over_16_9_not_too_wide' => [ + 'width' => 1080, + 'height' => 1921, + 'max' => 1080, + 'assertion' => [ + 'width' => 1080, + 'height' => 1921, + ] + ], + 'portrait_round_up' => [ + 'width' => 479, + 'height' => 640, + 'max' => 320, + 'assertion' => [ + 'width' => 240, + 'height' => 320, + ] + ], + ]; + } + + /** + * Test the Images::getScalingDimensions() method + * + * @dataProvider dataScalingDimensions + */ + public function testGetScalingDimensions(int $width, int $height, int $max, array $assertion) + { + self::assertArraySubset($assertion, Images::getScalingDimensions($width, $height, $max)); + } } diff --git a/tests/src/Util/JsonLDTest.php b/tests/src/Util/JsonLDTest.php index 39171249bd..7e59d06189 100644 --- a/tests/src/Util/JsonLDTest.php +++ b/tests/src/Util/JsonLDTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/tests/src/Util/NetworkTest.php b/tests/src/Util/NetworkTest.php index 945abf84db..ba15e120bc 100644 --- a/tests/src/Util/NetworkTest.php +++ b/tests/src/Util/NetworkTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/tests/src/Util/ProfilerTest.php b/tests/src/Util/ProfilerTest.php index 9ca2e664c0..6e190175cb 100644 --- a/tests/src/Util/ProfilerTest.php +++ b/tests/src/Util/ProfilerTest.php @@ -1,33 +1,19 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; use Friendica\Core\Config\Capability\IManageConfigValues; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\Profiler; use Mockery\MockInterface; use Psr\Log\LoggerInterface; -class ProfilerTest extends MockedTest +class ProfilerTest extends MockedTestCase { /** * @var LoggerInterface|MockInterface diff --git a/tests/src/Util/Router/FriendicaGroupCountBasedTest.php b/tests/src/Util/Router/FriendicaGroupCountBasedTest.php index 2a2649a9ec..d2440268d5 100644 --- a/tests/src/Util/Router/FriendicaGroupCountBasedTest.php +++ b/tests/src/Util/Router/FriendicaGroupCountBasedTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util\Router; @@ -25,10 +11,10 @@ use FastRoute\DataGenerator\GroupCountBased; use FastRoute\RouteCollector; use FastRoute\RouteParser\Std; use Friendica\Module\Special\Options; -use Friendica\Test\MockedTest; +use Friendica\Test\MockedTestCase; use Friendica\Util\Router\FriendicaGroupCountBased; -class FriendicaGroupCountBasedTest extends MockedTest +class FriendicaGroupCountBasedTest extends MockedTestCase { public function testOptions() { diff --git a/tests/src/Util/StringsTest.php b/tests/src/Util/StringsTest.php index f872267005..8e8c4fc715 100644 --- a/tests/src/Util/StringsTest.php +++ b/tests/src/Util/StringsTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; @@ -214,4 +200,21 @@ class StringsTest extends TestCase self::assertEquals($originalText, $text); } + + public function testCleanTags() + { + $rawTags = 'Open, #Source, Friendica Software; Federation #Fediverse'; + $cleaned = 'federation,fediverse,friendica,open,software,source'; + + self::assertEquals($cleaned, Strings::cleanTags($rawTags)); + } + + public function testgetTagArrayByString() + { + $list = 'Open, #Source, Friendica Software; Federation #Fediverse'; + $tags = ['federation', 'fediverse', 'friendica', 'open', 'software', 'source']; + + self::assertEquals($tags, Strings::getTagArrayByString($list)); + } + } diff --git a/tests/src/Util/TemporalTest.php b/tests/src/Util/TemporalTest.php index 10e3646ae4..b5e3c9ebaf 100644 --- a/tests/src/Util/TemporalTest.php +++ b/tests/src/Util/TemporalTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/tests/src/Util/XmlTest.php b/tests/src/Util/XmlTest.php index 2323c4008a..51df3b155a 100644 --- a/tests/src/Util/XmlTest.php +++ b/tests/src/Util/XmlTest.php @@ -1,23 +1,9 @@ . - * - */ + +// Copyright (C) 2010-2024, the Friendica project +// SPDX-FileCopyrightText: 2010-2024 the Friendica project +// +// SPDX-License-Identifier: AGPL-3.0-or-later namespace Friendica\Test\src\Util; diff --git a/translations.txt.license b/translations.txt.license new file mode 100644 index 0000000000..8a315c7ab7 --- /dev/null +++ b/translations.txt.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2010 - 2024 the Friendica project + +SPDX-License-Identifier: CC0-1.0 diff --git a/update.php b/update.php index ce22a28d14..d9cd656f8d 100644 --- a/update.php +++ b/update.php @@ -1,21 +1,9 @@ . + * SPDX-License-Identifier: AGPL-3.0-or-later * * Automatic post-database structure change updates * @@ -40,8 +28,8 @@ * If you need to run a script before the database update, name the function "pre_update_4712()" */ +use Friendica\Contact\LocalRelationship\Entity\LocalRelationship; use Friendica\Core\Config\ValueObject\Cache; -use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Storage\Capability\ICanReadFromStorage; use Friendica\Core\Storage\Type\Database as DatabaseStorage; @@ -51,6 +39,7 @@ use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; use Friendica\DI; +use Friendica\Model\Attach; use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\ItemURI; @@ -63,21 +52,22 @@ use Friendica\Protocol\Activity; use Friendica\Protocol\Delivery; use Friendica\Security\PermissionSet\Repository\PermissionSet; use Friendica\Util\DateTimeFormat; +use Friendica\Worker\UpdateContact; // Post-update script of PR 5751 function update_1298() { $keys = ['gender', 'marital', 'sexual']; foreach ($keys as $translateKey) { - $allData = DBA::select('profile', ['id', $translateKey]); + $allData = DBA::select('profile', ['id', $translateKey]); $allLangs = DI::l10n()->getAvailableLanguages(); - $success = 0; - $fail = 0; + $success = 0; + $fail = 0; foreach ($allData as $key => $data) { $toTranslate = $data[$translateKey]; if ($toTranslate != '') { foreach ($allLangs as $key => $lang) { - $a = new \stdClass(); + $a = new \stdClass(); $a->strings = []; // First we get the localizations @@ -104,8 +94,8 @@ function update_1298() $fail++; } else { DBA::update('profile', [$translateKey => $key], ['id' => $data['id']]); - Logger::notice('Updated contact', ['action' => 'update', 'contact' => $data['id'], "$translateKey" => $key, - 'was' => $data[$translateKey]]); + DI::logger()->notice('Updated contact', ['action' => 'update', 'contact' => $data['id'], "$translateKey" => $key, + 'was' => $data[$translateKey]]); Contact::updateSelfFromUserID($data['id']); Profile::publishUpdate($data['id']); @@ -114,7 +104,7 @@ function update_1298() } } - Logger::notice($translateKey . ' fix completed', ['action' => 'update', 'translateKey' => $translateKey, 'Success' => $success, 'Fail' => $fail ]); + DI::logger()->notice($translateKey . ' fix completed', ['action' => 'update', 'translateKey' => $translateKey, 'Success' => $success, 'Fail' => $fail ]); } return Update::SUCCESS; } @@ -135,7 +125,7 @@ function update_1309() $deliver_options = ['priority' => Worker::PRIORITY_MEDIUM, 'dont_fork' => true]; Worker::add($deliver_options, 'Delivery', Delivery::POST, $item['id'], $entry['cid']); - Logger::info('Added delivery worker', ['item' => $item['id'], 'contact' => $entry['cid']]); + DI::logger()->info('Added delivery worker', ['item' => $item['id'], 'contact' => $entry['cid']]); DBA::delete('queue', ['id' => $entry['id']]); } return Update::SUCCESS; @@ -200,7 +190,7 @@ function update_1330() // Update attachments and photos if (!DBA::e("UPDATE `photo` SET `photo`.`backend-class` = SUBSTR(`photo`.`backend-class`, 25) WHERE `photo`.`backend-class` LIKE 'Friendica\\\Model\\\Storage\\\%' ESCAPE '|'") || - !DBA::e("UPDATE `attach` SET `attach`.`backend-class` = SUBSTR(`attach`.`backend-class`, 25) WHERE `attach`.`backend-class` LIKE 'Friendica\\\Model\\\Storage\\\%' ESCAPE '|'")) { + !DBA::e("UPDATE `attach` SET `attach`.`backend-class` = SUBSTR(`attach`.`backend-class`, 25) WHERE `attach`.`backend-class` LIKE 'Friendica\\\Model\\\Storage\\\%' ESCAPE '|'")) { return Update::FAILED; }; @@ -210,7 +200,7 @@ function update_1330() function update_1332() { $condition = ["`is-default` IS NOT NULL"]; - $profiles = DBA::select('profile', [], $condition); + $profiles = DBA::select('profile', [], $condition); while ($profile = DBA::fetch($profiles)) { Profile::migrate($profile); @@ -461,7 +451,7 @@ function pre_update_1364() return Update::FAILED; } - if (!DBA::e("DELETE FROM `push_subscriber` WHERE NOT `uid` IN (SELECT `uid` FROM `user`)")) { + if (DBStructure::existsTable('push_subscriber') && !DBA::e("DELETE FROM `push_subscriber` WHERE NOT `uid` IN (SELECT `uid` FROM `user`)")) { return Update::FAILED; } @@ -660,13 +650,19 @@ function pre_update_1377() function update_1380() { - if (!DBA::e("UPDATE `notify` INNER JOIN `item` ON `item`.`id` = `notify`.`iid` SET `notify`.`uri-id` = `item`.`uri-id` WHERE `notify`.`uri-id` IS NULL AND `notify`.`otype` IN (?, ?)", - Notification\ObjectType::ITEM, Notification\ObjectType::PERSON)) { + if (!DBA::e( + "UPDATE `notify` INNER JOIN `item` ON `item`.`id` = `notify`.`iid` SET `notify`.`uri-id` = `item`.`uri-id` WHERE `notify`.`uri-id` IS NULL AND `notify`.`otype` IN (?, ?)", + Notification\ObjectType::ITEM, + Notification\ObjectType::PERSON + )) { return Update::FAILED; } - if (!DBA::e("UPDATE `notify` INNER JOIN `item` ON `item`.`id` = `notify`.`parent` SET `notify`.`parent-uri-id` = `item`.`uri-id` WHERE `notify`.`parent-uri-id` IS NULL AND `notify`.`otype` IN (?, ?)", - Notification\ObjectType::ITEM, Notification\ObjectType::PERSON)) { + if (!DBA::e( + "UPDATE `notify` INNER JOIN `item` ON `item`.`id` = `notify`.`parent` SET `notify`.`parent-uri-id` = `item`.`uri-id` WHERE `notify`.`parent-uri-id` IS NULL AND `notify`.`otype` IN (?, ?)", + Notification\ObjectType::ITEM, + Notification\ObjectType::PERSON + )) { return Update::FAILED; } @@ -773,7 +769,7 @@ function update_1398() if (!DBA::e("INSERT IGNORE INTO `post-thread` (`uri-id`, `owner-id`, `author-id`, `network`, `created`, `received`, `changed`, `commented`) SELECT `uri-id`, `owner-id`, `author-id`, `network`, `created`, `received`, `changed`, `commented` FROM `thread`")) { - return Update::FAILED; + return Update::FAILED; } if (!DBStructure::existsTable('thread')) { @@ -782,7 +778,7 @@ function update_1398() if (!DBA::e("UPDATE `post-thread-user` INNER JOIN `thread` ON `thread`.`uid` = `post-thread-user`.`uid` AND `thread`.`uri-id` = `post-thread-user`.`uri-id` SET `post-thread-user`.`mention` = `thread`.`mention`")) { - return Update::FAILED; + return Update::FAILED; } return Update::SUCCESS; @@ -794,7 +790,7 @@ function update_1399() SET `post-thread-user`.`contact-id` = `post-user`.`contact-id`, `post-thread-user`.`unseen` = `post-user`.`unseen`, `post-thread-user`.`hidden` = `post-user`.`hidden`, `post-thread-user`.`origin` = `post-user`.`origin`, `post-thread-user`.`psid` = `post-user`.`psid`, `post-thread-user`.`post-user-id` = `post-user`.`id`")) { - return Update::FAILED; + return Update::FAILED; } return Update::SUCCESS; @@ -806,7 +802,7 @@ function update_1400() `created`, `received`, `edited`, `gravity`, `causer-id`, `post-type`, `vid`, `private`, `visible`, `deleted`, `global`) SELECT `uri-id`, `parent-uri-id`, `thr-parent-id`, `owner-id`, `author-id`, `network`, `created`, `received`, `edited`, `gravity`, `causer-id`, `post-type`, `vid`, `private`, `visible`, `deleted`, `global` FROM `item`")) { - return Update::FAILED; + return Update::FAILED; } if (!DBA::e("UPDATE `post-user` INNER JOIN `item` ON `item`.`uri-id` = `post-user`.`uri-id` AND `item`.`uid` = `post-user`.`uid` @@ -946,7 +942,7 @@ function update_1419() { $mails = DBA::select('mail', ['id', 'from-url', 'uri', 'parent-uri', 'guid'], [], ['order' => ['id']]); while ($mail = DBA::fetch($mails)) { - $fields = []; + $fields = []; $fields['author-id'] = Contact::getIdForURL($mail['from-url'], 0, false); if (empty($fields['author-id'])) { continue; @@ -1025,7 +1021,7 @@ function update_1439() if (!empty($fcontact['url'])) { $id = Contact::getIdForURL($fcontact['url']); if (!empty($id)) { - DBA::update('intro',['suggest-cid' => $id], ['id' => $intro['id']]); + DBA::update('intro', ['suggest-cid' => $id], ['id' => $intro['id']]); } } } @@ -1086,7 +1082,7 @@ function update_1444() function update_1446() { $distributed_cache_driver_source = DI::config()->getCache()->getSource('system', 'distributed_cache_driver'); - $cache_driver_source = DI::config()->getCache()->getSource('system', 'cache_driver'); + $cache_driver_source = DI::config()->getCache()->getSource('system', 'cache_driver'); // In case the distributed cache driver is the default value, but the current cache driver isn't default, // we assume that the distributed cache driver should be the same as the current cache driver @@ -1132,7 +1128,7 @@ function update_1481() function update_1491() { - DBA::update('contact', ['remote_self' => Contact::MIRROR_OWN_POST], ['remote_self' => Contact::MIRROR_FORWARDED]); + DBA::update('contact', ['remote_self' => LocalRelationship::MIRROR_OWN_POST], ['remote_self' => 1]); return Update::SUCCESS; } @@ -1206,7 +1202,7 @@ function update_1509() foreach ($addons as $addon) { $newConfig->set('addons', $addon['name'], [ 'last_update' => $addon['timestamp'], - 'admin' => (bool)$addon['plugin_admin'], + 'admin' => (bool)$addon['plugin_admin'], ]); } @@ -1261,7 +1257,7 @@ function update_1514() if (file_exists(dirname(__FILE__) . '/config/node.config.php')) { $transactionalConfig = DI::config()->beginTransaction(); - $oldConfig = include dirname(__FILE__) . '/config/node.config.php'; + $oldConfig = include dirname(__FILE__) . '/config/node.config.php'; if (is_array($oldConfig)) { $categories = array_keys($oldConfig); @@ -1397,7 +1393,7 @@ function update_1535() DI::config()->set('system', 'compute_circle_counts', true); } DI::config()->delete('system', 'compute_group_counts'); - + return Update::SUCCESS; } @@ -1410,4 +1406,129 @@ function update_1539() DBA::close($users); return Update::SUCCESS; -} \ No newline at end of file +} + +function pre_update_1550() +{ + if (DBStructure::existsTable('post-engagement') && DBStructure::existsColumn('post-engagement', ['language'])) { + DBA::e("ALTER TABLE `post-engagement` DROP `language`"); + } + if (DBStructure::existsTable('post-searchindex') && DBStructure::existsColumn('post-searchindex', ['network'])) { + DBA::e("ALTER TABLE `post-searchindex` DROP `network`, DROP `private`"); + } + return Update::SUCCESS; +} + +function update_1552() +{ + DBA::e("UPDATE `post-content` INNER JOIN `post-tag` ON `post-tag`.`uri-id` = `post-content`.`uri-id` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` SET `sensitive` = ? WHERE `name` = ?", true, 'nsfw'); + + return Update::SUCCESS; +} + +function update_1554() +{ + DBA::e("UPDATE `post-engagement` INNER JOIN `post` ON `post`.`uri-id` = `post-engagement`.`uri-id` SET `post-engagement`.`network` = `post`.`network`"); + + return Update::SUCCESS; +} + +function update_1556() +{ + $users = DBA::select('user', ['uid'], ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]); + while ($user = DBA::fetch($users)) { + Worker::add(Worker::PRIORITY_LOW, 'ProfileUpdate', $user['uid']); + } + DBA::close($users); + + return Update::SUCCESS; +} + +function update_1557() +{ + $contacts = DBA::select('account-view', ['id'], ['platform' => 'friendica', 'contact-type' => Contact::TYPE_RELAY]); + while ($contact = DBA::fetch($contacts)) { + UpdateContact::add(Worker::PRIORITY_LOW, $contact['id']); + } + DBA::close($contacts); + return Update::SUCCESS; +} + +function update_1560() +{ + if (!DBA::e("INSERT IGNORE INTO `post-origin`(`id`, `uri-id`, `uid`, `parent-uri-id`, `thr-parent-id`, `created`, `received`, `gravity`, `vid`, `private`, `wall`) + SELECT `id`, `uri-id`, `uid`, `parent-uri-id`, `thr-parent-id`, `created`, `received`, `gravity`, `vid`, `private`, `wall` FROM `post-user` WHERE `post-user`.`origin` AND `post-user`.`uid` != ?", 0)) { + return Update::FAILED; + } +} + +function update_1564() +{ + $users = DBA::select('user', ['uid'], ['blocked' => true]); + while ($user = DBA::fetch($users)) { + User::block($user['uid']); + } + DBA::close($users); + + return Update::SUCCESS; +} + +function update_1566() +{ + $users = DBA::select('user', ['uid'], ["`account-type` = ? AND `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` AND `uid` > ?", User::ACCOUNT_TYPE_RELAY, 0]); + while ($user = DBA::fetch($users)) { + Profile::setResponsibleRelayContact($user['uid']); + } + DBA::close($users); +} + +function update_1571() +{ + $profiles = DBA::select('profile', ['uid', 'homepage', 'xmpp', 'matrix']); + while ($profile = DBA::fetch($profiles)) { + $homepage = str_replace(['<', '>', '"', ' '], '', $profile['homepage']); + $xmpp = str_replace(['<', '>', '"', ' '], '', $profile['xmpp']); + $matrix = str_replace(['<', '>', '"', ' '], '', $profile['matrix']); + + $fields = []; + if ($homepage != $profile['homepage']) { + $fields['homepage'] = $homepage; + } + if ($xmpp != $profile['xmpp']) { + $fields['xmpp'] = $xmpp; + } + if ($matrix != $profile['matrix']) { + $fields['matrix'] = $matrix; + } + if (!empty($fields)) { + Profile::update($fields, $profile['uid']); + } + } + DBA::close($profiles); + + return Update::SUCCESS; +} + +function update_1573() +{ + $postmedia = DBA::select('post-media', ['id', 'url'], ["`url` LIKE ?", '%/attach/%']); + while ($media = DBA::fetch($postmedia)) { + if (!DI::baseUrl()->isLocalUrl($media['url'])) { + continue; + } + if (preg_match('|.*?/attach/(\d+)|', $media['url'], $matches)) { + $attachment = Attach::selectFirst(['id', 'filename', 'filetype', 'filesize'], ['id' => $matches[1]]); + if (!empty($attachment)) { + $fields = [ + 'attach-id' => $attachment['id'], + 'name' => $attachment['filename'], + 'mimetype' => $attachment['filetype'], + 'size' => $attachment['filesize'], + ]; + DBA::update('post-media', $fields, ['id' => $media['id']]); + } + } + } + DBA::close($media); + return Update::SUCCESS; +} diff --git a/view/.htaccess b/view/.htaccess index 6871f09274..0ee2906ae0 100644 --- a/view/.htaccess +++ b/view/.htaccess @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2010-2024 the Friendica project +# +# SPDX-License-Identifier: CC0-1.0 + #Apache 2.4 diff --git a/view/global.css b/view/global.css index aab6e8d67d..0215e6d4df 100644 --- a/view/global.css +++ b/view/global.css @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2010-2024 the Friendica project + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + /* General style rules .*/ .pull-right { float: right } @@ -749,3 +755,61 @@ figure.img-allocated-height img{ width: 48px; height: 48px; } + +/** + * Log levels colorized in the admin panel + **/ +.loglevel-debug { +} +.loglevel-info { + color: #0f009f; /* blue */ +} +.loglevel-notice { + color: #007e01; /* green */ +} +.loglevel-warning { + color: #de9600; /* dark orange */ + font-weight: bold; +} +.loglevel-error { + color: #ff0000; /* red */ + font-weight: bold; +} +.loglevel-critical { + color: #731289; /* purple */ + font-weight: bold; +} +.loglevel-alert { + color: #ff0000; /* red */ + font-weight: bold; + font-style: italic; +} +.loglevel-emergency { + color: #731289; /* purple */ + font-weight: bold; + font-style: italic; +} + +summary.wall-item-summary { + font-weight: bold; + font-style: oblique; + padding-bottom: 5px; +} + +/* css instructions notification.tpl */ +/* Flexbox layout to align the icon and text in a single line */ +.notif-item a { + display: flex; + align-items: flex-start; +} + +/* Margin to create space between the icon and the text */ +.notif-image { + margin-right: 10px; + +/* Styles to ensure the text wraps properly after 70 characters */ +.notif-text { + display: inline-block; + max-width: 70ch; + overflow-wrap: break-word; +} diff --git a/view/install/style.css b/view/install/style.css index ab37a50555..8cc9b853d3 100644 --- a/view/install/style.css +++ b/view/install/style.css @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2010-2024 the Friendica project + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + /*** * Friendica Communication Server * diff --git a/view/js/ajaxupload.js b/view/js/ajaxupload.js index ebbbfda45c..8376e1a81d 100644 --- a/view/js/ajaxupload.js +++ b/view/js/ajaxupload.js @@ -1,10 +1,9 @@ // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat -/** - * AJAX Upload ( http://valums.com/ajax-upload/ ) - * Copyright (c) Andris Valums - * Licensed under the MIT license ( http://valums.com/mit-license/ ) - * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions. - */ + +// Copyright (c) Andris Valums +// SPDX-FileCopyrightText: Andris Valums +// +// SPDX-License-Identifier: MIT (function () { /* global window */ diff --git a/view/js/autocomplete.js b/view/js/autocomplete.js index dc649084af..fd5b08e9cc 100644 --- a/view/js/autocomplete.js +++ b/view/js/autocomplete.js @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2024 Yuku Takahashi +// +// SPDX-License-Identifier: MIT + // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat /** * Friendica people autocomplete @@ -175,7 +179,7 @@ function listNewLineAutocomplete(id) { if (word != null) { var textBefore = text.value.substring(0, caretPos); var textAfter = text.value.substring(caretPos, text.length); - $('#' + id).val(textBefore + '\r\n[*] ' + textAfter).trigger('change'); + $('#' + id).val(textBefore + '\r\n[li] ' + textAfter).trigger('change'); setCaretPosition(text, caretPos + 5); return true; } @@ -384,7 +388,7 @@ function string2bb(element) { element = string2bb(element); if(open_elements.indexOf(element) < 0) { if(element === 'list' || element === 'ol' || element === 'ul') { - return ['\[' + element + '\]' + '\n\[*\] ', '\n\[/' + element + '\]']; + return ['\[' + element + '\]' + '\n\[li\] ', '\n\[/' + element + '\]']; } else if(element === 'table') { return ['\[' + element + '\]' + '\n\[tr\]', '\[/tr\]\n\[/' + element + '\]']; diff --git a/view/js/country.js b/view/js/country.js index 65497fdbfc..06b77ae37f 100644 --- a/view/js/country.js +++ b/view/js/country.js @@ -1,5 +1,10 @@ // + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset(input, keepLocalTime, keepMinutes) { + var offset = this._offset || 0, + localAdjust; + if (!this.isValid()) { + return input != null ? this : NaN; + } + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(matchShortOffset, input); + if (input === null) { + return this; + } + } else if (Math.abs(input) < 16 && !keepMinutes) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addSubtract( + this, + createDuration(input - offset, 'm'), + 1, + false + ); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } + } + + function getSetZone(input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } + } + + function setOffsetToUTC(keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } + + function setOffsetToLocal(keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; + } + + function setOffsetToParsedOffset() { + if (this._tzm != null) { + this.utcOffset(this._tzm, false, true); + } else if (typeof this._i === 'string') { + var tZone = offsetFromString(matchOffset, this._i); + if (tZone != null) { + this.utcOffset(tZone); + } else { + this.utcOffset(0, true); + } + } + return this; + } + + function hasAlignedHourOffset(input) { + if (!this.isValid()) { + return false; + } + input = input ? createLocal(input).utcOffset() : 0; + + return (this.utcOffset() - input) % 60 === 0; + } + + function isDaylightSavingTime() { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } + + function isDaylightSavingTimeShifted() { + if (!isUndefined(this._isDSTShifted)) { + return this._isDSTShifted; + } + + var c = {}, + other; + + copyConfig(c, this); + c = prepareConfig(c); + + if (c._a) { + other = c._isUTC ? createUTC(c._a) : createLocal(c._a); + this._isDSTShifted = + this.isValid() && compareArrays(c._a, other.toArray()) > 0; + } else { + this._isDSTShifted = false; + } + + return this._isDSTShifted; + } + + function isLocal() { + return this.isValid() ? !this._isUTC : false; + } + + function isUtcOffset() { + return this.isValid() ? this._isUTC : false; + } + + function isUtc() { + return this.isValid() ? this._isUTC && this._offset === 0 : false; + } + + // ASP.NET json date format regex + var aspNetRegex = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/, + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + // and further modified to allow for strings containing both week and day + isoRegex = + /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; + + function createDuration(input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; + + if (isDuration(input)) { + duration = { + ms: input._milliseconds, + d: input._days, + M: input._months, + }; + } else if (isNumber(input) || !isNaN(+input)) { + duration = {}; + if (key) { + duration[key] = +input; + } else { + duration.milliseconds = +input; + } + } else if ((match = aspNetRegex.exec(input))) { + sign = match[1] === '-' ? -1 : 1; + duration = { + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match + }; + } else if ((match = isoRegex.exec(input))) { + sign = match[1] === '-' ? -1 : 1; + duration = { + y: parseIso(match[2], sign), + M: parseIso(match[3], sign), + w: parseIso(match[4], sign), + d: parseIso(match[5], sign), + h: parseIso(match[6], sign), + m: parseIso(match[7], sign), + s: parseIso(match[8], sign), + }; + } else if (duration == null) { + // checks for null or undefined + duration = {}; + } else if ( + typeof duration === 'object' && + ('from' in duration || 'to' in duration) + ) { + diffRes = momentsDifference( + createLocal(duration.from), + createLocal(duration.to) + ); + + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } + + ret = new Duration(duration); + + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } + + if (isDuration(input) && hasOwnProp(input, '_isValid')) { + ret._isValid = input._isValid; + } + + return ret; + } + + createDuration.fn = Duration.prototype; + createDuration.invalid = createInvalid$1; + + function parseIso(inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + } + + function positiveMomentsDifference(base, other) { + var res = {}; + + res.months = + other.month() - base.month() + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } + + res.milliseconds = +other - +base.clone().add(res.months, 'M'); + + return res; + } + + function momentsDifference(base, other) { + var res; + if (!(base.isValid() && other.isValid())) { + return { milliseconds: 0, months: 0 }; + } + + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } + + return res; + } + + // TODO: remove 'name' arg after deprecation is removed + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple( + name, + 'moment().' + + name + + '(period, number) is deprecated. Please use moment().' + + name + + '(number, period). ' + + 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.' + ); + tmp = val; + val = period; + period = tmp; + } + + dur = createDuration(val, period); + addSubtract(this, dur, direction); + return this; + }; + } + + function addSubtract(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = absRound(duration._days), + months = absRound(duration._months); + + if (!mom.isValid()) { + // No op + return; + } + + updateOffset = updateOffset == null ? true : updateOffset; + + if (months) { + setMonth(mom, get(mom, 'Month') + months * isAdding); + } + if (days) { + set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); + } + if (milliseconds) { + mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); + } + if (updateOffset) { + hooks.updateOffset(mom, days || months); + } + } + + var add = createAdder(1, 'add'), + subtract = createAdder(-1, 'subtract'); + + function isString(input) { + return typeof input === 'string' || input instanceof String; + } + + // type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined + function isMomentInput(input) { + return ( + isMoment(input) || + isDate(input) || + isString(input) || + isNumber(input) || + isNumberOrStringArray(input) || + isMomentInputObject(input) || + input === null || + input === undefined + ); + } + + function isMomentInputObject(input) { + var objectTest = isObject(input) && !isObjectEmpty(input), + propertyTest = false, + properties = [ + 'years', + 'year', + 'y', + 'months', + 'month', + 'M', + 'days', + 'day', + 'd', + 'dates', + 'date', + 'D', + 'hours', + 'hour', + 'h', + 'minutes', + 'minute', + 'm', + 'seconds', + 'second', + 's', + 'milliseconds', + 'millisecond', + 'ms', + ], + i, + property, + propertyLen = properties.length; + + for (i = 0; i < propertyLen; i += 1) { + property = properties[i]; + propertyTest = propertyTest || hasOwnProp(input, property); + } + + return objectTest && propertyTest; + } + + function isNumberOrStringArray(input) { + var arrayTest = isArray(input), + dataTypeTest = false; + if (arrayTest) { + dataTypeTest = + input.filter(function (item) { + return !isNumber(item) && isString(input); + }).length === 0; + } + return arrayTest && dataTypeTest; + } + + function isCalendarSpec(input) { + var objectTest = isObject(input) && !isObjectEmpty(input), + propertyTest = false, + properties = [ + 'sameDay', + 'nextDay', + 'lastDay', + 'nextWeek', + 'lastWeek', + 'sameElse', + ], + i, + property; + + for (i = 0; i < properties.length; i += 1) { + property = properties[i]; + propertyTest = propertyTest || hasOwnProp(input, property); + } + + return objectTest && propertyTest; + } + + function getCalendarFormat(myMoment, now) { + var diff = myMoment.diff(now, 'days', true); + return diff < -6 + ? 'sameElse' + : diff < -1 + ? 'lastWeek' + : diff < 0 + ? 'lastDay' + : diff < 1 + ? 'sameDay' + : diff < 2 + ? 'nextDay' + : diff < 7 + ? 'nextWeek' + : 'sameElse'; + } + + function calendar$1(time, formats) { + // Support for single parameter, formats only overload to the calendar function + if (arguments.length === 1) { + if (!arguments[0]) { + time = undefined; + formats = undefined; + } else if (isMomentInput(arguments[0])) { + time = arguments[0]; + formats = undefined; + } else if (isCalendarSpec(arguments[0])) { + formats = arguments[0]; + time = undefined; + } + } + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + format = hooks.calendarFormat(this, sod) || 'sameElse', + output = + formats && + (isFunction(formats[format]) + ? formats[format].call(this, now) + : formats[format]); + + return this.format( + output || this.localeData().calendar(format, this, createLocal(now)) + ); + } + + function clone() { + return new Moment(this); + } + + function isAfter(input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() > localInput.valueOf(); + } else { + return localInput.valueOf() < this.clone().startOf(units).valueOf(); + } + } + + function isBefore(input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() < localInput.valueOf(); + } else { + return this.clone().endOf(units).valueOf() < localInput.valueOf(); + } + } + + function isBetween(from, to, units, inclusivity) { + var localFrom = isMoment(from) ? from : createLocal(from), + localTo = isMoment(to) ? to : createLocal(to); + if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) { + return false; + } + inclusivity = inclusivity || '()'; + return ( + (inclusivity[0] === '(' + ? this.isAfter(localFrom, units) + : !this.isBefore(localFrom, units)) && + (inclusivity[1] === ')' + ? this.isBefore(localTo, units) + : !this.isAfter(localTo, units)) + ); + } + + function isSame(input, units) { + var localInput = isMoment(input) ? input : createLocal(input), + inputMs; + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() === localInput.valueOf(); + } else { + inputMs = localInput.valueOf(); + return ( + this.clone().startOf(units).valueOf() <= inputMs && + inputMs <= this.clone().endOf(units).valueOf() + ); + } + } + + function isSameOrAfter(input, units) { + return this.isSame(input, units) || this.isAfter(input, units); + } + + function isSameOrBefore(input, units) { + return this.isSame(input, units) || this.isBefore(input, units); + } + + function diff(input, units, asFloat) { + var that, zoneDelta, output; + + if (!this.isValid()) { + return NaN; + } + + that = cloneWithOffset(input, this); + + if (!that.isValid()) { + return NaN; + } + + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; + + units = normalizeUnits(units); + + switch (units) { + case 'year': + output = monthDiff(this, that) / 12; + break; + case 'month': + output = monthDiff(this, that); + break; + case 'quarter': + output = monthDiff(this, that) / 3; + break; + case 'second': + output = (this - that) / 1e3; + break; // 1000 + case 'minute': + output = (this - that) / 6e4; + break; // 1000 * 60 + case 'hour': + output = (this - that) / 36e5; + break; // 1000 * 60 * 60 + case 'day': + output = (this - that - zoneDelta) / 864e5; + break; // 1000 * 60 * 60 * 24, negate dst + case 'week': + output = (this - that - zoneDelta) / 6048e5; + break; // 1000 * 60 * 60 * 24 * 7, negate dst + default: + output = this - that; + } + + return asFloat ? output : absFloor(output); + } + + function monthDiff(a, b) { + if (a.date() < b.date()) { + // end-of-month calculations work correct when the start month has more + // days than the end month. + return -monthDiff(b, a); + } + // difference in months + var wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, + adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + //check for negative zero, return zero if negative zero + return -(wholeMonthDiff + adjust) || 0; + } + + hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; + + function toString() { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + } + + function toISOString(keepOffset) { + if (!this.isValid()) { + return null; + } + var utc = keepOffset !== true, + m = utc ? this.clone().utc() : this; + if (m.year() < 0 || m.year() > 9999) { + return formatMoment( + m, + utc + ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' + : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ' + ); + } + if (isFunction(Date.prototype.toISOString)) { + // native implementation is ~50x faster, use it when we can + if (utc) { + return this.toDate().toISOString(); + } else { + return new Date(this.valueOf() + this.utcOffset() * 60 * 1000) + .toISOString() + .replace('Z', formatMoment(m, 'Z')); + } + } + return formatMoment( + m, + utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ' + ); + } + + /** + * Return a human readable representation of a moment that can + * also be evaluated to get a new moment which is the same + * + * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects + */ + function inspect() { + if (!this.isValid()) { + return 'moment.invalid(/* ' + this._i + ' */)'; + } + var func = 'moment', + zone = '', + prefix, + year, + datetime, + suffix; + if (!this.isLocal()) { + func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; + zone = 'Z'; + } + prefix = '[' + func + '("]'; + year = 0 <= this.year() && this.year() <= 9999 ? 'YYYY' : 'YYYYYY'; + datetime = '-MM-DD[T]HH:mm:ss.SSS'; + suffix = zone + '[")]'; + + return this.format(prefix + year + datetime + suffix); + } + + function format(inputString) { + if (!inputString) { + inputString = this.isUtc() + ? hooks.defaultFormatUtc + : hooks.defaultFormat; + } + var output = formatMoment(this, inputString); + return this.localeData().postformat(output); + } + + function from(time, withoutSuffix) { + if ( + this.isValid() && + ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) + ) { + return createDuration({ to: this, from: time }) + .locale(this.locale()) + .humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function fromNow(withoutSuffix) { + return this.from(createLocal(), withoutSuffix); + } + + function to(time, withoutSuffix) { + if ( + this.isValid() && + ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) + ) { + return createDuration({ from: this, to: time }) + .locale(this.locale()) + .humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function toNow(withoutSuffix) { + return this.to(createLocal(), withoutSuffix); + } + + // If passed a locale key, it will set the locale for this + // instance. Otherwise, it will return the locale configuration + // variables for this instance. + function locale(key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + } + + var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ); + + function localeData() { + return this._locale; + } + + var MS_PER_SECOND = 1000, + MS_PER_MINUTE = 60 * MS_PER_SECOND, + MS_PER_HOUR = 60 * MS_PER_MINUTE, + MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR; + + // actual modulo - handles negative numbers (for dates before 1970): + function mod$1(dividend, divisor) { + return ((dividend % divisor) + divisor) % divisor; + } + + function localStartOfDate(y, m, d) { + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + return new Date(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return new Date(y, m, d).valueOf(); + } + } + + function utcStartOfDate(y, m, d) { + // Date.UTC remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return Date.UTC(y, m, d); + } + } + + function startOf(units) { + var time, startOfDate; + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + + switch (units) { + case 'year': + time = startOfDate(this.year(), 0, 1); + break; + case 'quarter': + time = startOfDate( + this.year(), + this.month() - (this.month() % 3), + 1 + ); + break; + case 'month': + time = startOfDate(this.year(), this.month(), 1); + break; + case 'week': + time = startOfDate( + this.year(), + this.month(), + this.date() - this.weekday() + ); + break; + case 'isoWeek': + time = startOfDate( + this.year(), + this.month(), + this.date() - (this.isoWeekday() - 1) + ); + break; + case 'day': + case 'date': + time = startOfDate(this.year(), this.month(), this.date()); + break; + case 'hour': + time = this._d.valueOf(); + time -= mod$1( + time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), + MS_PER_HOUR + ); + break; + case 'minute': + time = this._d.valueOf(); + time -= mod$1(time, MS_PER_MINUTE); + break; + case 'second': + time = this._d.valueOf(); + time -= mod$1(time, MS_PER_SECOND); + break; + } + + this._d.setTime(time); + hooks.updateOffset(this, true); + return this; + } + + function endOf(units) { + var time, startOfDate; + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + + switch (units) { + case 'year': + time = startOfDate(this.year() + 1, 0, 1) - 1; + break; + case 'quarter': + time = + startOfDate( + this.year(), + this.month() - (this.month() % 3) + 3, + 1 + ) - 1; + break; + case 'month': + time = startOfDate(this.year(), this.month() + 1, 1) - 1; + break; + case 'week': + time = + startOfDate( + this.year(), + this.month(), + this.date() - this.weekday() + 7 + ) - 1; + break; + case 'isoWeek': + time = + startOfDate( + this.year(), + this.month(), + this.date() - (this.isoWeekday() - 1) + 7 + ) - 1; + break; + case 'day': + case 'date': + time = startOfDate(this.year(), this.month(), this.date() + 1) - 1; + break; + case 'hour': + time = this._d.valueOf(); + time += + MS_PER_HOUR - + mod$1( + time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), + MS_PER_HOUR + ) - + 1; + break; + case 'minute': + time = this._d.valueOf(); + time += MS_PER_MINUTE - mod$1(time, MS_PER_MINUTE) - 1; + break; + case 'second': + time = this._d.valueOf(); + time += MS_PER_SECOND - mod$1(time, MS_PER_SECOND) - 1; + break; + } + + this._d.setTime(time); + hooks.updateOffset(this, true); + return this; + } + + function valueOf() { + return this._d.valueOf() - (this._offset || 0) * 60000; + } + + function unix() { + return Math.floor(this.valueOf() / 1000); + } + + function toDate() { + return new Date(this.valueOf()); + } + + function toArray() { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hour(), + m.minute(), + m.second(), + m.millisecond(), + ]; + } + + function toObject() { + var m = this; + return { + years: m.year(), + months: m.month(), + date: m.date(), + hours: m.hours(), + minutes: m.minutes(), + seconds: m.seconds(), + milliseconds: m.milliseconds(), + }; + } + + function toJSON() { + // new Date(NaN).toJSON() === null + return this.isValid() ? this.toISOString() : null; + } + + function isValid$2() { + return isValid(this); + } + + function parsingFlags() { + return extend({}, getParsingFlags(this)); + } + + function invalidAt() { + return getParsingFlags(this).overflow; + } + + function creationData() { + return { + input: this._i, + format: this._f, + locale: this._locale, + isUTC: this._isUTC, + strict: this._strict, + }; + } + + addFormatToken('N', 0, 0, 'eraAbbr'); + addFormatToken('NN', 0, 0, 'eraAbbr'); + addFormatToken('NNN', 0, 0, 'eraAbbr'); + addFormatToken('NNNN', 0, 0, 'eraName'); + addFormatToken('NNNNN', 0, 0, 'eraNarrow'); + + addFormatToken('y', ['y', 1], 'yo', 'eraYear'); + addFormatToken('y', ['yy', 2], 0, 'eraYear'); + addFormatToken('y', ['yyy', 3], 0, 'eraYear'); + addFormatToken('y', ['yyyy', 4], 0, 'eraYear'); + + addRegexToken('N', matchEraAbbr); + addRegexToken('NN', matchEraAbbr); + addRegexToken('NNN', matchEraAbbr); + addRegexToken('NNNN', matchEraName); + addRegexToken('NNNNN', matchEraNarrow); + + addParseToken( + ['N', 'NN', 'NNN', 'NNNN', 'NNNNN'], + function (input, array, config, token) { + var era = config._locale.erasParse(input, token, config._strict); + if (era) { + getParsingFlags(config).era = era; + } else { + getParsingFlags(config).invalidEra = input; + } + } + ); + + addRegexToken('y', matchUnsigned); + addRegexToken('yy', matchUnsigned); + addRegexToken('yyy', matchUnsigned); + addRegexToken('yyyy', matchUnsigned); + addRegexToken('yo', matchEraYearOrdinal); + + addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR); + addParseToken(['yo'], function (input, array, config, token) { + var match; + if (config._locale._eraYearOrdinalRegex) { + match = input.match(config._locale._eraYearOrdinalRegex); + } + + if (config._locale.eraYearOrdinalParse) { + array[YEAR] = config._locale.eraYearOrdinalParse(input, match); + } else { + array[YEAR] = parseInt(input, 10); + } + }); + + function localeEras(m, format) { + var i, + l, + date, + eras = this._eras || getLocale('en')._eras; + for (i = 0, l = eras.length; i < l; ++i) { + switch (typeof eras[i].since) { + case 'string': + // truncate time + date = hooks(eras[i].since).startOf('day'); + eras[i].since = date.valueOf(); + break; + } + + switch (typeof eras[i].until) { + case 'undefined': + eras[i].until = +Infinity; + break; + case 'string': + // truncate time + date = hooks(eras[i].until).startOf('day').valueOf(); + eras[i].until = date.valueOf(); + break; + } + } + return eras; + } + + function localeErasParse(eraName, format, strict) { + var i, + l, + eras = this.eras(), + name, + abbr, + narrow; + eraName = eraName.toUpperCase(); + + for (i = 0, l = eras.length; i < l; ++i) { + name = eras[i].name.toUpperCase(); + abbr = eras[i].abbr.toUpperCase(); + narrow = eras[i].narrow.toUpperCase(); + + if (strict) { + switch (format) { + case 'N': + case 'NN': + case 'NNN': + if (abbr === eraName) { + return eras[i]; + } + break; + + case 'NNNN': + if (name === eraName) { + return eras[i]; + } + break; + + case 'NNNNN': + if (narrow === eraName) { + return eras[i]; + } + break; + } + } else if ([name, abbr, narrow].indexOf(eraName) >= 0) { + return eras[i]; + } + } + } + + function localeErasConvertYear(era, year) { + var dir = era.since <= era.until ? +1 : -1; + if (year === undefined) { + return hooks(era.since).year(); + } else { + return hooks(era.since).year() + (year - era.offset) * dir; + } + } + + function getEraName() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].name; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].name; + } + } + + return ''; + } + + function getEraNarrow() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].narrow; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].narrow; + } + } + + return ''; + } + + function getEraAbbr() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].abbr; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].abbr; + } + } + + return ''; + } + + function getEraYear() { + var i, + l, + dir, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + dir = eras[i].since <= eras[i].until ? +1 : -1; + + // truncate time + val = this.clone().startOf('day').valueOf(); + + if ( + (eras[i].since <= val && val <= eras[i].until) || + (eras[i].until <= val && val <= eras[i].since) + ) { + return ( + (this.year() - hooks(eras[i].since).year()) * dir + + eras[i].offset + ); + } + } + + return this.year(); + } + + function erasNameRegex(isStrict) { + if (!hasOwnProp(this, '_erasNameRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasNameRegex : this._erasRegex; + } + + function erasAbbrRegex(isStrict) { + if (!hasOwnProp(this, '_erasAbbrRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasAbbrRegex : this._erasRegex; + } + + function erasNarrowRegex(isStrict) { + if (!hasOwnProp(this, '_erasNarrowRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasNarrowRegex : this._erasRegex; + } + + function matchEraAbbr(isStrict, locale) { + return locale.erasAbbrRegex(isStrict); + } + + function matchEraName(isStrict, locale) { + return locale.erasNameRegex(isStrict); + } + + function matchEraNarrow(isStrict, locale) { + return locale.erasNarrowRegex(isStrict); + } + + function matchEraYearOrdinal(isStrict, locale) { + return locale._eraYearOrdinalRegex || matchUnsigned; + } + + function computeErasParse() { + var abbrPieces = [], + namePieces = [], + narrowPieces = [], + mixedPieces = [], + i, + l, + eras = this.eras(); + + for (i = 0, l = eras.length; i < l; ++i) { + namePieces.push(regexEscape(eras[i].name)); + abbrPieces.push(regexEscape(eras[i].abbr)); + narrowPieces.push(regexEscape(eras[i].narrow)); + + mixedPieces.push(regexEscape(eras[i].name)); + mixedPieces.push(regexEscape(eras[i].abbr)); + mixedPieces.push(regexEscape(eras[i].narrow)); + } + + this._erasRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._erasNameRegex = new RegExp('^(' + namePieces.join('|') + ')', 'i'); + this._erasAbbrRegex = new RegExp('^(' + abbrPieces.join('|') + ')', 'i'); + this._erasNarrowRegex = new RegExp( + '^(' + narrowPieces.join('|') + ')', + 'i' + ); + } + + // FORMATTING + + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); + + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); + + function addWeekYearFormatToken(token, getter) { + addFormatToken(0, [token, token.length], 0, getter); + } + + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + + // ALIASES + + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); + + // PRIORITY + + addUnitPriority('weekYear', 1); + addUnitPriority('isoWeekYear', 1); + + // PARSING + + addRegexToken('G', matchSigned); + addRegexToken('g', matchSigned); + addRegexToken('GG', match1to2, match2); + addRegexToken('gg', match1to2, match2); + addRegexToken('GGGG', match1to4, match4); + addRegexToken('gggg', match1to4, match4); + addRegexToken('GGGGG', match1to6, match6); + addRegexToken('ggggg', match1to6, match6); + + addWeekParseToken( + ['gggg', 'ggggg', 'GGGG', 'GGGGG'], + function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + } + ); + + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = hooks.parseTwoDigitYear(input); + }); + + // MOMENTS + + function getSetWeekYear(input) { + return getSetWeekYearHelper.call( + this, + input, + this.week(), + this.weekday(), + this.localeData()._week.dow, + this.localeData()._week.doy + ); + } + + function getSetISOWeekYear(input) { + return getSetWeekYearHelper.call( + this, + input, + this.isoWeek(), + this.isoWeekday(), + 1, + 4 + ); + } + + function getISOWeeksInYear() { + return weeksInYear(this.year(), 1, 4); + } + + function getISOWeeksInISOWeekYear() { + return weeksInYear(this.isoWeekYear(), 1, 4); + } + + function getWeeksInYear() { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + } + + function getWeeksInWeekYear() { + var weekInfo = this.localeData()._week; + return weeksInYear(this.weekYear(), weekInfo.dow, weekInfo.doy); + } + + function getSetWeekYearHelper(input, week, weekday, dow, doy) { + var weeksTarget; + if (input == null) { + return weekOfYear(this, dow, doy).year; + } else { + weeksTarget = weeksInYear(input, dow, doy); + if (week > weeksTarget) { + week = weeksTarget; + } + return setWeekAll.call(this, input, week, weekday, dow, doy); + } + } + + function setWeekAll(weekYear, week, weekday, dow, doy) { + var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), + date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); + + this.year(date.getUTCFullYear()); + this.month(date.getUTCMonth()); + this.date(date.getUTCDate()); + return this; + } + + // FORMATTING + + addFormatToken('Q', 0, 'Qo', 'quarter'); + + // ALIASES + + addUnitAlias('quarter', 'Q'); + + // PRIORITY + + addUnitPriority('quarter', 7); + + // PARSING + + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); + + // MOMENTS + + function getSetQuarter(input) { + return input == null + ? Math.ceil((this.month() + 1) / 3) + : this.month((input - 1) * 3 + (this.month() % 3)); + } + + // FORMATTING + + addFormatToken('D', ['DD', 2], 'Do', 'date'); + + // ALIASES + + addUnitAlias('date', 'D'); + + // PRIORITY + addUnitPriority('date', 9); + + // PARSING + + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + // TODO: Remove "ordinalParse" fallback in next major release. + return isStrict + ? locale._dayOfMonthOrdinalParse || locale._ordinalParse + : locale._dayOfMonthOrdinalParseLenient; + }); + + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0]); + }); + + // MOMENTS + + var getSetDayOfMonth = makeGetSet('Date', true); + + // FORMATTING + + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + + // ALIASES + + addUnitAlias('dayOfYear', 'DDD'); + + // PRIORITY + addUnitPriority('dayOfYear', 4); + + // PARSING + + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); + + // HELPERS + + // MOMENTS + + function getSetDayOfYear(input) { + var dayOfYear = + Math.round( + (this.clone().startOf('day') - this.clone().startOf('year')) / 864e5 + ) + 1; + return input == null ? dayOfYear : this.add(input - dayOfYear, 'd'); + } + + // FORMATTING + + addFormatToken('m', ['mm', 2], 0, 'minute'); + + // ALIASES + + addUnitAlias('minute', 'm'); + + // PRIORITY + + addUnitPriority('minute', 14); + + // PARSING + + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); + + // MOMENTS + + var getSetMinute = makeGetSet('Minutes', false); + + // FORMATTING + + addFormatToken('s', ['ss', 2], 0, 'second'); + + // ALIASES + + addUnitAlias('second', 's'); + + // PRIORITY + + addUnitPriority('second', 15); + + // PARSING + + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); + + // MOMENTS + + var getSetSecond = makeGetSet('Seconds', false); + + // FORMATTING + + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); + }); + + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); + + addFormatToken(0, ['SSS', 3], 0, 'millisecond'); + addFormatToken(0, ['SSSS', 4], 0, function () { + return this.millisecond() * 10; + }); + addFormatToken(0, ['SSSSS', 5], 0, function () { + return this.millisecond() * 100; + }); + addFormatToken(0, ['SSSSSS', 6], 0, function () { + return this.millisecond() * 1000; + }); + addFormatToken(0, ['SSSSSSS', 7], 0, function () { + return this.millisecond() * 10000; + }); + addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + return this.millisecond() * 100000; + }); + addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + return this.millisecond() * 1000000; + }); + + // ALIASES + + addUnitAlias('millisecond', 'ms'); + + // PRIORITY + + addUnitPriority('millisecond', 16); + + // PARSING + + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + + var token, getSetMillisecond; + for (token = 'SSSS'; token.length <= 9; token += 'S') { + addRegexToken(token, matchUnsigned); + } + + function parseMs(input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); + } + + for (token = 'S'; token.length <= 9; token += 'S') { + addParseToken(token, parseMs); + } + + getSetMillisecond = makeGetSet('Milliseconds', false); + + // FORMATTING + + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); + + // MOMENTS + + function getZoneAbbr() { + return this._isUTC ? 'UTC' : ''; + } + + function getZoneName() { + return this._isUTC ? 'Coordinated Universal Time' : ''; + } + + var proto = Moment.prototype; + + proto.add = add; + proto.calendar = calendar$1; + proto.clone = clone; + proto.diff = diff; + proto.endOf = endOf; + proto.format = format; + proto.from = from; + proto.fromNow = fromNow; + proto.to = to; + proto.toNow = toNow; + proto.get = stringGet; + proto.invalidAt = invalidAt; + proto.isAfter = isAfter; + proto.isBefore = isBefore; + proto.isBetween = isBetween; + proto.isSame = isSame; + proto.isSameOrAfter = isSameOrAfter; + proto.isSameOrBefore = isSameOrBefore; + proto.isValid = isValid$2; + proto.lang = lang; + proto.locale = locale; + proto.localeData = localeData; + proto.max = prototypeMax; + proto.min = prototypeMin; + proto.parsingFlags = parsingFlags; + proto.set = stringSet; + proto.startOf = startOf; + proto.subtract = subtract; + proto.toArray = toArray; + proto.toObject = toObject; + proto.toDate = toDate; + proto.toISOString = toISOString; + proto.inspect = inspect; + if (typeof Symbol !== 'undefined' && Symbol.for != null) { + proto[Symbol.for('nodejs.util.inspect.custom')] = function () { + return 'Moment<' + this.format() + '>'; + }; + } + proto.toJSON = toJSON; + proto.toString = toString; + proto.unix = unix; + proto.valueOf = valueOf; + proto.creationData = creationData; + proto.eraName = getEraName; + proto.eraNarrow = getEraNarrow; + proto.eraAbbr = getEraAbbr; + proto.eraYear = getEraYear; + proto.year = getSetYear; + proto.isLeapYear = getIsLeapYear; + proto.weekYear = getSetWeekYear; + proto.isoWeekYear = getSetISOWeekYear; + proto.quarter = proto.quarters = getSetQuarter; + proto.month = getSetMonth; + proto.daysInMonth = getDaysInMonth; + proto.week = proto.weeks = getSetWeek; + proto.isoWeek = proto.isoWeeks = getSetISOWeek; + proto.weeksInYear = getWeeksInYear; + proto.weeksInWeekYear = getWeeksInWeekYear; + proto.isoWeeksInYear = getISOWeeksInYear; + proto.isoWeeksInISOWeekYear = getISOWeeksInISOWeekYear; + proto.date = getSetDayOfMonth; + proto.day = proto.days = getSetDayOfWeek; + proto.weekday = getSetLocaleDayOfWeek; + proto.isoWeekday = getSetISODayOfWeek; + proto.dayOfYear = getSetDayOfYear; + proto.hour = proto.hours = getSetHour; + proto.minute = proto.minutes = getSetMinute; + proto.second = proto.seconds = getSetSecond; + proto.millisecond = proto.milliseconds = getSetMillisecond; + proto.utcOffset = getSetOffset; + proto.utc = setOffsetToUTC; + proto.local = setOffsetToLocal; + proto.parseZone = setOffsetToParsedOffset; + proto.hasAlignedHourOffset = hasAlignedHourOffset; + proto.isDST = isDaylightSavingTime; + proto.isLocal = isLocal; + proto.isUtcOffset = isUtcOffset; + proto.isUtc = isUtc; + proto.isUTC = isUtc; + proto.zoneAbbr = getZoneAbbr; + proto.zoneName = getZoneName; + proto.dates = deprecate( + 'dates accessor is deprecated. Use date instead.', + getSetDayOfMonth + ); + proto.months = deprecate( + 'months accessor is deprecated. Use month instead', + getSetMonth + ); + proto.years = deprecate( + 'years accessor is deprecated. Use year instead', + getSetYear + ); + proto.zone = deprecate( + 'moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', + getSetZone + ); + proto.isDSTShifted = deprecate( + 'isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', + isDaylightSavingTimeShifted + ); + + function createUnix(input) { + return createLocal(input * 1000); + } + + function createInZone() { + return createLocal.apply(null, arguments).parseZone(); + } + + function preParsePostFormat(string) { + return string; + } + + var proto$1 = Locale.prototype; + + proto$1.calendar = calendar; + proto$1.longDateFormat = longDateFormat; + proto$1.invalidDate = invalidDate; + proto$1.ordinal = ordinal; + proto$1.preparse = preParsePostFormat; + proto$1.postformat = preParsePostFormat; + proto$1.relativeTime = relativeTime; + proto$1.pastFuture = pastFuture; + proto$1.set = set; + proto$1.eras = localeEras; + proto$1.erasParse = localeErasParse; + proto$1.erasConvertYear = localeErasConvertYear; + proto$1.erasAbbrRegex = erasAbbrRegex; + proto$1.erasNameRegex = erasNameRegex; + proto$1.erasNarrowRegex = erasNarrowRegex; + + proto$1.months = localeMonths; + proto$1.monthsShort = localeMonthsShort; + proto$1.monthsParse = localeMonthsParse; + proto$1.monthsRegex = monthsRegex; + proto$1.monthsShortRegex = monthsShortRegex; + proto$1.week = localeWeek; + proto$1.firstDayOfYear = localeFirstDayOfYear; + proto$1.firstDayOfWeek = localeFirstDayOfWeek; + + proto$1.weekdays = localeWeekdays; + proto$1.weekdaysMin = localeWeekdaysMin; + proto$1.weekdaysShort = localeWeekdaysShort; + proto$1.weekdaysParse = localeWeekdaysParse; + + proto$1.weekdaysRegex = weekdaysRegex; + proto$1.weekdaysShortRegex = weekdaysShortRegex; + proto$1.weekdaysMinRegex = weekdaysMinRegex; + + proto$1.isPM = localeIsPM; + proto$1.meridiem = localeMeridiem; + + function get$1(format, index, field, setter) { + var locale = getLocale(), + utc = createUTC().set(setter, index); + return locale[field](utc, format); + } + + function listMonthsImpl(format, index, field) { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + + if (index != null) { + return get$1(format, index, field, 'month'); + } + + var i, + out = []; + for (i = 0; i < 12; i++) { + out[i] = get$1(format, i, field, 'month'); + } + return out; + } + + // () + // (5) + // (fmt, 5) + // (fmt) + // (true) + // (true, 5) + // (true, fmt, 5) + // (true, fmt) + function listWeekdaysImpl(localeSorted, format, index, field) { + if (typeof localeSorted === 'boolean') { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } else { + format = localeSorted; + index = format; + localeSorted = false; + + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } + + var locale = getLocale(), + shift = localeSorted ? locale._week.dow : 0, + i, + out = []; + + if (index != null) { + return get$1(format, (index + shift) % 7, field, 'day'); + } + + for (i = 0; i < 7; i++) { + out[i] = get$1(format, (i + shift) % 7, field, 'day'); + } + return out; + } + + function listMonths(format, index) { + return listMonthsImpl(format, index, 'months'); + } + + function listMonthsShort(format, index) { + return listMonthsImpl(format, index, 'monthsShort'); + } + + function listWeekdays(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); + } + + function listWeekdaysShort(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); + } + + function listWeekdaysMin(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); + } + + getSetGlobalLocale('en', { + eras: [ + { + since: '0001-01-01', + until: +Infinity, + offset: 1, + name: 'Anno Domini', + narrow: 'AD', + abbr: 'AD', + }, + { + since: '0000-12-31', + until: -Infinity, + offset: 1, + name: 'Before Christ', + narrow: 'BC', + abbr: 'BC', + }, + ], + dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal: function (number) { + var b = number % 10, + output = + toInt((number % 100) / 10) === 1 + ? 'th' + : b === 1 + ? 'st' + : b === 2 + ? 'nd' + : b === 3 + ? 'rd' + : 'th'; + return number + output; + }, + }); + + // Side effect imports + + hooks.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + getSetGlobalLocale + ); + hooks.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + getLocale + ); + + var mathAbs = Math.abs; + + function abs() { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; + } + + function addSubtract$1(duration, input, value, direction) { + var other = createDuration(input, value); + + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); + } + + // supports only 2.0-style add(1, 's') or add(duration) + function add$1(input, value) { + return addSubtract$1(this, input, value, 1); + } + + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function subtract$1(input, value) { + return addSubtract$1(this, input, value, -1); + } + + function absCeil(number) { + if (number < 0) { + return Math.floor(number); + } else { + return Math.ceil(number); + } + } + + function bubble() { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, + minutes, + hours, + years, + monthsFromDays; + + // if we have a mix of positive and negative values, bubble down first + // check: https://github.com/moment/moment/issues/2166 + if ( + !( + (milliseconds >= 0 && days >= 0 && months >= 0) || + (milliseconds <= 0 && days <= 0 && months <= 0) + ) + ) { + milliseconds += absCeil(monthsToDays(months) + days) * 864e5; + days = 0; + months = 0; + } + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; + + hours = absFloor(minutes / 60); + data.hours = hours % 24; + + days += absFloor(hours / 24); + + // convert days to months + monthsFromDays = absFloor(daysToMonths(days)); + months += monthsFromDays; + days -= absCeil(monthsToDays(monthsFromDays)); + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; + } + + function daysToMonths(days) { + // 400 years have 146097 days (taking into account leap year rules) + // 400 years have 12 months === 4800 + return (days * 4800) / 146097; + } + + function monthsToDays(months) { + // the reverse of daysToMonths + return (months * 146097) / 4800; + } + + function as(units) { + if (!this.isValid()) { + return NaN; + } + var days, + months, + milliseconds = this._milliseconds; + + units = normalizeUnits(units); + + if (units === 'month' || units === 'quarter' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToMonths(days); + switch (units) { + case 'month': + return months; + case 'quarter': + return months / 3; + case 'year': + return months / 12; + } + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(monthsToDays(this._months)); + switch (units) { + case 'week': + return days / 7 + milliseconds / 6048e5; + case 'day': + return days + milliseconds / 864e5; + case 'hour': + return days * 24 + milliseconds / 36e5; + case 'minute': + return days * 1440 + milliseconds / 6e4; + case 'second': + return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': + return Math.floor(days * 864e5) + milliseconds; + default: + throw new Error('Unknown unit ' + units); + } + } + } + + // TODO: Use this.as('ms')? + function valueOf$1() { + if (!this.isValid()) { + return NaN; + } + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } + + function makeAs(alias) { + return function () { + return this.as(alias); + }; + } + + var asMilliseconds = makeAs('ms'), + asSeconds = makeAs('s'), + asMinutes = makeAs('m'), + asHours = makeAs('h'), + asDays = makeAs('d'), + asWeeks = makeAs('w'), + asMonths = makeAs('M'), + asQuarters = makeAs('Q'), + asYears = makeAs('y'); + + function clone$1() { + return createDuration(this); + } + + function get$2(units) { + units = normalizeUnits(units); + return this.isValid() ? this[units + 's']() : NaN; + } + + function makeGetter(name) { + return function () { + return this.isValid() ? this._data[name] : NaN; + }; + } + + var milliseconds = makeGetter('milliseconds'), + seconds = makeGetter('seconds'), + minutes = makeGetter('minutes'), + hours = makeGetter('hours'), + days = makeGetter('days'), + months = makeGetter('months'), + years = makeGetter('years'); + + function weeks() { + return absFloor(this.days() / 7); + } + + var round = Math.round, + thresholds = { + ss: 44, // a few seconds to seconds + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month/week + w: null, // weeks to month + M: 11, // months to year + }; + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) { + var duration = createDuration(posNegDuration).abs(), + seconds = round(duration.as('s')), + minutes = round(duration.as('m')), + hours = round(duration.as('h')), + days = round(duration.as('d')), + months = round(duration.as('M')), + weeks = round(duration.as('w')), + years = round(duration.as('y')), + a = + (seconds <= thresholds.ss && ['s', seconds]) || + (seconds < thresholds.s && ['ss', seconds]) || + (minutes <= 1 && ['m']) || + (minutes < thresholds.m && ['mm', minutes]) || + (hours <= 1 && ['h']) || + (hours < thresholds.h && ['hh', hours]) || + (days <= 1 && ['d']) || + (days < thresholds.d && ['dd', days]); + + if (thresholds.w != null) { + a = + a || + (weeks <= 1 && ['w']) || + (weeks < thresholds.w && ['ww', weeks]); + } + a = a || + (months <= 1 && ['M']) || + (months < thresholds.M && ['MM', months]) || + (years <= 1 && ['y']) || ['yy', years]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); + } + + // This function allows you to set the rounding function for relative time strings + function getSetRelativeTimeRounding(roundingFunction) { + if (roundingFunction === undefined) { + return round; + } + if (typeof roundingFunction === 'function') { + round = roundingFunction; + return true; + } + return false; + } + + // This function allows you to set a threshold for relative time strings + function getSetRelativeTimeThreshold(threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + if (threshold === 's') { + thresholds.ss = limit - 1; + } + return true; + } + + function humanize(argWithSuffix, argThresholds) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var withSuffix = false, + th = thresholds, + locale, + output; + + if (typeof argWithSuffix === 'object') { + argThresholds = argWithSuffix; + argWithSuffix = false; + } + if (typeof argWithSuffix === 'boolean') { + withSuffix = argWithSuffix; + } + if (typeof argThresholds === 'object') { + th = Object.assign({}, thresholds, argThresholds); + if (argThresholds.s != null && argThresholds.ss == null) { + th.ss = argThresholds.s - 1; + } + } + + locale = this.localeData(); + output = relativeTime$1(this, !withSuffix, th, locale); + + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); + } + + var abs$1 = Math.abs; + + function sign(x) { + return (x > 0) - (x < 0) || +x; + } + + function toISOString$1() { + // for ISO strings we do not use the normal bubbling rules: + // * milliseconds bubble up until they become hours + // * days do not bubble at all + // * months bubble up until they become years + // This is because there is no context-free conversion between hours and days + // (think of clock changes) + // and also not between days and months (28-31 days per month) + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var seconds = abs$1(this._milliseconds) / 1000, + days = abs$1(this._days), + months = abs$1(this._months), + minutes, + hours, + years, + s, + total = this.asSeconds(), + totalSign, + ymSign, + daysSign, + hmsSign; + + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + // 3600 seconds -> 60 minutes -> 1 hour + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); + seconds %= 60; + minutes %= 60; + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; + + totalSign = total < 0 ? '-' : ''; + ymSign = sign(this._months) !== sign(total) ? '-' : ''; + daysSign = sign(this._days) !== sign(total) ? '-' : ''; + hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; + + return ( + totalSign + + 'P' + + (years ? ymSign + years + 'Y' : '') + + (months ? ymSign + months + 'M' : '') + + (days ? daysSign + days + 'D' : '') + + (hours || minutes || seconds ? 'T' : '') + + (hours ? hmsSign + hours + 'H' : '') + + (minutes ? hmsSign + minutes + 'M' : '') + + (seconds ? hmsSign + s + 'S' : '') + ); + } + + var proto$2 = Duration.prototype; + + proto$2.isValid = isValid$1; + proto$2.abs = abs; + proto$2.add = add$1; + proto$2.subtract = subtract$1; + proto$2.as = as; + proto$2.asMilliseconds = asMilliseconds; + proto$2.asSeconds = asSeconds; + proto$2.asMinutes = asMinutes; + proto$2.asHours = asHours; + proto$2.asDays = asDays; + proto$2.asWeeks = asWeeks; + proto$2.asMonths = asMonths; + proto$2.asQuarters = asQuarters; + proto$2.asYears = asYears; + proto$2.valueOf = valueOf$1; + proto$2._bubble = bubble; + proto$2.clone = clone$1; + proto$2.get = get$2; + proto$2.milliseconds = milliseconds; + proto$2.seconds = seconds; + proto$2.minutes = minutes; + proto$2.hours = hours; + proto$2.days = days; + proto$2.weeks = weeks; + proto$2.months = months; + proto$2.years = years; + proto$2.humanize = humanize; + proto$2.toISOString = toISOString$1; + proto$2.toString = toISOString$1; + proto$2.toJSON = toISOString$1; + proto$2.locale = locale; + proto$2.localeData = localeData; + + proto$2.toIsoString = deprecate( + 'toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', + toISOString$1 + ); + proto$2.lang = lang; + + // FORMATTING + + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); + + // PARSING + + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); + + //! moment.js + + hooks.version = '2.29.4'; + + setHookCallback(createLocal); + + hooks.fn = proto; + hooks.min = min; + hooks.max = max; + hooks.now = now; + hooks.utc = createUTC; + hooks.unix = createUnix; + hooks.months = listMonths; + hooks.isDate = isDate; + hooks.locale = getSetGlobalLocale; + hooks.invalid = createInvalid; + hooks.duration = createDuration; + hooks.isMoment = isMoment; + hooks.weekdays = listWeekdays; + hooks.parseZone = createInZone; + hooks.localeData = getLocale; + hooks.isDuration = isDuration; + hooks.monthsShort = listMonthsShort; + hooks.weekdaysMin = listWeekdaysMin; + hooks.defineLocale = defineLocale; + hooks.updateLocale = updateLocale; + hooks.locales = listLocales; + hooks.weekdaysShort = listWeekdaysShort; + hooks.normalizeUnits = normalizeUnits; + hooks.relativeTimeRounding = getSetRelativeTimeRounding; + hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; + hooks.calendarFormat = getCalendarFormat; + hooks.prototype = proto; + + // currently HTML5 input type only supports 24-hour formats + hooks.HTML5_FMT = { + DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // + DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // + DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // + DATE: 'YYYY-MM-DD', // + TIME: 'HH:mm', // + TIME_SECONDS: 'HH:mm:ss', // + TIME_MS: 'HH:mm:ss.SSS', // + WEEK: 'GGGG-[W]WW', // + MONTH: 'YYYY-MM', // + }; + + return hooks; + + }))); + } (moment)); + return moment.exports; + } + + /*! + * Chart.js v2.9.4 + * https://www.chartjs.org + * (c) 2020 Chart.js Contributors + * Released under the MIT License + */ + + (function (module, exports) { + (function (global, factory) { + module.exports = factory(function() { try { return requireMoment(); } catch(e) { } }()) ; + }(commonjsGlobal, (function (moment) { + moment = moment && moment.hasOwnProperty('default') ? moment['default'] : moment; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + function getCjsExportFromNamespace (n) { + return n && n['default'] || n; + } + + var colorName = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] + }; + + var conversions = createCommonjsModule(function (module) { + /* MIT license */ + + + // NOTE: conversions should only return primitive values (i.e. arrays, or + // values that give correct `typeof` results). + // do not use box values types (i.e. Number(), String(), etc.) + + var reverseKeywords = {}; + for (var key in colorName) { + if (colorName.hasOwnProperty(key)) { + reverseKeywords[colorName[key]] = key; + } + } + + var convert = module.exports = { + rgb: {channels: 3, labels: 'rgb'}, + hsl: {channels: 3, labels: 'hsl'}, + hsv: {channels: 3, labels: 'hsv'}, + hwb: {channels: 3, labels: 'hwb'}, + cmyk: {channels: 4, labels: 'cmyk'}, + xyz: {channels: 3, labels: 'xyz'}, + lab: {channels: 3, labels: 'lab'}, + lch: {channels: 3, labels: 'lch'}, + hex: {channels: 1, labels: ['hex']}, + keyword: {channels: 1, labels: ['keyword']}, + ansi16: {channels: 1, labels: ['ansi16']}, + ansi256: {channels: 1, labels: ['ansi256']}, + hcg: {channels: 3, labels: ['h', 'c', 'g']}, + apple: {channels: 3, labels: ['r16', 'g16', 'b16']}, + gray: {channels: 1, labels: ['gray']} + }; + + // hide .channels and .labels properties + for (var model in convert) { + if (convert.hasOwnProperty(model)) { + if (!('channels' in convert[model])) { + throw new Error('missing channels property: ' + model); + } + + if (!('labels' in convert[model])) { + throw new Error('missing channel labels property: ' + model); + } + + if (convert[model].labels.length !== convert[model].channels) { + throw new Error('channel and label counts mismatch: ' + model); + } + + var channels = convert[model].channels; + var labels = convert[model].labels; + delete convert[model].channels; + delete convert[model].labels; + Object.defineProperty(convert[model], 'channels', {value: channels}); + Object.defineProperty(convert[model], 'labels', {value: labels}); + } + } + + convert.rgb.hsl = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var min = Math.min(r, g, b); + var max = Math.max(r, g, b); + var delta = max - min; + var h; + var s; + var l; + + if (max === min) { + h = 0; + } else if (r === max) { + h = (g - b) / delta; + } else if (g === max) { + h = 2 + (b - r) / delta; + } else if (b === max) { + h = 4 + (r - g) / delta; + } + + h = Math.min(h * 60, 360); + + if (h < 0) { + h += 360; + } + + l = (min + max) / 2; + + if (max === min) { + s = 0; + } else if (l <= 0.5) { + s = delta / (max + min); + } else { + s = delta / (2 - max - min); + } + + return [h, s * 100, l * 100]; + }; + + convert.rgb.hsv = function (rgb) { + var rdif; + var gdif; + var bdif; + var h; + var s; + + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var v = Math.max(r, g, b); + var diff = v - Math.min(r, g, b); + var diffc = function (c) { + return (v - c) / 6 / diff + 1 / 2; + }; + + if (diff === 0) { + h = s = 0; + } else { + s = diff / v; + rdif = diffc(r); + gdif = diffc(g); + bdif = diffc(b); + + if (r === v) { + h = bdif - gdif; + } else if (g === v) { + h = (1 / 3) + rdif - bdif; + } else if (b === v) { + h = (2 / 3) + gdif - rdif; + } + if (h < 0) { + h += 1; + } else if (h > 1) { + h -= 1; + } + } + + return [ + h * 360, + s * 100, + v * 100 + ]; + }; + + convert.rgb.hwb = function (rgb) { + var r = rgb[0]; + var g = rgb[1]; + var b = rgb[2]; + var h = convert.rgb.hsl(rgb)[0]; + var w = 1 / 255 * Math.min(r, Math.min(g, b)); + + b = 1 - 1 / 255 * Math.max(r, Math.max(g, b)); + + return [h, w * 100, b * 100]; + }; + + convert.rgb.cmyk = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var c; + var m; + var y; + var k; + + k = Math.min(1 - r, 1 - g, 1 - b); + c = (1 - r - k) / (1 - k) || 0; + m = (1 - g - k) / (1 - k) || 0; + y = (1 - b - k) / (1 - k) || 0; + + return [c * 100, m * 100, y * 100, k * 100]; + }; + + /** + * See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance + * */ + function comparativeDistance(x, y) { + return ( + Math.pow(x[0] - y[0], 2) + + Math.pow(x[1] - y[1], 2) + + Math.pow(x[2] - y[2], 2) + ); + } + + convert.rgb.keyword = function (rgb) { + var reversed = reverseKeywords[rgb]; + if (reversed) { + return reversed; + } + + var currentClosestDistance = Infinity; + var currentClosestKeyword; + + for (var keyword in colorName) { + if (colorName.hasOwnProperty(keyword)) { + var value = colorName[keyword]; + + // Compute comparative distance + var distance = comparativeDistance(rgb, value); + + // Check if its less, if so set as closest + if (distance < currentClosestDistance) { + currentClosestDistance = distance; + currentClosestKeyword = keyword; + } + } + } + + return currentClosestKeyword; + }; + + convert.keyword.rgb = function (keyword) { + return colorName[keyword]; + }; + + convert.rgb.xyz = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + + // assume sRGB + r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); + g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); + b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); + + var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + + return [x * 100, y * 100, z * 100]; + }; + + convert.rgb.lab = function (rgb) { + var xyz = convert.rgb.xyz(rgb); + var x = xyz[0]; + var y = xyz[1]; + var z = xyz[2]; + var l; + var a; + var b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; + }; + + convert.hsl.rgb = function (hsl) { + var h = hsl[0] / 360; + var s = hsl[1] / 100; + var l = hsl[2] / 100; + var t1; + var t2; + var t3; + var rgb; + var val; + + if (s === 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) { + t2 = l * (1 + s); + } else { + t2 = l + s - l * s; + } + + t1 = 2 * l - t2; + + rgb = [0, 0, 0]; + for (var i = 0; i < 3; i++) { + t3 = h + 1 / 3 * -(i - 1); + if (t3 < 0) { + t3++; + } + if (t3 > 1) { + t3--; + } + + if (6 * t3 < 1) { + val = t1 + (t2 - t1) * 6 * t3; + } else if (2 * t3 < 1) { + val = t2; + } else if (3 * t3 < 2) { + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + } else { + val = t1; + } + + rgb[i] = val * 255; + } + + return rgb; + }; + + convert.hsl.hsv = function (hsl) { + var h = hsl[0]; + var s = hsl[1] / 100; + var l = hsl[2] / 100; + var smin = s; + var lmin = Math.max(l, 0.01); + var sv; + var v; + + l *= 2; + s *= (l <= 1) ? l : 2 - l; + smin *= lmin <= 1 ? lmin : 2 - lmin; + v = (l + s) / 2; + sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s); + + return [h, sv * 100, v * 100]; + }; + + convert.hsv.rgb = function (hsv) { + var h = hsv[0] / 60; + var s = hsv[1] / 100; + var v = hsv[2] / 100; + var hi = Math.floor(h) % 6; + + var f = h - Math.floor(h); + var p = 255 * v * (1 - s); + var q = 255 * v * (1 - (s * f)); + var t = 255 * v * (1 - (s * (1 - f))); + v *= 255; + + switch (hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } + }; + + convert.hsv.hsl = function (hsv) { + var h = hsv[0]; + var s = hsv[1] / 100; + var v = hsv[2] / 100; + var vmin = Math.max(v, 0.01); + var lmin; + var sl; + var l; + + l = (2 - s) * v; + lmin = (2 - s) * vmin; + sl = s * vmin; + sl /= (lmin <= 1) ? lmin : 2 - lmin; + sl = sl || 0; + l /= 2; + + return [h, sl * 100, l * 100]; + }; + + // http://dev.w3.org/csswg/css-color/#hwb-to-rgb + convert.hwb.rgb = function (hwb) { + var h = hwb[0] / 360; + var wh = hwb[1] / 100; + var bl = hwb[2] / 100; + var ratio = wh + bl; + var i; + var v; + var f; + var n; + + // wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } + + i = Math.floor(6 * h); + v = 1 - bl; + f = 6 * h - i; + + if ((i & 0x01) !== 0) { + f = 1 - f; + } + + n = wh + f * (v - wh); // linear interpolation + + var r; + var g; + var b; + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } + + return [r * 255, g * 255, b * 255]; + }; + + convert.cmyk.rgb = function (cmyk) { + var c = cmyk[0] / 100; + var m = cmyk[1] / 100; + var y = cmyk[2] / 100; + var k = cmyk[3] / 100; + var r; + var g; + var b; + + r = 1 - Math.min(1, c * (1 - k) + k); + g = 1 - Math.min(1, m * (1 - k) + k); + b = 1 - Math.min(1, y * (1 - k) + k); + + return [r * 255, g * 255, b * 255]; + }; + + convert.xyz.rgb = function (xyz) { + var x = xyz[0] / 100; + var y = xyz[1] / 100; + var z = xyz[2] / 100; + var r; + var g; + var b; + + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); + + // assume sRGB + r = r > 0.0031308 + ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r * 12.92; + + g = g > 0.0031308 + ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g * 12.92; + + b = b > 0.0031308 + ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b * 12.92; + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; + }; + + convert.xyz.lab = function (xyz) { + var x = xyz[0]; + var y = xyz[1]; + var z = xyz[2]; + var l; + var a; + var b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; + }; + + convert.lab.xyz = function (lab) { + var l = lab[0]; + var a = lab[1]; + var b = lab[2]; + var x; + var y; + var z; + + y = (l + 16) / 116; + x = a / 500 + y; + z = y - b / 200; + + var y2 = Math.pow(y, 3); + var x2 = Math.pow(x, 3); + var z2 = Math.pow(z, 3); + y = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787; + x = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787; + z = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787; + + x *= 95.047; + y *= 100; + z *= 108.883; + + return [x, y, z]; + }; + + convert.lab.lch = function (lab) { + var l = lab[0]; + var a = lab[1]; + var b = lab[2]; + var hr; + var h; + var c; + + hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; + + if (h < 0) { + h += 360; + } + + c = Math.sqrt(a * a + b * b); + + return [l, c, h]; + }; + + convert.lch.lab = function (lch) { + var l = lch[0]; + var c = lch[1]; + var h = lch[2]; + var a; + var b; + var hr; + + hr = h / 360 * 2 * Math.PI; + a = c * Math.cos(hr); + b = c * Math.sin(hr); + + return [l, a, b]; + }; + + convert.rgb.ansi16 = function (args) { + var r = args[0]; + var g = args[1]; + var b = args[2]; + var value = 1 in arguments ? arguments[1] : convert.rgb.hsv(args)[2]; // hsv -> ansi16 optimization + + value = Math.round(value / 50); + + if (value === 0) { + return 30; + } + + var ansi = 30 + + ((Math.round(b / 255) << 2) + | (Math.round(g / 255) << 1) + | Math.round(r / 255)); + + if (value === 2) { + ansi += 60; + } + + return ansi; + }; + + convert.hsv.ansi16 = function (args) { + // optimization here; we already know the value and don't need to get + // it converted for us. + return convert.rgb.ansi16(convert.hsv.rgb(args), args[2]); + }; + + convert.rgb.ansi256 = function (args) { + var r = args[0]; + var g = args[1]; + var b = args[2]; + + // we use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (r === g && g === b) { + if (r < 8) { + return 16; + } + + if (r > 248) { + return 231; + } + + return Math.round(((r - 8) / 247) * 24) + 232; + } + + var ansi = 16 + + (36 * Math.round(r / 255 * 5)) + + (6 * Math.round(g / 255 * 5)) + + Math.round(b / 255 * 5); + + return ansi; + }; + + convert.ansi16.rgb = function (args) { + var color = args % 10; + + // handle greyscale + if (color === 0 || color === 7) { + if (args > 50) { + color += 3.5; + } + + color = color / 10.5 * 255; + + return [color, color, color]; + } + + var mult = (~~(args > 50) + 1) * 0.5; + var r = ((color & 1) * mult) * 255; + var g = (((color >> 1) & 1) * mult) * 255; + var b = (((color >> 2) & 1) * mult) * 255; + + return [r, g, b]; + }; + + convert.ansi256.rgb = function (args) { + // handle greyscale + if (args >= 232) { + var c = (args - 232) * 10 + 8; + return [c, c, c]; + } + + args -= 16; + + var rem; + var r = Math.floor(args / 36) / 5 * 255; + var g = Math.floor((rem = args % 36) / 6) / 5 * 255; + var b = (rem % 6) / 5 * 255; + + return [r, g, b]; + }; + + convert.rgb.hex = function (args) { + var integer = ((Math.round(args[0]) & 0xFF) << 16) + + ((Math.round(args[1]) & 0xFF) << 8) + + (Math.round(args[2]) & 0xFF); + + var string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; + }; + + convert.hex.rgb = function (args) { + var match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i); + if (!match) { + return [0, 0, 0]; + } + + var colorString = match[0]; + + if (match[0].length === 3) { + colorString = colorString.split('').map(function (char) { + return char + char; + }).join(''); + } + + var integer = parseInt(colorString, 16); + var r = (integer >> 16) & 0xFF; + var g = (integer >> 8) & 0xFF; + var b = integer & 0xFF; + + return [r, g, b]; + }; + + convert.rgb.hcg = function (rgb) { + var r = rgb[0] / 255; + var g = rgb[1] / 255; + var b = rgb[2] / 255; + var max = Math.max(Math.max(r, g), b); + var min = Math.min(Math.min(r, g), b); + var chroma = (max - min); + var grayscale; + var hue; + + if (chroma < 1) { + grayscale = min / (1 - chroma); + } else { + grayscale = 0; + } + + if (chroma <= 0) { + hue = 0; + } else + if (max === r) { + hue = ((g - b) / chroma) % 6; + } else + if (max === g) { + hue = 2 + (b - r) / chroma; + } else { + hue = 4 + (r - g) / chroma + 4; + } + + hue /= 6; + hue %= 1; + + return [hue * 360, chroma * 100, grayscale * 100]; + }; + + convert.hsl.hcg = function (hsl) { + var s = hsl[1] / 100; + var l = hsl[2] / 100; + var c = 1; + var f = 0; + + if (l < 0.5) { + c = 2.0 * s * l; + } else { + c = 2.0 * s * (1.0 - l); + } + + if (c < 1.0) { + f = (l - 0.5 * c) / (1.0 - c); + } + + return [hsl[0], c * 100, f * 100]; + }; + + convert.hsv.hcg = function (hsv) { + var s = hsv[1] / 100; + var v = hsv[2] / 100; + + var c = s * v; + var f = 0; + + if (c < 1.0) { + f = (v - c) / (1 - c); + } + + return [hsv[0], c * 100, f * 100]; + }; + + convert.hcg.rgb = function (hcg) { + var h = hcg[0] / 360; + var c = hcg[1] / 100; + var g = hcg[2] / 100; + + if (c === 0.0) { + return [g * 255, g * 255, g * 255]; + } + + var pure = [0, 0, 0]; + var hi = (h % 1) * 6; + var v = hi % 1; + var w = 1 - v; + var mg = 0; + + switch (Math.floor(hi)) { + case 0: + pure[0] = 1; pure[1] = v; pure[2] = 0; break; + case 1: + pure[0] = w; pure[1] = 1; pure[2] = 0; break; + case 2: + pure[0] = 0; pure[1] = 1; pure[2] = v; break; + case 3: + pure[0] = 0; pure[1] = w; pure[2] = 1; break; + case 4: + pure[0] = v; pure[1] = 0; pure[2] = 1; break; + default: + pure[0] = 1; pure[1] = 0; pure[2] = w; + } + + mg = (1.0 - c) * g; + + return [ + (c * pure[0] + mg) * 255, + (c * pure[1] + mg) * 255, + (c * pure[2] + mg) * 255 + ]; + }; + + convert.hcg.hsv = function (hcg) { + var c = hcg[1] / 100; + var g = hcg[2] / 100; + + var v = c + g * (1.0 - c); + var f = 0; + + if (v > 0.0) { + f = c / v; + } + + return [hcg[0], f * 100, v * 100]; + }; + + convert.hcg.hsl = function (hcg) { + var c = hcg[1] / 100; + var g = hcg[2] / 100; + + var l = g * (1.0 - c) + 0.5 * c; + var s = 0; + + if (l > 0.0 && l < 0.5) { + s = c / (2 * l); + } else + if (l >= 0.5 && l < 1.0) { + s = c / (2 * (1 - l)); + } + + return [hcg[0], s * 100, l * 100]; + }; + + convert.hcg.hwb = function (hcg) { + var c = hcg[1] / 100; + var g = hcg[2] / 100; + var v = c + g * (1.0 - c); + return [hcg[0], (v - c) * 100, (1 - v) * 100]; + }; + + convert.hwb.hcg = function (hwb) { + var w = hwb[1] / 100; + var b = hwb[2] / 100; + var v = 1 - b; + var c = v - w; + var g = 0; + + if (c < 1) { + g = (v - c) / (1 - c); + } + + return [hwb[0], c * 100, g * 100]; + }; + + convert.apple.rgb = function (apple) { + return [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255]; + }; + + convert.rgb.apple = function (rgb) { + return [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535]; + }; + + convert.gray.rgb = function (args) { + return [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255]; + }; + + convert.gray.hsl = convert.gray.hsv = function (args) { + return [0, 0, args[0]]; + }; + + convert.gray.hwb = function (gray) { + return [0, 100, gray[0]]; + }; + + convert.gray.cmyk = function (gray) { + return [0, 0, 0, gray[0]]; + }; + + convert.gray.lab = function (gray) { + return [gray[0], 0, 0]; + }; + + convert.gray.hex = function (gray) { + var val = Math.round(gray[0] / 100 * 255) & 0xFF; + var integer = (val << 16) + (val << 8) + val; + + var string = integer.toString(16).toUpperCase(); + return '000000'.substring(string.length) + string; + }; + + convert.rgb.gray = function (rgb) { + var val = (rgb[0] + rgb[1] + rgb[2]) / 3; + return [val / 255 * 100]; + }; + }); + conversions.rgb; + conversions.hsl; + conversions.hsv; + conversions.hwb; + conversions.cmyk; + conversions.xyz; + conversions.lab; + conversions.lch; + conversions.hex; + conversions.keyword; + conversions.ansi16; + conversions.ansi256; + conversions.hcg; + conversions.apple; + conversions.gray; + + /* + this function routes a model to all other models. + + all functions that are routed have a property `.conversion` attached + to the returned synthetic function. This property is an array + of strings, each with the steps in between the 'from' and 'to' + color models (inclusive). + + conversions that are not possible simply are not included. + */ + + function buildGraph() { + var graph = {}; + // https://jsperf.com/object-keys-vs-for-in-with-closure/3 + var models = Object.keys(conversions); + + for (var len = models.length, i = 0; i < len; i++) { + graph[models[i]] = { + // http://jsperf.com/1-vs-infinity + // micro-opt, but this is simple. + distance: -1, + parent: null + }; + } + + return graph; + } + + // https://en.wikipedia.org/wiki/Breadth-first_search + function deriveBFS(fromModel) { + var graph = buildGraph(); + var queue = [fromModel]; // unshift -> queue -> pop + + graph[fromModel].distance = 0; + + while (queue.length) { + var current = queue.pop(); + var adjacents = Object.keys(conversions[current]); + + for (var len = adjacents.length, i = 0; i < len; i++) { + var adjacent = adjacents[i]; + var node = graph[adjacent]; + + if (node.distance === -1) { + node.distance = graph[current].distance + 1; + node.parent = current; + queue.unshift(adjacent); + } + } + } + + return graph; + } + + function link(from, to) { + return function (args) { + return to(from(args)); + }; + } + + function wrapConversion(toModel, graph) { + var path = [graph[toModel].parent, toModel]; + var fn = conversions[graph[toModel].parent][toModel]; + + var cur = graph[toModel].parent; + while (graph[cur].parent) { + path.unshift(graph[cur].parent); + fn = link(conversions[graph[cur].parent][cur], fn); + cur = graph[cur].parent; + } + + fn.conversion = path; + return fn; + } + + var route = function (fromModel) { + var graph = deriveBFS(fromModel); + var conversion = {}; + + var models = Object.keys(graph); + for (var len = models.length, i = 0; i < len; i++) { + var toModel = models[i]; + var node = graph[toModel]; + + if (node.parent === null) { + // no possible conversion, or this node is the source model. + continue; + } + + conversion[toModel] = wrapConversion(toModel, graph); + } + + return conversion; + }; + + var convert = {}; + + var models = Object.keys(conversions); + + function wrapRaw(fn) { + var wrappedFn = function (args) { + if (args === undefined || args === null) { + return args; + } + + if (arguments.length > 1) { + args = Array.prototype.slice.call(arguments); + } + + return fn(args); + }; + + // preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; + } + + function wrapRounded(fn) { + var wrappedFn = function (args) { + if (args === undefined || args === null) { + return args; + } + + if (arguments.length > 1) { + args = Array.prototype.slice.call(arguments); + } + + var result = fn(args); + + // we're assuming the result is an array here. + // see notice in conversions.js; don't use box types + // in conversion functions. + if (typeof result === 'object') { + for (var len = result.length, i = 0; i < len; i++) { + result[i] = Math.round(result[i]); + } + } + + return result; + }; + + // preserve .conversion property if there is one + if ('conversion' in fn) { + wrappedFn.conversion = fn.conversion; + } + + return wrappedFn; + } + + models.forEach(function (fromModel) { + convert[fromModel] = {}; + + Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels}); + Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels}); + + var routes = route(fromModel); + var routeModels = Object.keys(routes); + + routeModels.forEach(function (toModel) { + var fn = routes[toModel]; + + convert[fromModel][toModel] = wrapRounded(fn); + convert[fromModel][toModel].raw = wrapRaw(fn); + }); + }); + + var colorConvert = convert; + + var colorName$1 = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] + }; + + /* MIT license */ + + + var colorString = { + getRgba: getRgba, + getHsla: getHsla, + getRgb: getRgb, + getHsl: getHsl, + getHwb: getHwb, + getAlpha: getAlpha, + + hexString: hexString, + rgbString: rgbString, + rgbaString: rgbaString, + percentString: percentString, + percentaString: percentaString, + hslString: hslString, + hslaString: hslaString, + hwbString: hwbString, + keyword: keyword + }; + + function getRgba(string) { + if (!string) { + return; + } + var abbr = /^#([a-fA-F0-9]{3,4})$/i, + hex = /^#([a-fA-F0-9]{6}([a-fA-F0-9]{2})?)$/i, + rgba = /^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i, + per = /^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/i, + keyword = /(\w+)/; + + var rgb = [0, 0, 0], + a = 1, + match = string.match(abbr), + hexAlpha = ""; + if (match) { + match = match[1]; + hexAlpha = match[3]; + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match[i] + match[i], 16); + } + if (hexAlpha) { + a = Math.round((parseInt(hexAlpha + hexAlpha, 16) / 255) * 100) / 100; + } + } + else if (match = string.match(hex)) { + hexAlpha = match[2]; + match = match[1]; + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match.slice(i * 2, i * 2 + 2), 16); + } + if (hexAlpha) { + a = Math.round((parseInt(hexAlpha, 16) / 255) * 100) / 100; + } + } + else if (match = string.match(rgba)) { + for (var i = 0; i < rgb.length; i++) { + rgb[i] = parseInt(match[i + 1]); + } + a = parseFloat(match[4]); + } + else if (match = string.match(per)) { + for (var i = 0; i < rgb.length; i++) { + rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55); + } + a = parseFloat(match[4]); + } + else if (match = string.match(keyword)) { + if (match[1] == "transparent") { + return [0, 0, 0, 0]; + } + rgb = colorName$1[match[1]]; + if (!rgb) { + return; + } + } + + for (var i = 0; i < rgb.length; i++) { + rgb[i] = scale(rgb[i], 0, 255); + } + if (!a && a != 0) { + a = 1; + } + else { + a = scale(a, 0, 1); + } + rgb[3] = a; + return rgb; + } + + function getHsla(string) { + if (!string) { + return; + } + var hsl = /^hsla?\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; + var match = string.match(hsl); + if (match) { + var alpha = parseFloat(match[4]); + var h = scale(parseInt(match[1]), 0, 360), + s = scale(parseFloat(match[2]), 0, 100), + l = scale(parseFloat(match[3]), 0, 100), + a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, s, l, a]; + } + } + + function getHwb(string) { + if (!string) { + return; + } + var hwb = /^hwb\(\s*([+-]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)/; + var match = string.match(hwb); + if (match) { + var alpha = parseFloat(match[4]); + var h = scale(parseInt(match[1]), 0, 360), + w = scale(parseFloat(match[2]), 0, 100), + b = scale(parseFloat(match[3]), 0, 100), + a = scale(isNaN(alpha) ? 1 : alpha, 0, 1); + return [h, w, b, a]; + } + } + + function getRgb(string) { + var rgba = getRgba(string); + return rgba && rgba.slice(0, 3); + } + + function getHsl(string) { + var hsla = getHsla(string); + return hsla && hsla.slice(0, 3); + } + + function getAlpha(string) { + var vals = getRgba(string); + if (vals) { + return vals[3]; + } + else if (vals = getHsla(string)) { + return vals[3]; + } + else if (vals = getHwb(string)) { + return vals[3]; + } + } + + // generators + function hexString(rgba, a) { + var a = (a !== undefined && rgba.length === 3) ? a : rgba[3]; + return "#" + hexDouble(rgba[0]) + + hexDouble(rgba[1]) + + hexDouble(rgba[2]) + + ( + (a >= 0 && a < 1) + ? hexDouble(Math.round(a * 255)) + : "" + ); + } + + function rgbString(rgba, alpha) { + if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { + return rgbaString(rgba, alpha); + } + return "rgb(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ")"; + } + + function rgbaString(rgba, alpha) { + if (alpha === undefined) { + alpha = (rgba[3] !== undefined ? rgba[3] : 1); + } + return "rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + + ", " + alpha + ")"; + } + + function percentString(rgba, alpha) { + if (alpha < 1 || (rgba[3] && rgba[3] < 1)) { + return percentaString(rgba, alpha); + } + var r = Math.round(rgba[0]/255 * 100), + g = Math.round(rgba[1]/255 * 100), + b = Math.round(rgba[2]/255 * 100); + + return "rgb(" + r + "%, " + g + "%, " + b + "%)"; + } + + function percentaString(rgba, alpha) { + var r = Math.round(rgba[0]/255 * 100), + g = Math.round(rgba[1]/255 * 100), + b = Math.round(rgba[2]/255 * 100); + return "rgba(" + r + "%, " + g + "%, " + b + "%, " + (alpha || rgba[3] || 1) + ")"; + } + + function hslString(hsla, alpha) { + if (alpha < 1 || (hsla[3] && hsla[3] < 1)) { + return hslaString(hsla, alpha); + } + return "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)"; + } + + function hslaString(hsla, alpha) { + if (alpha === undefined) { + alpha = (hsla[3] !== undefined ? hsla[3] : 1); + } + return "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, " + + alpha + ")"; + } + + // hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax + // (hwb have alpha optional & 1 is default value) + function hwbString(hwb, alpha) { + if (alpha === undefined) { + alpha = (hwb[3] !== undefined ? hwb[3] : 1); + } + return "hwb(" + hwb[0] + ", " + hwb[1] + "%, " + hwb[2] + "%" + + (alpha !== undefined && alpha !== 1 ? ", " + alpha : "") + ")"; + } + + function keyword(rgb) { + return reverseNames[rgb.slice(0, 3)]; + } + + // helpers + function scale(num, min, max) { + return Math.min(Math.max(min, num), max); + } + + function hexDouble(num) { + var str = num.toString(16).toUpperCase(); + return (str.length < 2) ? "0" + str : str; + } + + + //create a list of reverse color names + var reverseNames = {}; + for (var name in colorName$1) { + reverseNames[colorName$1[name]] = name; + } + + /* MIT license */ + + + + var Color = function (obj) { + if (obj instanceof Color) { + return obj; + } + if (!(this instanceof Color)) { + return new Color(obj); + } + + this.valid = false; + this.values = { + rgb: [0, 0, 0], + hsl: [0, 0, 0], + hsv: [0, 0, 0], + hwb: [0, 0, 0], + cmyk: [0, 0, 0, 0], + alpha: 1 + }; + + // parse Color() argument + var vals; + if (typeof obj === 'string') { + vals = colorString.getRgba(obj); + if (vals) { + this.setValues('rgb', vals); + } else if (vals = colorString.getHsla(obj)) { + this.setValues('hsl', vals); + } else if (vals = colorString.getHwb(obj)) { + this.setValues('hwb', vals); + } + } else if (typeof obj === 'object') { + vals = obj; + if (vals.r !== undefined || vals.red !== undefined) { + this.setValues('rgb', vals); + } else if (vals.l !== undefined || vals.lightness !== undefined) { + this.setValues('hsl', vals); + } else if (vals.v !== undefined || vals.value !== undefined) { + this.setValues('hsv', vals); + } else if (vals.w !== undefined || vals.whiteness !== undefined) { + this.setValues('hwb', vals); + } else if (vals.c !== undefined || vals.cyan !== undefined) { + this.setValues('cmyk', vals); + } + } + }; + + Color.prototype = { + isValid: function () { + return this.valid; + }, + rgb: function () { + return this.setSpace('rgb', arguments); + }, + hsl: function () { + return this.setSpace('hsl', arguments); + }, + hsv: function () { + return this.setSpace('hsv', arguments); + }, + hwb: function () { + return this.setSpace('hwb', arguments); + }, + cmyk: function () { + return this.setSpace('cmyk', arguments); + }, + + rgbArray: function () { + return this.values.rgb; + }, + hslArray: function () { + return this.values.hsl; + }, + hsvArray: function () { + return this.values.hsv; + }, + hwbArray: function () { + var values = this.values; + if (values.alpha !== 1) { + return values.hwb.concat([values.alpha]); + } + return values.hwb; + }, + cmykArray: function () { + return this.values.cmyk; + }, + rgbaArray: function () { + var values = this.values; + return values.rgb.concat([values.alpha]); + }, + hslaArray: function () { + var values = this.values; + return values.hsl.concat([values.alpha]); + }, + alpha: function (val) { + if (val === undefined) { + return this.values.alpha; + } + this.setValues('alpha', val); + return this; + }, + + red: function (val) { + return this.setChannel('rgb', 0, val); + }, + green: function (val) { + return this.setChannel('rgb', 1, val); + }, + blue: function (val) { + return this.setChannel('rgb', 2, val); + }, + hue: function (val) { + if (val) { + val %= 360; + val = val < 0 ? 360 + val : val; + } + return this.setChannel('hsl', 0, val); + }, + saturation: function (val) { + return this.setChannel('hsl', 1, val); + }, + lightness: function (val) { + return this.setChannel('hsl', 2, val); + }, + saturationv: function (val) { + return this.setChannel('hsv', 1, val); + }, + whiteness: function (val) { + return this.setChannel('hwb', 1, val); + }, + blackness: function (val) { + return this.setChannel('hwb', 2, val); + }, + value: function (val) { + return this.setChannel('hsv', 2, val); + }, + cyan: function (val) { + return this.setChannel('cmyk', 0, val); + }, + magenta: function (val) { + return this.setChannel('cmyk', 1, val); + }, + yellow: function (val) { + return this.setChannel('cmyk', 2, val); + }, + black: function (val) { + return this.setChannel('cmyk', 3, val); + }, + + hexString: function () { + return colorString.hexString(this.values.rgb); + }, + rgbString: function () { + return colorString.rgbString(this.values.rgb, this.values.alpha); + }, + rgbaString: function () { + return colorString.rgbaString(this.values.rgb, this.values.alpha); + }, + percentString: function () { + return colorString.percentString(this.values.rgb, this.values.alpha); + }, + hslString: function () { + return colorString.hslString(this.values.hsl, this.values.alpha); + }, + hslaString: function () { + return colorString.hslaString(this.values.hsl, this.values.alpha); + }, + hwbString: function () { + return colorString.hwbString(this.values.hwb, this.values.alpha); + }, + keyword: function () { + return colorString.keyword(this.values.rgb, this.values.alpha); + }, + + rgbNumber: function () { + var rgb = this.values.rgb; + return (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + }, + + luminosity: function () { + // http://www.w3.org/TR/WCAG20/#relativeluminancedef + var rgb = this.values.rgb; + var lum = []; + for (var i = 0; i < rgb.length; i++) { + var chan = rgb[i] / 255; + lum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4); + } + return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; + }, + + contrast: function (color2) { + // http://www.w3.org/TR/WCAG20/#contrast-ratiodef + var lum1 = this.luminosity(); + var lum2 = color2.luminosity(); + if (lum1 > lum2) { + return (lum1 + 0.05) / (lum2 + 0.05); + } + return (lum2 + 0.05) / (lum1 + 0.05); + }, + + level: function (color2) { + var contrastRatio = this.contrast(color2); + if (contrastRatio >= 7.1) { + return 'AAA'; + } + + return (contrastRatio >= 4.5) ? 'AA' : ''; + }, + + dark: function () { + // YIQ equation from http://24ways.org/2010/calculating-color-contrast + var rgb = this.values.rgb; + var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + return yiq < 128; + }, + + light: function () { + return !this.dark(); + }, + + negate: function () { + var rgb = []; + for (var i = 0; i < 3; i++) { + rgb[i] = 255 - this.values.rgb[i]; + } + this.setValues('rgb', rgb); + return this; + }, + + lighten: function (ratio) { + var hsl = this.values.hsl; + hsl[2] += hsl[2] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + darken: function (ratio) { + var hsl = this.values.hsl; + hsl[2] -= hsl[2] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + saturate: function (ratio) { + var hsl = this.values.hsl; + hsl[1] += hsl[1] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + desaturate: function (ratio) { + var hsl = this.values.hsl; + hsl[1] -= hsl[1] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + whiten: function (ratio) { + var hwb = this.values.hwb; + hwb[1] += hwb[1] * ratio; + this.setValues('hwb', hwb); + return this; + }, + + blacken: function (ratio) { + var hwb = this.values.hwb; + hwb[2] += hwb[2] * ratio; + this.setValues('hwb', hwb); + return this; + }, + + greyscale: function () { + var rgb = this.values.rgb; + // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale + var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; + this.setValues('rgb', [val, val, val]); + return this; + }, + + clearer: function (ratio) { + var alpha = this.values.alpha; + this.setValues('alpha', alpha - (alpha * ratio)); + return this; + }, + + opaquer: function (ratio) { + var alpha = this.values.alpha; + this.setValues('alpha', alpha + (alpha * ratio)); + return this; + }, + + rotate: function (degrees) { + var hsl = this.values.hsl; + var hue = (hsl[0] + degrees) % 360; + hsl[0] = hue < 0 ? 360 + hue : hue; + this.setValues('hsl', hsl); + return this; + }, + + /** + * Ported from sass implementation in C + * https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209 + */ + mix: function (mixinColor, weight) { + var color1 = this; + var color2 = mixinColor; + var p = weight === undefined ? 0.5 : weight; + + var w = 2 * p - 1; + var a = color1.alpha() - color2.alpha(); + + var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + return this + .rgb( + w1 * color1.red() + w2 * color2.red(), + w1 * color1.green() + w2 * color2.green(), + w1 * color1.blue() + w2 * color2.blue() + ) + .alpha(color1.alpha() * p + color2.alpha() * (1 - p)); + }, + + toJSON: function () { + return this.rgb(); + }, + + clone: function () { + // NOTE(SB): using node-clone creates a dependency to Buffer when using browserify, + // making the final build way to big to embed in Chart.js. So let's do it manually, + // assuming that values to clone are 1 dimension arrays containing only numbers, + // except 'alpha' which is a number. + var result = new Color(); + var source = this.values; + var target = result.values; + var value, type; + + for (var prop in source) { + if (source.hasOwnProperty(prop)) { + value = source[prop]; + type = ({}).toString.call(value); + if (type === '[object Array]') { + target[prop] = value.slice(0); + } else if (type === '[object Number]') { + target[prop] = value; + } else { + console.error('unexpected color value:', value); + } + } + } + + return result; + } + }; + + Color.prototype.spaces = { + rgb: ['red', 'green', 'blue'], + hsl: ['hue', 'saturation', 'lightness'], + hsv: ['hue', 'saturation', 'value'], + hwb: ['hue', 'whiteness', 'blackness'], + cmyk: ['cyan', 'magenta', 'yellow', 'black'] + }; + + Color.prototype.maxes = { + rgb: [255, 255, 255], + hsl: [360, 100, 100], + hsv: [360, 100, 100], + hwb: [360, 100, 100], + cmyk: [100, 100, 100, 100] + }; + + Color.prototype.getValues = function (space) { + var values = this.values; + var vals = {}; + + for (var i = 0; i < space.length; i++) { + vals[space.charAt(i)] = values[space][i]; + } + + if (values.alpha !== 1) { + vals.a = values.alpha; + } + + // {r: 255, g: 255, b: 255, a: 0.4} + return vals; + }; + + Color.prototype.setValues = function (space, vals) { + var values = this.values; + var spaces = this.spaces; + var maxes = this.maxes; + var alpha = 1; + var i; + + this.valid = true; + + if (space === 'alpha') { + alpha = vals; + } else if (vals.length) { + // [10, 10, 10] + values[space] = vals.slice(0, space.length); + alpha = vals[space.length]; + } else if (vals[space.charAt(0)] !== undefined) { + // {r: 10, g: 10, b: 10} + for (i = 0; i < space.length; i++) { + values[space][i] = vals[space.charAt(i)]; + } + + alpha = vals.a; + } else if (vals[spaces[space][0]] !== undefined) { + // {red: 10, green: 10, blue: 10} + var chans = spaces[space]; + + for (i = 0; i < space.length; i++) { + values[space][i] = vals[chans[i]]; + } + + alpha = vals.alpha; + } + + values.alpha = Math.max(0, Math.min(1, (alpha === undefined ? values.alpha : alpha))); + + if (space === 'alpha') { + return false; + } + + var capped; + + // cap values of the space prior converting all values + for (i = 0; i < space.length; i++) { + capped = Math.max(0, Math.min(maxes[space][i], values[space][i])); + values[space][i] = Math.round(capped); + } + + // convert to all the other color spaces + for (var sname in spaces) { + if (sname !== space) { + values[sname] = colorConvert[space][sname](values[space]); + } + } + + return true; + }; + + Color.prototype.setSpace = function (space, args) { + var vals = args[0]; + + if (vals === undefined) { + // color.rgb() + return this.getValues(space); + } + + // color.rgb(10, 10, 10) + if (typeof vals === 'number') { + vals = Array.prototype.slice.call(args); + } + + this.setValues(space, vals); + return this; + }; + + Color.prototype.setChannel = function (space, index, val) { + var svalues = this.values[space]; + if (val === undefined) { + // color.red() + return svalues[index]; + } else if (val === svalues[index]) { + // color.red(color.red()) + return this; + } + + // color.red(100) + svalues[index] = val; + this.setValues(space, svalues); + + return this; + }; + + if (typeof window !== 'undefined') { + window.Color = Color; + } + + var chartjsColor = Color; + + function isValidKey(key) { + return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1; + } + + /** + * @namespace Chart.helpers + */ + var helpers = { + /** + * An empty function that can be used, for example, for optional callback. + */ + noop: function() {}, + + /** + * Returns a unique id, sequentially generated from a global variable. + * @returns {number} + * @function + */ + uid: (function() { + var id = 0; + return function() { + return id++; + }; + }()), + + /** + * Returns true if `value` is neither null nor undefined, else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @since 2.7.0 + */ + isNullOrUndef: function(value) { + return value === null || typeof value === 'undefined'; + }, + + /** + * Returns true if `value` is an array (including typed arrays), else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @function + */ + isArray: function(value) { + if (Array.isArray && Array.isArray(value)) { + return true; + } + var type = Object.prototype.toString.call(value); + if (type.substr(0, 7) === '[object' && type.substr(-6) === 'Array]') { + return true; + } + return false; + }, + + /** + * Returns true if `value` is an object (excluding null), else returns false. + * @param {*} value - The value to test. + * @returns {boolean} + * @since 2.7.0 + */ + isObject: function(value) { + return value !== null && Object.prototype.toString.call(value) === '[object Object]'; + }, + + /** + * Returns true if `value` is a finite number, else returns false + * @param {*} value - The value to test. + * @returns {boolean} + */ + isFinite: function(value) { + return (typeof value === 'number' || value instanceof Number) && isFinite(value); + }, + + /** + * Returns `value` if defined, else returns `defaultValue`. + * @param {*} value - The value to return if defined. + * @param {*} defaultValue - The value to return if `value` is undefined. + * @returns {*} + */ + valueOrDefault: function(value, defaultValue) { + return typeof value === 'undefined' ? defaultValue : value; + }, + + /** + * Returns value at the given `index` in array if defined, else returns `defaultValue`. + * @param {Array} value - The array to lookup for value at `index`. + * @param {number} index - The index in `value` to lookup for value. + * @param {*} defaultValue - The value to return if `value[index]` is undefined. + * @returns {*} + */ + valueAtIndexOrDefault: function(value, index, defaultValue) { + return helpers.valueOrDefault(helpers.isArray(value) ? value[index] : value, defaultValue); + }, + + /** + * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the + * value returned by `fn`. If `fn` is not a function, this method returns undefined. + * @param {function} fn - The function to call. + * @param {Array|undefined|null} args - The arguments with which `fn` should be called. + * @param {object} [thisArg] - The value of `this` provided for the call to `fn`. + * @returns {*} + */ + callback: function(fn, args, thisArg) { + if (fn && typeof fn.call === 'function') { + return fn.apply(thisArg, args); + } + }, + + /** + * Note(SB) for performance sake, this method should only be used when loopable type + * is unknown or in none intensive code (not called often and small loopable). Else + * it's preferable to use a regular for() loop and save extra function calls. + * @param {object|Array} loopable - The object or array to be iterated. + * @param {function} fn - The function to call for each item. + * @param {object} [thisArg] - The value of `this` provided for the call to `fn`. + * @param {boolean} [reverse] - If true, iterates backward on the loopable. + */ + each: function(loopable, fn, thisArg, reverse) { + var i, len, keys; + if (helpers.isArray(loopable)) { + len = loopable.length; + if (reverse) { + for (i = len - 1; i >= 0; i--) { + fn.call(thisArg, loopable[i], i); + } + } else { + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[i], i); + } + } + } else if (helpers.isObject(loopable)) { + keys = Object.keys(loopable); + len = keys.length; + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[keys[i]], keys[i]); + } + } + }, + + /** + * Returns true if the `a0` and `a1` arrays have the same content, else returns false. + * @see https://stackoverflow.com/a/14853974 + * @param {Array} a0 - The array to compare + * @param {Array} a1 - The array to compare + * @returns {boolean} + */ + arrayEquals: function(a0, a1) { + var i, ilen, v0, v1; + + if (!a0 || !a1 || a0.length !== a1.length) { + return false; + } + + for (i = 0, ilen = a0.length; i < ilen; ++i) { + v0 = a0[i]; + v1 = a1[i]; + + if (v0 instanceof Array && v1 instanceof Array) { + if (!helpers.arrayEquals(v0, v1)) { + return false; + } + } else if (v0 !== v1) { + // NOTE: two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + + return true; + }, + + /** + * Returns a deep copy of `source` without keeping references on objects and arrays. + * @param {*} source - The value to clone. + * @returns {*} + */ + clone: function(source) { + if (helpers.isArray(source)) { + return source.map(helpers.clone); + } + + if (helpers.isObject(source)) { + var target = Object.create(source); + var keys = Object.keys(source); + var klen = keys.length; + var k = 0; + + for (; k < klen; ++k) { + target[keys[k]] = helpers.clone(source[keys[k]]); + } + + return target; + } + + return source; + }, + + /** + * The default merger when Chart.helpers.merge is called without merger option. + * Note(SB): also used by mergeConfig and mergeScaleConfig as fallback. + * @private + */ + _merger: function(key, target, source, options) { + if (!isValidKey(key)) { + // We want to ensure we do not copy prototypes over + // as this can pollute global namespaces + return; + } + + var tval = target[key]; + var sval = source[key]; + + if (helpers.isObject(tval) && helpers.isObject(sval)) { + helpers.merge(tval, sval, options); + } else { + target[key] = helpers.clone(sval); + } + }, + + /** + * Merges source[key] in target[key] only if target[key] is undefined. + * @private + */ + _mergerIf: function(key, target, source) { + if (!isValidKey(key)) { + // We want to ensure we do not copy prototypes over + // as this can pollute global namespaces + return; + } + + var tval = target[key]; + var sval = source[key]; + + if (helpers.isObject(tval) && helpers.isObject(sval)) { + helpers.mergeIf(tval, sval); + } else if (!target.hasOwnProperty(key)) { + target[key] = helpers.clone(sval); + } + }, + + /** + * Recursively deep copies `source` properties into `target` with the given `options`. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param {object} target - The target object in which all sources are merged into. + * @param {object|object[]} source - Object(s) to merge into `target`. + * @param {object} [options] - Merging options: + * @param {function} [options.merger] - The merge method (key, target, source, options) + * @returns {object} The `target` object. + */ + merge: function(target, source, options) { + var sources = helpers.isArray(source) ? source : [source]; + var ilen = sources.length; + var merge, i, keys, klen, k; + + if (!helpers.isObject(target)) { + return target; + } + + options = options || {}; + merge = options.merger || helpers._merger; + + for (i = 0; i < ilen; ++i) { + source = sources[i]; + if (!helpers.isObject(source)) { + continue; + } + + keys = Object.keys(source); + for (k = 0, klen = keys.length; k < klen; ++k) { + merge(keys[k], target, source, options); + } + } + + return target; + }, + + /** + * Recursively deep copies `source` properties into `target` *only* if not defined in target. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param {object} target - The target object in which all sources are merged into. + * @param {object|object[]} source - Object(s) to merge into `target`. + * @returns {object} The `target` object. + */ + mergeIf: function(target, source) { + return helpers.merge(target, source, {merger: helpers._mergerIf}); + }, + + /** + * Applies the contents of two or more objects together into the first object. + * @param {object} target - The target object in which all objects are merged into. + * @param {object} arg1 - Object containing additional properties to merge in target. + * @param {object} argN - Additional objects containing properties to merge in target. + * @returns {object} The `target` object. + */ + extend: Object.assign || function(target) { + return helpers.merge(target, [].slice.call(arguments, 1), { + merger: function(key, dst, src) { + dst[key] = src[key]; + } + }); + }, + + /** + * Basic javascript inheritance based on the model created in Backbone.js + */ + inherits: function(extensions) { + var me = this; + var ChartElement = (extensions && extensions.hasOwnProperty('constructor')) ? extensions.constructor : function() { + return me.apply(this, arguments); + }; + + var Surrogate = function() { + this.constructor = ChartElement; + }; + + Surrogate.prototype = me.prototype; + ChartElement.prototype = new Surrogate(); + ChartElement.extend = helpers.inherits; + + if (extensions) { + helpers.extend(ChartElement.prototype, extensions); + } + + ChartElement.__super__ = me.prototype; + return ChartElement; + }, + + _deprecated: function(scope, value, previous, current) { + if (value !== undefined) { + console.warn(scope + ': "' + previous + + '" is deprecated. Please use "' + current + '" instead'); + } + } + }; + + var helpers_core = helpers; + + // DEPRECATIONS + + /** + * Provided for backward compatibility, use Chart.helpers.callback instead. + * @function Chart.helpers.callCallback + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + helpers.callCallback = helpers.callback; + + /** + * Provided for backward compatibility, use Array.prototype.indexOf instead. + * Array.prototype.indexOf compatibility: Chrome, Opera, Safari, FF1.5+, IE9+ + * @function Chart.helpers.indexOf + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers.indexOf = function(array, item, fromIndex) { + return Array.prototype.indexOf.call(array, item, fromIndex); + }; + + /** + * Provided for backward compatibility, use Chart.helpers.valueOrDefault instead. + * @function Chart.helpers.getValueOrDefault + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers.getValueOrDefault = helpers.valueOrDefault; + + /** + * Provided for backward compatibility, use Chart.helpers.valueAtIndexOrDefault instead. + * @function Chart.helpers.getValueAtIndexOrDefault + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers.getValueAtIndexOrDefault = helpers.valueAtIndexOrDefault; + + /** + * Easing functions adapted from Robert Penner's easing equations. + * @namespace Chart.helpers.easingEffects + * @see http://www.robertpenner.com/easing/ + */ + var effects = { + linear: function(t) { + return t; + }, + + easeInQuad: function(t) { + return t * t; + }, + + easeOutQuad: function(t) { + return -t * (t - 2); + }, + + easeInOutQuad: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t; + } + return -0.5 * ((--t) * (t - 2) - 1); + }, + + easeInCubic: function(t) { + return t * t * t; + }, + + easeOutCubic: function(t) { + return (t = t - 1) * t * t + 1; + }, + + easeInOutCubic: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t * t; + } + return 0.5 * ((t -= 2) * t * t + 2); + }, + + easeInQuart: function(t) { + return t * t * t * t; + }, + + easeOutQuart: function(t) { + return -((t = t - 1) * t * t * t - 1); + }, + + easeInOutQuart: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t * t * t; + } + return -0.5 * ((t -= 2) * t * t * t - 2); + }, + + easeInQuint: function(t) { + return t * t * t * t * t; + }, + + easeOutQuint: function(t) { + return (t = t - 1) * t * t * t * t + 1; + }, + + easeInOutQuint: function(t) { + if ((t /= 0.5) < 1) { + return 0.5 * t * t * t * t * t; + } + return 0.5 * ((t -= 2) * t * t * t * t + 2); + }, + + easeInSine: function(t) { + return -Math.cos(t * (Math.PI / 2)) + 1; + }, + + easeOutSine: function(t) { + return Math.sin(t * (Math.PI / 2)); + }, + + easeInOutSine: function(t) { + return -0.5 * (Math.cos(Math.PI * t) - 1); + }, + + easeInExpo: function(t) { + return (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)); + }, + + easeOutExpo: function(t) { + return (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1; + }, + + easeInOutExpo: function(t) { + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if ((t /= 0.5) < 1) { + return 0.5 * Math.pow(2, 10 * (t - 1)); + } + return 0.5 * (-Math.pow(2, -10 * --t) + 2); + }, + + easeInCirc: function(t) { + if (t >= 1) { + return t; + } + return -(Math.sqrt(1 - t * t) - 1); + }, + + easeOutCirc: function(t) { + return Math.sqrt(1 - (t = t - 1) * t); + }, + + easeInOutCirc: function(t) { + if ((t /= 0.5) < 1) { + return -0.5 * (Math.sqrt(1 - t * t) - 1); + } + return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + + easeInElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if (!p) { + p = 0.3; + } + { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * (2 * Math.PI) / p)); + }, + + easeOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if (!p) { + p = 0.3; + } + { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + return a * Math.pow(2, -10 * t) * Math.sin((t - s) * (2 * Math.PI) / p) + 1; + }, + + easeInOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if ((t /= 0.5) === 2) { + return 1; + } + if (!p) { + p = 0.45; + } + { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + if (t < 1) { + return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * (2 * Math.PI) / p)); + } + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function(t) { + var s = 1.70158; + return t * t * ((s + 1) * t - s); + }, + + easeOutBack: function(t) { + var s = 1.70158; + return (t = t - 1) * t * ((s + 1) * t + s) + 1; + }, + + easeInOutBack: function(t) { + var s = 1.70158; + if ((t /= 0.5) < 1) { + return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)); + } + return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + + easeInBounce: function(t) { + return 1 - effects.easeOutBounce(1 - t); + }, + + easeOutBounce: function(t) { + if (t < (1 / 2.75)) { + return 7.5625 * t * t; + } + if (t < (2 / 2.75)) { + return 7.5625 * (t -= (1.5 / 2.75)) * t + 0.75; + } + if (t < (2.5 / 2.75)) { + return 7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375; + } + return 7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375; + }, + + easeInOutBounce: function(t) { + if (t < 0.5) { + return effects.easeInBounce(t * 2) * 0.5; + } + return effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5; + } + }; + + var helpers_easing = { + effects: effects + }; + + // DEPRECATIONS + + /** + * Provided for backward compatibility, use Chart.helpers.easing.effects instead. + * @function Chart.helpers.easingEffects + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers_core.easingEffects = effects; + + var PI = Math.PI; + var RAD_PER_DEG = PI / 180; + var DOUBLE_PI = PI * 2; + var HALF_PI = PI / 2; + var QUARTER_PI = PI / 4; + var TWO_THIRDS_PI = PI * 2 / 3; + + /** + * @namespace Chart.helpers.canvas + */ + var exports$1 = { + /** + * Clears the entire canvas associated to the given `chart`. + * @param {Chart} chart - The chart for which to clear the canvas. + */ + clear: function(chart) { + chart.ctx.clearRect(0, 0, chart.width, chart.height); + }, + + /** + * Creates a "path" for a rectangle with rounded corners at position (x, y) with a + * given size (width, height) and the same `radius` for all corners. + * @param {CanvasRenderingContext2D} ctx - The canvas 2D Context. + * @param {number} x - The x axis of the coordinate for the rectangle starting point. + * @param {number} y - The y axis of the coordinate for the rectangle starting point. + * @param {number} width - The rectangle's width. + * @param {number} height - The rectangle's height. + * @param {number} radius - The rounded amount (in pixels) for the four corners. + * @todo handle `radius` as top-left, top-right, bottom-right, bottom-left array/object? + */ + roundedRect: function(ctx, x, y, width, height, radius) { + if (radius) { + var r = Math.min(radius, height / 2, width / 2); + var left = x + r; + var top = y + r; + var right = x + width - r; + var bottom = y + height - r; + + ctx.moveTo(x, top); + if (left < right && top < bottom) { + ctx.arc(left, top, r, -PI, -HALF_PI); + ctx.arc(right, top, r, -HALF_PI, 0); + ctx.arc(right, bottom, r, 0, HALF_PI); + ctx.arc(left, bottom, r, HALF_PI, PI); + } else if (left < right) { + ctx.moveTo(left, y); + ctx.arc(right, top, r, -HALF_PI, HALF_PI); + ctx.arc(left, top, r, HALF_PI, PI + HALF_PI); + } else if (top < bottom) { + ctx.arc(left, top, r, -PI, 0); + ctx.arc(left, bottom, r, 0, PI); + } else { + ctx.arc(left, top, r, -PI, PI); + } + ctx.closePath(); + ctx.moveTo(x, y); + } else { + ctx.rect(x, y, width, height); + } + }, + + drawPoint: function(ctx, style, radius, x, y, rotation) { + var type, xOffset, yOffset, size, cornerRadius; + var rad = (rotation || 0) * RAD_PER_DEG; + + if (style && typeof style === 'object') { + type = style.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rad); + ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height); + ctx.restore(); + return; + } + } + + if (isNaN(radius) || radius <= 0) { + return; + } + + ctx.beginPath(); + + switch (style) { + // Default includes circle + default: + ctx.arc(x, y, radius, 0, DOUBLE_PI); + ctx.closePath(); + break; + case 'triangle': + ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); + ctx.closePath(); + break; + case 'rectRounded': + // NOTE: the rounded rect implementation changed to use `arc` instead of + // `quadraticCurveTo` since it generates better results when rect is + // almost a circle. 0.516 (instead of 0.5) produces results with visually + // closer proportion to the previous impl and it is inscribed in the + // circle with `radius`. For more details, see the following PRs: + // https://github.com/chartjs/Chart.js/issues/5597 + // https://github.com/chartjs/Chart.js/issues/5858 + cornerRadius = radius * 0.516; + size = radius - cornerRadius; + xOffset = Math.cos(rad + QUARTER_PI) * size; + yOffset = Math.sin(rad + QUARTER_PI) * size; + ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); + ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad); + ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI); + ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); + ctx.closePath(); + break; + case 'rect': + if (!rotation) { + size = Math.SQRT1_2 * radius; + ctx.rect(x - size, y - size, 2 * size, 2 * size); + break; + } + rad += QUARTER_PI; + /* falls through */ + case 'rectRot': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + yOffset, y - xOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.lineTo(x - yOffset, y + xOffset); + ctx.closePath(); + break; + case 'crossRot': + rad += QUARTER_PI; + /* falls through */ + case 'cross': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'star': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + rad += QUARTER_PI; + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x + yOffset, y - xOffset); + ctx.lineTo(x - yOffset, y + xOffset); + break; + case 'line': + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + break; + case 'dash': + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius); + break; + } + + ctx.fill(); + ctx.stroke(); + }, + + /** + * Returns true if the point is inside the rectangle + * @param {object} point - The point to test + * @param {object} area - The rectangle + * @returns {boolean} + * @private + */ + _isPointInArea: function(point, area) { + var epsilon = 1e-6; // 1e-6 is margin in pixels for accumulated error. + + return point.x > area.left - epsilon && point.x < area.right + epsilon && + point.y > area.top - epsilon && point.y < area.bottom + epsilon; + }, + + clipArea: function(ctx, area) { + ctx.save(); + ctx.beginPath(); + ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top); + ctx.clip(); + }, + + unclipArea: function(ctx) { + ctx.restore(); + }, + + lineTo: function(ctx, previous, target, flip) { + var stepped = target.steppedLine; + if (stepped) { + if (stepped === 'middle') { + var midpoint = (previous.x + target.x) / 2.0; + ctx.lineTo(midpoint, flip ? target.y : previous.y); + ctx.lineTo(midpoint, flip ? previous.y : target.y); + } else if ((stepped === 'after' && !flip) || (stepped !== 'after' && flip)) { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); + return; + } + + if (!target.tension) { + ctx.lineTo(target.x, target.y); + return; + } + + ctx.bezierCurveTo( + flip ? previous.controlPointPreviousX : previous.controlPointNextX, + flip ? previous.controlPointPreviousY : previous.controlPointNextY, + flip ? target.controlPointNextX : target.controlPointPreviousX, + flip ? target.controlPointNextY : target.controlPointPreviousY, + target.x, + target.y); + } + }; + + var helpers_canvas = exports$1; + + // DEPRECATIONS + + /** + * Provided for backward compatibility, use Chart.helpers.canvas.clear instead. + * @namespace Chart.helpers.clear + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers_core.clear = exports$1.clear; + + /** + * Provided for backward compatibility, use Chart.helpers.canvas.roundedRect instead. + * @namespace Chart.helpers.drawRoundedRectangle + * @deprecated since version 2.7.0 + * @todo remove at version 3 + * @private + */ + helpers_core.drawRoundedRectangle = function(ctx) { + ctx.beginPath(); + exports$1.roundedRect.apply(exports$1, arguments); + }; + + var defaults = { + /** + * @private + */ + _set: function(scope, values) { + return helpers_core.merge(this[scope] || (this[scope] = {}), values); + } + }; + + // TODO(v3): remove 'global' from namespace. all default are global and + // there's inconsistency around which options are under 'global' + defaults._set('global', { + defaultColor: 'rgba(0,0,0,0.1)', + defaultFontColor: '#666', + defaultFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + defaultFontSize: 12, + defaultFontStyle: 'normal', + defaultLineHeight: 1.2, + showLines: true + }); + + var core_defaults = defaults; + + var valueOrDefault = helpers_core.valueOrDefault; + + /** + * Converts the given font object into a CSS font string. + * @param {object} font - A font object. + * @return {string} The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font + * @private + */ + function toFontString(font) { + if (!font || helpers_core.isNullOrUndef(font.size) || helpers_core.isNullOrUndef(font.family)) { + return null; + } + + return (font.style ? font.style + ' ' : '') + + (font.weight ? font.weight + ' ' : '') + + font.size + 'px ' + + font.family; + } + + /** + * @alias Chart.helpers.options + * @namespace + */ + var helpers_options = { + /** + * Converts the given line height `value` in pixels for a specific font `size`. + * @param {number|string} value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). + * @param {number} size - The font size (in pixels) used to resolve relative `value`. + * @returns {number} The effective line height in pixels (size * 1.2 if value is invalid). + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height + * @since 2.7.0 + */ + toLineHeight: function(value, size) { + var matches = ('' + value).match(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/); + if (!matches || matches[1] === 'normal') { + return size * 1.2; + } + + value = +matches[2]; + + switch (matches[3]) { + case 'px': + return value; + case '%': + value /= 100; + break; + } + + return size * value; + }, + + /** + * Converts the given value into a padding object with pre-computed width/height. + * @param {number|object} value - If a number, set the value to all TRBL component, + * else, if and object, use defined properties and sets undefined ones to 0. + * @returns {object} The padding values (top, right, bottom, left, width, height) + * @since 2.7.0 + */ + toPadding: function(value) { + var t, r, b, l; + + if (helpers_core.isObject(value)) { + t = +value.top || 0; + r = +value.right || 0; + b = +value.bottom || 0; + l = +value.left || 0; + } else { + t = r = b = l = +value || 0; + } + + return { + top: t, + right: r, + bottom: b, + left: l, + height: t + b, + width: l + r + }; + }, + + /** + * Parses font options and returns the font object. + * @param {object} options - A object that contains font options to be parsed. + * @return {object} The font object. + * @todo Support font.* options and renamed to toFont(). + * @private + */ + _parseFont: function(options) { + var globalDefaults = core_defaults.global; + var size = valueOrDefault(options.fontSize, globalDefaults.defaultFontSize); + var font = { + family: valueOrDefault(options.fontFamily, globalDefaults.defaultFontFamily), + lineHeight: helpers_core.options.toLineHeight(valueOrDefault(options.lineHeight, globalDefaults.defaultLineHeight), size), + size: size, + style: valueOrDefault(options.fontStyle, globalDefaults.defaultFontStyle), + weight: null, + string: '' + }; + + font.string = toFontString(font); + return font; + }, + + /** + * Evaluates the given `inputs` sequentially and returns the first defined value. + * @param {Array} inputs - An array of values, falling back to the last value. + * @param {object} [context] - If defined and the current value is a function, the value + * is called with `context` as first argument and the result becomes the new input. + * @param {number} [index] - If defined and the current value is an array, the value + * at `index` become the new input. + * @param {object} [info] - object to return information about resolution in + * @param {boolean} [info.cacheable] - Will be set to `false` if option is not cacheable. + * @since 2.7.0 + */ + resolve: function(inputs, context, index, info) { + var cacheable = true; + var i, ilen, value; + + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + cacheable = false; + } + if (index !== undefined && helpers_core.isArray(value)) { + value = value[index]; + cacheable = false; + } + if (value !== undefined) { + if (info && !cacheable) { + info.cacheable = false; + } + return value; + } + } + } + }; + + /** + * @alias Chart.helpers.math + * @namespace + */ + var exports$2 = { + /** + * Returns an array of factors sorted from 1 to sqrt(value) + * @private + */ + _factorize: function(value) { + var result = []; + var sqrt = Math.sqrt(value); + var i; + + for (i = 1; i < sqrt; i++) { + if (value % i === 0) { + result.push(i); + result.push(value / i); + } + } + if (sqrt === (sqrt | 0)) { // if value is a square number + result.push(sqrt); + } + + result.sort(function(a, b) { + return a - b; + }).pop(); + return result; + }, + + log10: Math.log10 || function(x) { + var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10. + // Check for whole powers of 10, + // which due to floating point rounding error should be corrected. + var powerOf10 = Math.round(exponent); + var isPowerOf10 = x === Math.pow(10, powerOf10); + + return isPowerOf10 ? powerOf10 : exponent; + } + }; + + var helpers_math = exports$2; + + // DEPRECATIONS + + /** + * Provided for backward compatibility, use Chart.helpers.math.log10 instead. + * @namespace Chart.helpers.log10 + * @deprecated since version 2.9.0 + * @todo remove at version 3 + * @private + */ + helpers_core.log10 = exports$2.log10; + + var getRtlAdapter = function(rectX, width) { + return { + x: function(x) { + return rectX + rectX + width - x; + }, + setWidth: function(w) { + width = w; + }, + textAlign: function(align) { + if (align === 'center') { + return align; + } + return align === 'right' ? 'left' : 'right'; + }, + xPlus: function(x, value) { + return x - value; + }, + leftForLtr: function(x, itemWidth) { + return x - itemWidth; + }, + }; + }; + + var getLtrAdapter = function() { + return { + x: function(x) { + return x; + }, + setWidth: function(w) { // eslint-disable-line no-unused-vars + }, + textAlign: function(align) { + return align; + }, + xPlus: function(x, value) { + return x + value; + }, + leftForLtr: function(x, _itemWidth) { // eslint-disable-line no-unused-vars + return x; + }, + }; + }; + + var getAdapter = function(rtl, rectX, width) { + return rtl ? getRtlAdapter(rectX, width) : getLtrAdapter(); + }; + + var overrideTextDirection = function(ctx, direction) { + var style, original; + if (direction === 'ltr' || direction === 'rtl') { + style = ctx.canvas.style; + original = [ + style.getPropertyValue('direction'), + style.getPropertyPriority('direction'), + ]; + + style.setProperty('direction', direction, 'important'); + ctx.prevTextDirection = original; + } + }; + + var restoreTextDirection = function(ctx) { + var original = ctx.prevTextDirection; + if (original !== undefined) { + delete ctx.prevTextDirection; + ctx.canvas.style.setProperty('direction', original[0], original[1]); + } + }; + + var helpers_rtl = { + getRtlAdapter: getAdapter, + overrideTextDirection: overrideTextDirection, + restoreTextDirection: restoreTextDirection, + }; + + var helpers$1 = helpers_core; + var easing = helpers_easing; + var canvas = helpers_canvas; + var options = helpers_options; + var math = helpers_math; + var rtl = helpers_rtl; + helpers$1.easing = easing; + helpers$1.canvas = canvas; + helpers$1.options = options; + helpers$1.math = math; + helpers$1.rtl = rtl; + + function interpolate(start, view, model, ease) { + var keys = Object.keys(model); + var i, ilen, key, actual, origin, target, type, c0, c1; + + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + + target = model[key]; + + // if a value is added to the model after pivot() has been called, the view + // doesn't contain it, so let's initialize the view to the target value. + if (!view.hasOwnProperty(key)) { + view[key] = target; + } + + actual = view[key]; + + if (actual === target || key[0] === '_') { + continue; + } + + if (!start.hasOwnProperty(key)) { + start[key] = actual; + } + + origin = start[key]; + + type = typeof target; + + if (type === typeof origin) { + if (type === 'string') { + c0 = chartjsColor(origin); + if (c0.valid) { + c1 = chartjsColor(target); + if (c1.valid) { + view[key] = c1.mix(c0, ease).rgbString(); + continue; + } + } + } else if (helpers$1.isFinite(origin) && helpers$1.isFinite(target)) { + view[key] = origin + (target - origin) * ease; + continue; + } + } + + view[key] = target; + } + } + + var Element = function(configuration) { + helpers$1.extend(this, configuration); + this.initialize.apply(this, arguments); + }; + + helpers$1.extend(Element.prototype, { + _type: undefined, + + initialize: function() { + this.hidden = false; + }, + + pivot: function() { + var me = this; + if (!me._view) { + me._view = helpers$1.extend({}, me._model); + } + me._start = {}; + return me; + }, + + transition: function(ease) { + var me = this; + var model = me._model; + var start = me._start; + var view = me._view; + + // No animation -> No Transition + if (!model || ease === 1) { + me._view = helpers$1.extend({}, model); + me._start = null; + return me; + } + + if (!view) { + view = me._view = {}; + } + + if (!start) { + start = me._start = {}; + } + + interpolate(start, view, model, ease); + + return me; + }, + + tooltipPosition: function() { + return { + x: this._model.x, + y: this._model.y + }; + }, + + hasValue: function() { + return helpers$1.isNumber(this._model.x) && helpers$1.isNumber(this._model.y); + } + }); + + Element.extend = helpers$1.inherits; + + var core_element = Element; + + var exports$3 = core_element.extend({ + chart: null, // the animation associated chart instance + currentStep: 0, // the current animation step + numSteps: 60, // default number of steps + easing: '', // the easing to use for this animation + render: null, // render function used by the animation service + + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes + }); + + var core_animation = exports$3; + + // DEPRECATIONS + + /** + * Provided for backward compatibility, use Chart.Animation instead + * @prop Chart.Animation#animationObject + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ + Object.defineProperty(exports$3.prototype, 'animationObject', { + get: function() { + return this; + } + }); + + /** + * Provided for backward compatibility, use Chart.Animation#chart instead + * @prop Chart.Animation#chartInstance + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ + Object.defineProperty(exports$3.prototype, 'chartInstance', { + get: function() { + return this.chart; + }, + set: function(value) { + this.chart = value; + } + }); + + core_defaults._set('global', { + animation: { + duration: 1000, + easing: 'easeOutQuart', + onProgress: helpers$1.noop, + onComplete: helpers$1.noop + } + }); + + var core_animations = { + animations: [], + request: null, + + /** + * @param {Chart} chart - The chart to animate. + * @param {Chart.Animation} animation - The animation that we will animate. + * @param {number} duration - The animation duration in ms. + * @param {boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions + */ + addAnimation: function(chart, animation, duration, lazy) { + var animations = this.animations; + var i, ilen; + + animation.chart = chart; + animation.startTime = Date.now(); + animation.duration = duration; + + if (!lazy) { + chart.animating = true; + } + + for (i = 0, ilen = animations.length; i < ilen; ++i) { + if (animations[i].chart === chart) { + animations[i] = animation; + return; + } + } + + animations.push(animation); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (animations.length === 1) { + this.requestAnimationFrame(); + } + }, + + cancelAnimation: function(chart) { + var index = helpers$1.findIndex(this.animations, function(animation) { + return animation.chart === chart; + }); + + if (index !== -1) { + this.animations.splice(index, 1); + chart.animating = false; + } + }, + + requestAnimationFrame: function() { + var me = this; + if (me.request === null) { + // Skip animation frame requests until the active one is executed. + // This can happen when processing mouse events, e.g. 'mousemove' + // and 'mouseout' events will trigger multiple renders. + me.request = helpers$1.requestAnimFrame.call(window, function() { + me.request = null; + me.startDigest(); + }); + } + }, + + /** + * @private + */ + startDigest: function() { + var me = this; + + me.advance(); + + // Do we have more stuff to animate? + if (me.animations.length > 0) { + me.requestAnimationFrame(); + } + }, + + /** + * @private + */ + advance: function() { + var animations = this.animations; + var animation, chart, numSteps, nextStep; + var i = 0; + + // 1 animation per chart, so we are looping charts here + while (i < animations.length) { + animation = animations[i]; + chart = animation.chart; + numSteps = animation.numSteps; + + // Make sure that currentStep starts at 1 + // https://github.com/chartjs/Chart.js/issues/6104 + nextStep = Math.floor((Date.now() - animation.startTime) / animation.duration * numSteps) + 1; + animation.currentStep = Math.min(nextStep, numSteps); + + helpers$1.callback(animation.render, [chart, animation], chart); + helpers$1.callback(animation.onAnimationProgress, [animation], chart); + + if (animation.currentStep >= numSteps) { + helpers$1.callback(animation.onAnimationComplete, [animation], chart); + chart.animating = false; + animations.splice(i, 1); + } else { + ++i; + } + } + } + }; + + var resolve = helpers$1.options.resolve; + + var arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift']; + + /** + * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', + * 'unshift') and notify the listener AFTER the array has been altered. Listeners are + * called on the 'onData*' callbacks (e.g. onDataPush, etc.) with same arguments. + */ + function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + + arrayEvents.forEach(function(key) { + var method = 'onData' + key.charAt(0).toUpperCase() + key.slice(1); + var base = array[key]; + + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value: function() { + var args = Array.prototype.slice.call(arguments); + var res = base.apply(this, args); + + helpers$1.each(array._chartjs.listeners, function(object) { + if (typeof object[method] === 'function') { + object[method].apply(object, args); + } + }); + + return res; + } + }); + }); + } + + /** + * Removes the given array event listener and cleanup extra attached properties (such as + * the _chartjs stub and overridden methods) if array doesn't have any more listeners. + */ + function unlistenArrayEvents(array, listener) { + var stub = array._chartjs; + if (!stub) { + return; + } + + var listeners = stub.listeners; + var index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + + if (listeners.length > 0) { + return; + } + + arrayEvents.forEach(function(key) { + delete array[key]; + }); + + delete array._chartjs; + } + + // Base class for all dataset controllers (line, bar, etc) + var DatasetController = function(chart, datasetIndex) { + this.initialize(chart, datasetIndex); + }; + + helpers$1.extend(DatasetController.prototype, { + + /** + * Element type used to generate a meta dataset (e.g. Chart.element.Line). + * @type {Chart.core.element} + */ + datasetElementType: null, + + /** + * Element type used to generate a meta data (e.g. Chart.element.Point). + * @type {Chart.core.element} + */ + dataElementType: null, + + /** + * Dataset element option keys to be resolved in _resolveDatasetElementOptions. + * A derived controller may override this to resolve controller-specific options. + * The keys defined here are for backward compatibility for legend styles. + * @private + */ + _datasetElementOptions: [ + 'backgroundColor', + 'borderCapStyle', + 'borderColor', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'borderWidth' + ], + + /** + * Data element option keys to be resolved in _resolveDataElementOptions. + * A derived controller may override this to resolve controller-specific options. + * The keys defined here are for backward compatibility for legend styles. + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'pointStyle' + ], + + initialize: function(chart, datasetIndex) { + var me = this; + me.chart = chart; + me.index = datasetIndex; + me.linkScales(); + me.addElements(); + me._type = me.getMeta().type; + }, + + updateIndex: function(datasetIndex) { + this.index = datasetIndex; + }, + + linkScales: function() { + var me = this; + var meta = me.getMeta(); + var chart = me.chart; + var scales = chart.scales; + var dataset = me.getDataset(); + var scalesOpts = chart.options.scales; + + if (meta.xAxisID === null || !(meta.xAxisID in scales) || dataset.xAxisID) { + meta.xAxisID = dataset.xAxisID || scalesOpts.xAxes[0].id; + } + if (meta.yAxisID === null || !(meta.yAxisID in scales) || dataset.yAxisID) { + meta.yAxisID = dataset.yAxisID || scalesOpts.yAxes[0].id; + } + }, + + getDataset: function() { + return this.chart.data.datasets[this.index]; + }, + + getMeta: function() { + return this.chart.getDatasetMeta(this.index); + }, + + getScaleForId: function(scaleID) { + return this.chart.scales[scaleID]; + }, + + /** + * @private + */ + _getValueScaleId: function() { + return this.getMeta().yAxisID; + }, + + /** + * @private + */ + _getIndexScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + _getValueScale: function() { + return this.getScaleForId(this._getValueScaleId()); + }, + + /** + * @private + */ + _getIndexScale: function() { + return this.getScaleForId(this._getIndexScaleId()); + }, + + reset: function() { + this._update(true); + }, + + /** + * @private + */ + destroy: function() { + if (this._data) { + unlistenArrayEvents(this._data, this); + } + }, + + createMetaDataset: function() { + var me = this; + var type = me.datasetElementType; + return type && new type({ + _chart: me.chart, + _datasetIndex: me.index + }); + }, + + createMetaData: function(index) { + var me = this; + var type = me.dataElementType; + return type && new type({ + _chart: me.chart, + _datasetIndex: me.index, + _index: index + }); + }, + + addElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data || []; + var metaData = meta.data; + var i, ilen; + + for (i = 0, ilen = data.length; i < ilen; ++i) { + metaData[i] = metaData[i] || me.createMetaData(i); + } + + meta.dataset = meta.dataset || me.createMetaDataset(); + }, + + addElementAndReset: function(index) { + var element = this.createMetaData(index); + this.getMeta().data.splice(index, 0, element); + this.updateElement(element, index, true); + }, + + buildOrUpdateElements: function() { + var me = this; + var dataset = me.getDataset(); + var data = dataset.data || (dataset.data = []); + + // In order to correctly handle data addition/deletion animation (an thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal meta data accordingly. + if (me._data !== data) { + if (me._data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(me._data, me); + } + + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, me); + } + me._data = data; + } + + // Re-sync meta data in case the user replaced the data array or if we missed + // any updates and so make sure that we handle number of datapoints changing. + me.resyncElements(); + }, + + /** + * Returns the merged user-supplied and default dataset-level options + * @private + */ + _configure: function() { + var me = this; + me._config = helpers$1.merge(Object.create(null), [ + me.chart.options.datasets[me._type], + me.getDataset(), + ], { + merger: function(key, target, source) { + if (key !== '_meta' && key !== 'data') { + helpers$1._merger(key, target, source); + } + } + }); + }, + + _update: function(reset) { + var me = this; + me._configure(); + me._cachedDataOpts = null; + me.update(reset); + }, + + update: helpers$1.noop, + + transition: function(easingValue) { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; + + for (; i < ilen; ++i) { + elements[i].transition(easingValue); + } + + if (meta.dataset) { + meta.dataset.transition(easingValue); + } + }, + + draw: function() { + var meta = this.getMeta(); + var elements = meta.data || []; + var ilen = elements.length; + var i = 0; + + if (meta.dataset) { + meta.dataset.draw(); + } + + for (; i < ilen; ++i) { + elements[i].draw(); + } + }, + + /** + * Returns a set of predefined style properties that should be used to represent the dataset + * or the data if the index is specified + * @param {number} index - data index + * @return {IStyleInterface} style object + */ + getStyle: function(index) { + var me = this; + var meta = me.getMeta(); + var dataset = meta.dataset; + var style; + + me._configure(); + if (dataset && index === undefined) { + style = me._resolveDatasetElementOptions(dataset || {}); + } else { + index = index || 0; + style = me._resolveDataElementOptions(meta.data[index] || {}, index); + } + + if (style.fill === false || style.fill === null) { + style.backgroundColor = style.borderColor; + } + + return style; + }, + + /** + * @private + */ + _resolveDatasetElementOptions: function(element, hover) { + var me = this; + var chart = me.chart; + var datasetOpts = me._config; + var custom = element.custom || {}; + var options = chart.options.elements[me.datasetElementType.prototype._type] || {}; + var elementOptions = me._datasetElementOptions; + var values = {}; + var i, ilen, key, readKey; + + // Scriptable options + var context = { + chart: chart, + dataset: me.getDataset(), + datasetIndex: me.index, + hover: hover + }; + + for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { + key = elementOptions[i]; + readKey = hover ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key; + values[key] = resolve([ + custom[readKey], + datasetOpts[readKey], + options[readKey] + ], context); + } + + return values; + }, + + /** + * @private + */ + _resolveDataElementOptions: function(element, index) { + var me = this; + var custom = element && element.custom; + var cached = me._cachedDataOpts; + if (cached && !custom) { + return cached; + } + var chart = me.chart; + var datasetOpts = me._config; + var options = chart.options.elements[me.dataElementType.prototype._type] || {}; + var elementOptions = me._dataElementOptions; + var values = {}; + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: me.getDataset(), + datasetIndex: me.index + }; + + // `resolve` sets cacheable to `false` if any option is indexed or scripted + var info = {cacheable: !custom}; + + var keys, i, ilen, key; + + custom = custom || {}; + + if (helpers$1.isArray(elementOptions)) { + for (i = 0, ilen = elementOptions.length; i < ilen; ++i) { + key = elementOptions[i]; + values[key] = resolve([ + custom[key], + datasetOpts[key], + options[key] + ], context, index, info); + } + } else { + keys = Object.keys(elementOptions); + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + values[key] = resolve([ + custom[key], + datasetOpts[elementOptions[key]], + datasetOpts[key], + options[key] + ], context, index, info); + } + } + + if (info.cacheable) { + me._cachedDataOpts = Object.freeze(values); + } + + return values; + }, + + removeHoverStyle: function(element) { + helpers$1.merge(element._model, element.$previousStyle || {}); + delete element.$previousStyle; + }, + + setHoverStyle: function(element) { + var dataset = this.chart.data.datasets[element._datasetIndex]; + var index = element._index; + var custom = element.custom || {}; + var model = element._model; + var getHoverColor = helpers$1.getHoverColor; + + element.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth + }; + + model.backgroundColor = resolve([custom.hoverBackgroundColor, dataset.hoverBackgroundColor, getHoverColor(model.backgroundColor)], undefined, index); + model.borderColor = resolve([custom.hoverBorderColor, dataset.hoverBorderColor, getHoverColor(model.borderColor)], undefined, index); + model.borderWidth = resolve([custom.hoverBorderWidth, dataset.hoverBorderWidth, model.borderWidth], undefined, index); + }, + + /** + * @private + */ + _removeDatasetHoverStyle: function() { + var element = this.getMeta().dataset; + + if (element) { + this.removeHoverStyle(element); + } + }, + + /** + * @private + */ + _setDatasetHoverStyle: function() { + var element = this.getMeta().dataset; + var prev = {}; + var i, ilen, key, keys, hoverOptions, model; + + if (!element) { + return; + } + + model = element._model; + hoverOptions = this._resolveDatasetElementOptions(element, true); + + keys = Object.keys(hoverOptions); + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + prev[key] = model[key]; + model[key] = hoverOptions[key]; + } + + element.$previousStyle = prev; + }, + + /** + * @private + */ + resyncElements: function() { + var me = this; + var meta = me.getMeta(); + var data = me.getDataset().data; + var numMeta = meta.data.length; + var numData = data.length; + + if (numData < numMeta) { + meta.data.splice(numData, numMeta - numData); + } else if (numData > numMeta) { + me.insertElements(numMeta, numData - numMeta); + } + }, + + /** + * @private + */ + insertElements: function(start, count) { + for (var i = 0; i < count; ++i) { + this.addElementAndReset(start + i); + } + }, + + /** + * @private + */ + onDataPush: function() { + var count = arguments.length; + this.insertElements(this.getDataset().data.length - count, count); + }, + + /** + * @private + */ + onDataPop: function() { + this.getMeta().data.pop(); + }, + + /** + * @private + */ + onDataShift: function() { + this.getMeta().data.shift(); + }, + + /** + * @private + */ + onDataSplice: function(start, count) { + this.getMeta().data.splice(start, count); + this.insertElements(start, arguments.length - 2); + }, + + /** + * @private + */ + onDataUnshift: function() { + this.insertElements(0, arguments.length); + } + }); + + DatasetController.extend = helpers$1.inherits; + + var core_datasetController = DatasetController; + + var TAU = Math.PI * 2; + + core_defaults._set('global', { + elements: { + arc: { + backgroundColor: core_defaults.global.defaultColor, + borderColor: '#fff', + borderWidth: 2, + borderAlign: 'center' + } + } + }); + + function clipArc(ctx, arc) { + var startAngle = arc.startAngle; + var endAngle = arc.endAngle; + var pixelMargin = arc.pixelMargin; + var angleMargin = pixelMargin / arc.outerRadius; + var x = arc.x; + var y = arc.y; + + // Draw an inner border by cliping the arc and drawing a double-width border + // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders + ctx.beginPath(); + ctx.arc(x, y, arc.outerRadius, startAngle - angleMargin, endAngle + angleMargin); + if (arc.innerRadius > pixelMargin) { + angleMargin = pixelMargin / arc.innerRadius; + ctx.arc(x, y, arc.innerRadius - pixelMargin, endAngle + angleMargin, startAngle - angleMargin, true); + } else { + ctx.arc(x, y, pixelMargin, endAngle + Math.PI / 2, startAngle - Math.PI / 2); + } + ctx.closePath(); + ctx.clip(); + } + + function drawFullCircleBorders(ctx, vm, arc, inner) { + var endAngle = arc.endAngle; + var i; + + if (inner) { + arc.endAngle = arc.startAngle + TAU; + clipArc(ctx, arc); + arc.endAngle = endAngle; + if (arc.endAngle === arc.startAngle && arc.fullCircles) { + arc.endAngle += TAU; + arc.fullCircles--; + } + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.startAngle + TAU, arc.startAngle, true); + for (i = 0; i < arc.fullCircles; ++i) { + ctx.stroke(); + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, vm.outerRadius, arc.startAngle, arc.startAngle + TAU); + for (i = 0; i < arc.fullCircles; ++i) { + ctx.stroke(); + } + } + + function drawBorder(ctx, vm, arc) { + var inner = vm.borderAlign === 'inner'; + + if (inner) { + ctx.lineWidth = vm.borderWidth * 2; + ctx.lineJoin = 'round'; + } else { + ctx.lineWidth = vm.borderWidth; + ctx.lineJoin = 'bevel'; + } + + if (arc.fullCircles) { + drawFullCircleBorders(ctx, vm, arc, inner); + } + + if (inner) { + clipArc(ctx, arc); + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, vm.outerRadius, arc.startAngle, arc.endAngle); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true); + ctx.closePath(); + ctx.stroke(); + } + + var element_arc = core_element.extend({ + _type: 'arc', + + inLabelRange: function(mouseX) { + var vm = this._view; + + if (vm) { + return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + vm.hoverRadius, 2)); + } + return false; + }, + + inRange: function(chartX, chartY) { + var vm = this._view; + + if (vm) { + var pointRelativePosition = helpers$1.getAngleFromPoint(vm, {x: chartX, y: chartY}); + var angle = pointRelativePosition.angle; + var distance = pointRelativePosition.distance; + + // Sanitise angle range + var startAngle = vm.startAngle; + var endAngle = vm.endAngle; + while (endAngle < startAngle) { + endAngle += TAU; + } + while (angle > endAngle) { + angle -= TAU; + } + while (angle < startAngle) { + angle += TAU; + } + + // Check if within the range of the open/close angle + var betweenAngles = (angle >= startAngle && angle <= endAngle); + var withinRadius = (distance >= vm.innerRadius && distance <= vm.outerRadius); + + return (betweenAngles && withinRadius); + } + return false; + }, + + getCenterPoint: function() { + var vm = this._view; + var halfAngle = (vm.startAngle + vm.endAngle) / 2; + var halfRadius = (vm.innerRadius + vm.outerRadius) / 2; + return { + x: vm.x + Math.cos(halfAngle) * halfRadius, + y: vm.y + Math.sin(halfAngle) * halfRadius + }; + }, + + getArea: function() { + var vm = this._view; + return Math.PI * ((vm.endAngle - vm.startAngle) / (2 * Math.PI)) * (Math.pow(vm.outerRadius, 2) - Math.pow(vm.innerRadius, 2)); + }, + + tooltipPosition: function() { + var vm = this._view; + var centreAngle = vm.startAngle + ((vm.endAngle - vm.startAngle) / 2); + var rangeFromCentre = (vm.outerRadius - vm.innerRadius) / 2 + vm.innerRadius; + + return { + x: vm.x + (Math.cos(centreAngle) * rangeFromCentre), + y: vm.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; + var pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0; + var arc = { + x: vm.x, + y: vm.y, + innerRadius: vm.innerRadius, + outerRadius: Math.max(vm.outerRadius - pixelMargin, 0), + pixelMargin: pixelMargin, + startAngle: vm.startAngle, + endAngle: vm.endAngle, + fullCircles: Math.floor(vm.circumference / TAU) + }; + var i; + + ctx.save(); + + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + + if (arc.fullCircles) { + arc.endAngle = arc.startAngle + TAU; + ctx.beginPath(); + ctx.arc(arc.x, arc.y, arc.outerRadius, arc.startAngle, arc.endAngle); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true); + ctx.closePath(); + for (i = 0; i < arc.fullCircles; ++i) { + ctx.fill(); + } + arc.endAngle = arc.startAngle + vm.circumference % TAU; + } + + ctx.beginPath(); + ctx.arc(arc.x, arc.y, arc.outerRadius, arc.startAngle, arc.endAngle); + ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true); + ctx.closePath(); + ctx.fill(); + + if (vm.borderWidth) { + drawBorder(ctx, vm, arc); + } + + ctx.restore(); + } + }); + + var valueOrDefault$1 = helpers$1.valueOrDefault; + + var defaultColor = core_defaults.global.defaultColor; + + core_defaults._set('global', { + elements: { + line: { + tension: 0.4, + backgroundColor: defaultColor, + borderWidth: 3, + borderColor: defaultColor, + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0.0, + borderJoinStyle: 'miter', + capBezierPoints: true, + fill: true, // do we fill in the area between the line and its base axis + } + } + }); + + var element_line = core_element.extend({ + _type: 'line', + + draw: function() { + var me = this; + var vm = me._view; + var ctx = me._chart.ctx; + var spanGaps = vm.spanGaps; + var points = me._children.slice(); // clone array + var globalDefaults = core_defaults.global; + var globalOptionLineElements = globalDefaults.elements.line; + var lastDrawnIndex = -1; + var closePath = me._loop; + var index, previous, currentVM; + + if (!points.length) { + return; + } + + if (me._loop) { + for (index = 0; index < points.length; ++index) { + previous = helpers$1.previousItem(points, index); + // If the line has an open path, shift the point array + if (!points[index]._view.skip && previous._view.skip) { + points = points.slice(index).concat(points.slice(0, index)); + closePath = spanGaps; + break; + } + } + // If the line has a close path, add the first point again + if (closePath) { + points.push(points[0]); + } + } + + ctx.save(); + + // Stroke Line Options + ctx.lineCap = vm.borderCapStyle || globalOptionLineElements.borderCapStyle; + + // IE 9 and 10 do not support line dash + if (ctx.setLineDash) { + ctx.setLineDash(vm.borderDash || globalOptionLineElements.borderDash); + } + + ctx.lineDashOffset = valueOrDefault$1(vm.borderDashOffset, globalOptionLineElements.borderDashOffset); + ctx.lineJoin = vm.borderJoinStyle || globalOptionLineElements.borderJoinStyle; + ctx.lineWidth = valueOrDefault$1(vm.borderWidth, globalOptionLineElements.borderWidth); + ctx.strokeStyle = vm.borderColor || globalDefaults.defaultColor; + + // Stroke Line + ctx.beginPath(); + + // First point moves to it's starting position no matter what + currentVM = points[0]._view; + if (!currentVM.skip) { + ctx.moveTo(currentVM.x, currentVM.y); + lastDrawnIndex = 0; + } + + for (index = 1; index < points.length; ++index) { + currentVM = points[index]._view; + previous = lastDrawnIndex === -1 ? helpers$1.previousItem(points, index) : points[lastDrawnIndex]; + + if (!currentVM.skip) { + if ((lastDrawnIndex !== (index - 1) && !spanGaps) || lastDrawnIndex === -1) { + // There was a gap and this is the first point after the gap + ctx.moveTo(currentVM.x, currentVM.y); + } else { + // Line to next point + helpers$1.canvas.lineTo(ctx, previous._view, currentVM); + } + lastDrawnIndex = index; + } + } + + if (closePath) { + ctx.closePath(); + } + + ctx.stroke(); + ctx.restore(); + } + }); + + var valueOrDefault$2 = helpers$1.valueOrDefault; + + var defaultColor$1 = core_defaults.global.defaultColor; + + core_defaults._set('global', { + elements: { + point: { + radius: 3, + pointStyle: 'circle', + backgroundColor: defaultColor$1, + borderColor: defaultColor$1, + borderWidth: 1, + // Hover + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1 + } + } + }); + + function xRange(mouseX) { + var vm = this._view; + return vm ? (Math.abs(mouseX - vm.x) < vm.radius + vm.hitRadius) : false; + } + + function yRange(mouseY) { + var vm = this._view; + return vm ? (Math.abs(mouseY - vm.y) < vm.radius + vm.hitRadius) : false; + } + + var element_point = core_element.extend({ + _type: 'point', + + inRange: function(mouseX, mouseY) { + var vm = this._view; + return vm ? ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(vm.hitRadius + vm.radius, 2)) : false; + }, + + inLabelRange: xRange, + inXRange: xRange, + inYRange: yRange, + + getCenterPoint: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y + }; + }, + + getArea: function() { + return Math.PI * Math.pow(this._view.radius, 2); + }, + + tooltipPosition: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y, + padding: vm.radius + vm.borderWidth + }; + }, + + draw: function(chartArea) { + var vm = this._view; + var ctx = this._chart.ctx; + var pointStyle = vm.pointStyle; + var rotation = vm.rotation; + var radius = vm.radius; + var x = vm.x; + var y = vm.y; + var globalDefaults = core_defaults.global; + var defaultColor = globalDefaults.defaultColor; // eslint-disable-line no-shadow + + if (vm.skip) { + return; + } + + // Clipping for Points. + if (chartArea === undefined || helpers$1.canvas._isPointInArea(vm, chartArea)) { + ctx.strokeStyle = vm.borderColor || defaultColor; + ctx.lineWidth = valueOrDefault$2(vm.borderWidth, globalDefaults.elements.point.borderWidth); + ctx.fillStyle = vm.backgroundColor || defaultColor; + helpers$1.canvas.drawPoint(ctx, pointStyle, radius, x, y, rotation); + } + } + }); + + var defaultColor$2 = core_defaults.global.defaultColor; + + core_defaults._set('global', { + elements: { + rectangle: { + backgroundColor: defaultColor$2, + borderColor: defaultColor$2, + borderSkipped: 'bottom', + borderWidth: 0 + } + } + }); + + function isVertical(vm) { + return vm && vm.width !== undefined; + } + + /** + * Helper function to get the bounds of the bar regardless of the orientation + * @param bar {Chart.Element.Rectangle} the bar + * @return {Bounds} bounds of the bar + * @private + */ + function getBarBounds(vm) { + var x1, x2, y1, y2, half; + + if (isVertical(vm)) { + half = vm.width / 2; + x1 = vm.x - half; + x2 = vm.x + half; + y1 = Math.min(vm.y, vm.base); + y2 = Math.max(vm.y, vm.base); + } else { + half = vm.height / 2; + x1 = Math.min(vm.x, vm.base); + x2 = Math.max(vm.x, vm.base); + y1 = vm.y - half; + y2 = vm.y + half; + } + + return { + left: x1, + top: y1, + right: x2, + bottom: y2 + }; + } + + function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; + } + + function parseBorderSkipped(vm) { + var edge = vm.borderSkipped; + var res = {}; + + if (!edge) { + return res; + } + + if (vm.horizontal) { + if (vm.base > vm.x) { + edge = swap(edge, 'left', 'right'); + } + } else if (vm.base < vm.y) { + edge = swap(edge, 'bottom', 'top'); + } + + res[edge] = true; + return res; + } + + function parseBorderWidth(vm, maxW, maxH) { + var value = vm.borderWidth; + var skip = parseBorderSkipped(vm); + var t, r, b, l; + + if (helpers$1.isObject(value)) { + t = +value.top || 0; + r = +value.right || 0; + b = +value.bottom || 0; + l = +value.left || 0; + } else { + t = r = b = l = +value || 0; + } + + return { + t: skip.top || (t < 0) ? 0 : t > maxH ? maxH : t, + r: skip.right || (r < 0) ? 0 : r > maxW ? maxW : r, + b: skip.bottom || (b < 0) ? 0 : b > maxH ? maxH : b, + l: skip.left || (l < 0) ? 0 : l > maxW ? maxW : l + }; + } + + function boundingRects(vm) { + var bounds = getBarBounds(vm); + var width = bounds.right - bounds.left; + var height = bounds.bottom - bounds.top; + var border = parseBorderWidth(vm, width / 2, height / 2); + + return { + outer: { + x: bounds.left, + y: bounds.top, + w: width, + h: height + }, + inner: { + x: bounds.left + border.l, + y: bounds.top + border.t, + w: width - border.l - border.r, + h: height - border.t - border.b + } + }; + } + + function inRange(vm, x, y) { + var skipX = x === null; + var skipY = y === null; + var bounds = !vm || (skipX && skipY) ? false : getBarBounds(vm); + + return bounds + && (skipX || x >= bounds.left && x <= bounds.right) + && (skipY || y >= bounds.top && y <= bounds.bottom); + } + + var element_rectangle = core_element.extend({ + _type: 'rectangle', + + draw: function() { + var ctx = this._chart.ctx; + var vm = this._view; + var rects = boundingRects(vm); + var outer = rects.outer; + var inner = rects.inner; + + ctx.fillStyle = vm.backgroundColor; + ctx.fillRect(outer.x, outer.y, outer.w, outer.h); + + if (outer.w === inner.w && outer.h === inner.h) { + return; + } + + ctx.save(); + ctx.beginPath(); + ctx.rect(outer.x, outer.y, outer.w, outer.h); + ctx.clip(); + ctx.fillStyle = vm.borderColor; + ctx.rect(inner.x, inner.y, inner.w, inner.h); + ctx.fill('evenodd'); + ctx.restore(); + }, + + height: function() { + var vm = this._view; + return vm.base - vm.y; + }, + + inRange: function(mouseX, mouseY) { + return inRange(this._view, mouseX, mouseY); + }, + + inLabelRange: function(mouseX, mouseY) { + var vm = this._view; + return isVertical(vm) + ? inRange(vm, mouseX, null) + : inRange(vm, null, mouseY); + }, + + inXRange: function(mouseX) { + return inRange(this._view, mouseX, null); + }, + + inYRange: function(mouseY) { + return inRange(this._view, null, mouseY); + }, + + getCenterPoint: function() { + var vm = this._view; + var x, y; + if (isVertical(vm)) { + x = vm.x; + y = (vm.y + vm.base) / 2; + } else { + x = (vm.x + vm.base) / 2; + y = vm.y; + } + + return {x: x, y: y}; + }, + + getArea: function() { + var vm = this._view; + + return isVertical(vm) + ? vm.width * Math.abs(vm.y - vm.base) + : vm.height * Math.abs(vm.x - vm.base); + }, + + tooltipPosition: function() { + var vm = this._view; + return { + x: vm.x, + y: vm.y + }; + } + }); + + var elements = {}; + var Arc = element_arc; + var Line = element_line; + var Point = element_point; + var Rectangle = element_rectangle; + elements.Arc = Arc; + elements.Line = Line; + elements.Point = Point; + elements.Rectangle = Rectangle; + + var deprecated = helpers$1._deprecated; + var valueOrDefault$3 = helpers$1.valueOrDefault; + + core_defaults._set('bar', { + hover: { + mode: 'label' + }, + + scales: { + xAxes: [{ + type: 'category', + offset: true, + gridLines: { + offsetGridLines: true + } + }], + + yAxes: [{ + type: 'linear' + }] + } + }); + + core_defaults._set('global', { + datasets: { + bar: { + categoryPercentage: 0.8, + barPercentage: 0.9 + } + } + }); + + /** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ + function computeMinSampleSize(scale, pixels) { + var min = scale._length; + var prev, curr, i, ilen; + + for (i = 1, ilen = pixels.length; i < ilen; ++i) { + min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1])); + } + + for (i = 0, ilen = scale.getTicks().length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, Math.abs(curr - prev)) : min; + prev = curr; + } + + return min; + } + + /** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ + function computeFitCategoryTraits(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var curr = ruler.pixels[index]; + var min = helpers$1.isNullOrUndef(thickness) + ? computeMinSampleSize(ruler.scale, ruler.pixels) + : -1; + var size, ratio; + + if (helpers$1.isNullOrUndef(thickness)) { + size = min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; + } + + /** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ + function computeFlexCategoryTraits(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale size. + prev = curr - (next === null ? ruler.end - ruler.start : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + start = curr - (curr - Math.min(prev, next)) / 2 * percent; + size = Math.abs(next - prev) / 2 * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; + } + + var controller_bar = core_datasetController.extend({ + + dataElementType: elements.Rectangle, + + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderSkipped', + 'borderWidth', + 'barPercentage', + 'barThickness', + 'categoryPercentage', + 'maxBarThickness', + 'minBarLength' + ], + + initialize: function() { + var me = this; + var meta, scaleOpts; + + core_datasetController.prototype.initialize.apply(me, arguments); + + meta = me.getMeta(); + meta.stack = me.getDataset().stack; + meta.bar = true; + + scaleOpts = me._getIndexScale().options; + deprecated('bar chart', scaleOpts.barPercentage, 'scales.[x/y]Axes.barPercentage', 'dataset.barPercentage'); + deprecated('bar chart', scaleOpts.barThickness, 'scales.[x/y]Axes.barThickness', 'dataset.barThickness'); + deprecated('bar chart', scaleOpts.categoryPercentage, 'scales.[x/y]Axes.categoryPercentage', 'dataset.categoryPercentage'); + deprecated('bar chart', me._getValueScale().options.minBarLength, 'scales.[x/y]Axes.minBarLength', 'dataset.minBarLength'); + deprecated('bar chart', scaleOpts.maxBarThickness, 'scales.[x/y]Axes.maxBarThickness', 'dataset.maxBarThickness'); + }, + + update: function(reset) { + var me = this; + var rects = me.getMeta().data; + var i, ilen; + + me._ruler = me.getRuler(); + + for (i = 0, ilen = rects.length; i < ilen; ++i) { + me.updateElement(rects[i], i, reset); + } + }, + + updateElement: function(rectangle, index, reset) { + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + var options = me._resolveDataElementOptions(rectangle, index); + + rectangle._xScale = me.getScaleForId(meta.xAxisID); + rectangle._yScale = me.getScaleForId(meta.yAxisID); + rectangle._datasetIndex = me.index; + rectangle._index = index; + rectangle._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderSkipped: options.borderSkipped, + borderWidth: options.borderWidth, + datasetLabel: dataset.label, + label: me.chart.data.labels[index] + }; + + if (helpers$1.isArray(dataset.data[index])) { + rectangle._model.borderSkipped = null; + } + + me._updateElementGeometry(rectangle, index, reset, options); + + rectangle.pivot(); + }, + + /** + * @private + */ + _updateElementGeometry: function(rectangle, index, reset, options) { + var me = this; + var model = rectangle._model; + var vscale = me._getValueScale(); + var base = vscale.getBasePixel(); + var horizontal = vscale.isHorizontal(); + var ruler = me._ruler || me.getRuler(); + var vpixels = me.calculateBarValuePixels(me.index, index, options); + var ipixels = me.calculateBarIndexPixels(me.index, index, ruler, options); + + model.horizontal = horizontal; + model.base = reset ? base : vpixels.base; + model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; + model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; + model.height = horizontal ? ipixels.size : undefined; + model.width = horizontal ? undefined : ipixels.size; + }, + + /** + * Returns the stacks based on groups and bar visibility. + * @param {number} [last] - The dataset index + * @returns {string[]} The list of stack IDs + * @private + */ + _getStacks: function(last) { + var me = this; + var scale = me._getIndexScale(); + var metasets = scale._getMatchingVisibleMetas(me._type); + var stacked = scale.options.stacked; + var ilen = metasets.length; + var stacks = []; + var i, meta; + + for (i = 0; i < ilen; ++i) { + meta = metasets[i]; + // stacked | meta.stack + // | found | not found | undefined + // false | x | x | x + // true | | x | + // undefined | | x | x + if (stacked === false || stacks.indexOf(meta.stack) === -1 || + (stacked === undefined && meta.stack === undefined)) { + stacks.push(meta.stack); + } + if (meta.index === last) { + break; + } + } + + return stacks; + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function() { + return this._getStacks().length; + }, + + /** + * Returns the stack index for the given dataset based on groups and bar visibility. + * @param {number} [datasetIndex] - The dataset index + * @param {string} [name] - The stack name to find + * @returns {number} The stack index + * @private + */ + getStackIndex: function(datasetIndex, name) { + var stacks = this._getStacks(datasetIndex); + var index = (name !== undefined) + ? stacks.indexOf(name) + : -1; // indexOf returns -1 if element is not present + + return (index === -1) + ? stacks.length - 1 + : index; + }, + + /** + * @private + */ + getRuler: function() { + var me = this; + var scale = me._getIndexScale(); + var pixels = []; + var i, ilen; + + for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { + pixels.push(scale.getPixelForValue(null, i, me.index)); + } + + return { + pixels: pixels, + start: scale._startPixel, + end: scale._endPixel, + stackCount: me.getStackCount(), + scale: scale + }; + }, + + /** + * Note: pixel values are not clamped to the scale area. + * @private + */ + calculateBarValuePixels: function(datasetIndex, index, options) { + var me = this; + var chart = me.chart; + var scale = me._getValueScale(); + var isHorizontal = scale.isHorizontal(); + var datasets = chart.data.datasets; + var metasets = scale._getMatchingVisibleMetas(me._type); + var value = scale._parseValue(datasets[datasetIndex].data[index]); + var minBarLength = options.minBarLength; + var stacked = scale.options.stacked; + var stack = me.getMeta().stack; + var start = value.start === undefined ? 0 : value.max >= 0 && value.min >= 0 ? value.min : value.max; + var length = value.start === undefined ? value.end : value.max >= 0 && value.min >= 0 ? value.max - value.min : value.min - value.max; + var ilen = metasets.length; + var i, imeta, ivalue, base, head, size, stackLength; + + if (stacked || (stacked === undefined && stack !== undefined)) { + for (i = 0; i < ilen; ++i) { + imeta = metasets[i]; + + if (imeta.index === datasetIndex) { + break; + } + + if (imeta.stack === stack) { + stackLength = scale._parseValue(datasets[imeta.index].data[index]); + ivalue = stackLength.start === undefined ? stackLength.end : stackLength.min >= 0 && stackLength.max >= 0 ? stackLength.max : stackLength.min; + + if ((value.min < 0 && ivalue < 0) || (value.max >= 0 && ivalue > 0)) { + start += ivalue; + } + } + } + } + + base = scale.getPixelForValue(start); + head = scale.getPixelForValue(start + length); + size = head - base; + + if (minBarLength !== undefined && Math.abs(size) < minBarLength) { + size = minBarLength; + if (length >= 0 && !isHorizontal || length < 0 && isHorizontal) { + head = base - minBarLength; + } else { + head = base + minBarLength; + } + } + + return { + size: size, + base: base, + head: head, + center: head + size / 2 + }; + }, + + /** + * @private + */ + calculateBarIndexPixels: function(datasetIndex, index, ruler, options) { + var me = this; + var range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options) + : computeFitCategoryTraits(index, ruler, options); + + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + valueOrDefault$3(options.maxBarThickness, Infinity), + range.chunk * range.ratio); + + return { + base: center - size / 2, + head: center + size / 2, + center: center, + size: size + }; + }, + + draw: function() { + var me = this; + var chart = me.chart; + var scale = me._getValueScale(); + var rects = me.getMeta().data; + var dataset = me.getDataset(); + var ilen = rects.length; + var i = 0; + + helpers$1.canvas.clipArea(chart.ctx, chart.chartArea); + + for (; i < ilen; ++i) { + var val = scale._parseValue(dataset.data[i]); + if (!isNaN(val.min) && !isNaN(val.max)) { + rects[i].draw(); + } + } + + helpers$1.canvas.unclipArea(chart.ctx); + }, + + /** + * @private + */ + _resolveDataElementOptions: function() { + var me = this; + var values = helpers$1.extend({}, core_datasetController.prototype._resolveDataElementOptions.apply(me, arguments)); + var indexOpts = me._getIndexScale().options; + var valueOpts = me._getValueScale().options; + + values.barPercentage = valueOrDefault$3(indexOpts.barPercentage, values.barPercentage); + values.barThickness = valueOrDefault$3(indexOpts.barThickness, values.barThickness); + values.categoryPercentage = valueOrDefault$3(indexOpts.categoryPercentage, values.categoryPercentage); + values.maxBarThickness = valueOrDefault$3(indexOpts.maxBarThickness, values.maxBarThickness); + values.minBarLength = valueOrDefault$3(valueOpts.minBarLength, values.minBarLength); + + return values; + } + + }); + + var valueOrDefault$4 = helpers$1.valueOrDefault; + var resolve$1 = helpers$1.options.resolve; + + core_defaults._set('bubble', { + hover: { + mode: 'single' + }, + + scales: { + xAxes: [{ + type: 'linear', // bubble should probably use a linear scale by default + position: 'bottom', + id: 'x-axis-0' // need an ID so datasets can reference the scale + }], + yAxes: [{ + type: 'linear', + position: 'left', + id: 'y-axis-0' + }] + }, + + tooltips: { + callbacks: { + title: function() { + // Title doesn't make sense for scatter since we format the data as a point + return ''; + }, + label: function(item, data) { + var datasetLabel = data.datasets[item.datasetIndex].label || ''; + var dataPoint = data.datasets[item.datasetIndex].data[item.index]; + return datasetLabel + ': (' + item.xLabel + ', ' + item.yLabel + ', ' + dataPoint.r + ')'; + } + } + } + }); + + var controller_bubble = core_datasetController.extend({ + /** + * @protected + */ + dataElementType: elements.Point, + + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + 'hoverRadius', + 'hitRadius', + 'pointStyle', + 'rotation' + ], + + /** + * @protected + */ + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var points = meta.data; + + // Update Points + helpers$1.each(points, function(point, index) { + me.updateElement(point, index, reset); + }); + }, + + /** + * @protected + */ + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var custom = point.custom || {}; + var xScale = me.getScaleForId(meta.xAxisID); + var yScale = me.getScaleForId(meta.yAxisID); + var options = me._resolveDataElementOptions(point, index); + var data = me.getDataset().data[index]; + var dsIndex = me.index; + + var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex); + var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex); + + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = dsIndex; + point._index = index; + point._model = { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + hitRadius: options.hitRadius, + pointStyle: options.pointStyle, + rotation: options.rotation, + radius: reset ? 0 : options.radius, + skip: custom.skip || isNaN(x) || isNaN(y), + x: x, + y: y, + }; + + point.pivot(); + }, + + /** + * @protected + */ + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + var getHoverColor = helpers$1.getHoverColor; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = valueOrDefault$4(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$4(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$4(options.hoverBorderWidth, options.borderWidth); + model.radius = options.radius + options.hoverRadius; + }, + + /** + * @private + */ + _resolveDataElementOptions: function(point, index) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var custom = point.custom || {}; + var data = dataset.data[index] || {}; + var values = core_datasetController.prototype._resolveDataElementOptions.apply(me, arguments); + + // Scriptable options + var context = { + chart: chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + // In case values were cached (and thus frozen), we need to clone the values + if (me._cachedDataOpts === values) { + values = helpers$1.extend({}, values); + } + + // Custom radius resolution + values.radius = resolve$1([ + custom.radius, + data.r, + me._config.radius, + chart.options.elements.point.radius + ], context, index); + + return values; + } + }); + + var valueOrDefault$5 = helpers$1.valueOrDefault; + + var PI$1 = Math.PI; + var DOUBLE_PI$1 = PI$1 * 2; + var HALF_PI$1 = PI$1 / 2; + + core_defaults._set('doughnut', { + animation: { + // Boolean - Whether we animate the rotation of the Doughnut + animateRotate: true, + // Boolean - Whether we animate scaling the Doughnut from the centre + animateScale: false + }, + hover: { + mode: 'single' + }, + legendCallback: function(chart) { + var list = document.createElement('ul'); + var data = chart.data; + var datasets = data.datasets; + var labels = data.labels; + var i, ilen, listItem, listItemSpan; + + list.setAttribute('class', chart.id + '-legend'); + if (datasets.length) { + for (i = 0, ilen = datasets[0].data.length; i < ilen; ++i) { + listItem = list.appendChild(document.createElement('li')); + listItemSpan = listItem.appendChild(document.createElement('span')); + listItemSpan.style.backgroundColor = datasets[0].backgroundColor[i]; + if (labels[i]) { + listItem.appendChild(document.createTextNode(labels[i])); + } + } + } + + return list.outerHTML; + }, + legend: { + labels: { + generateLabels: function(chart) { + var data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map(function(label, i) { + var meta = chart.getDatasetMeta(0); + var style = meta.controller.getStyle(i); + + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + hidden: isNaN(data.datasets[0].data[i]) || meta.data[i].hidden, + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick: function(e, legendItem) { + var index = legendItem.index; + var chart = this.chart; + var i, ilen, meta; + + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + // toggle visibility of index if exists + if (meta.data[index]) { + meta.data[index].hidden = !meta.data[index].hidden; + } + } + + chart.update(); + } + }, + + // The percentage of the chart that we cut out of the middle. + cutoutPercentage: 50, + + // The rotation of the chart, where the first data arc begins. + rotation: -HALF_PI$1, + + // The total circumference of the chart. + circumference: DOUBLE_PI$1, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title: function() { + return ''; + }, + label: function(tooltipItem, data) { + var dataLabel = data.labels[tooltipItem.index]; + var value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + + if (helpers$1.isArray(dataLabel)) { + // show value on first line of multiline label + // need to clone because we are changing the value + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + + return dataLabel; + } + } + } + }); + + var controller_doughnut = core_datasetController.extend({ + + dataElementType: elements.Arc, + + linkScales: helpers$1.noop, + + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ], + + // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly + getRingIndex: function(datasetIndex) { + var ringIndex = 0; + + for (var j = 0; j < datasetIndex; ++j) { + if (this.chart.isDatasetVisible(j)) { + ++ringIndex; + } + } + + return ringIndex; + }, + + update: function(reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var ratioX = 1; + var ratioY = 1; + var offsetX = 0; + var offsetY = 0; + var meta = me.getMeta(); + var arcs = meta.data; + var cutout = opts.cutoutPercentage / 100 || 0; + var circumference = opts.circumference; + var chartWeight = me._getRingWeight(me.index); + var maxWidth, maxHeight, i, ilen; + + // If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc + if (circumference < DOUBLE_PI$1) { + var startAngle = opts.rotation % DOUBLE_PI$1; + startAngle += startAngle >= PI$1 ? -DOUBLE_PI$1 : startAngle < -PI$1 ? DOUBLE_PI$1 : 0; + var endAngle = startAngle + circumference; + var startX = Math.cos(startAngle); + var startY = Math.sin(startAngle); + var endX = Math.cos(endAngle); + var endY = Math.sin(endAngle); + var contains0 = (startAngle <= 0 && endAngle >= 0) || endAngle >= DOUBLE_PI$1; + var contains90 = (startAngle <= HALF_PI$1 && endAngle >= HALF_PI$1) || endAngle >= DOUBLE_PI$1 + HALF_PI$1; + var contains180 = startAngle === -PI$1 || endAngle >= PI$1; + var contains270 = (startAngle <= -HALF_PI$1 && endAngle >= -HALF_PI$1) || endAngle >= PI$1 + HALF_PI$1; + var minX = contains180 ? -1 : Math.min(startX, startX * cutout, endX, endX * cutout); + var minY = contains270 ? -1 : Math.min(startY, startY * cutout, endY, endY * cutout); + var maxX = contains0 ? 1 : Math.max(startX, startX * cutout, endX, endX * cutout); + var maxY = contains90 ? 1 : Math.max(startY, startY * cutout, endY, endY * cutout); + ratioX = (maxX - minX) / 2; + ratioY = (maxY - minY) / 2; + offsetX = -(maxX + minX) / 2; + offsetY = -(maxY + minY) / 2; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + arcs[i]._options = me._resolveDataElementOptions(arcs[i], i); + } + + chart.borderWidth = me.getMaxBorderWidth(); + maxWidth = (chartArea.right - chartArea.left - chart.borderWidth) / ratioX; + maxHeight = (chartArea.bottom - chartArea.top - chart.borderWidth) / ratioY; + chart.outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); + chart.innerRadius = Math.max(chart.outerRadius * cutout, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / (me._getVisibleDatasetWeightTotal() || 1); + chart.offsetX = offsetX * chart.outerRadius; + chart.offsetY = offsetY * chart.outerRadius; + + meta.total = me.calculateTotal(); + + me.outerRadius = chart.outerRadius - chart.radiusLength * me._getRingWeightOffset(me.index); + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength * chartWeight, 0); + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + me.updateElement(arcs[i], i, reset); + } + }, + + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var animationOpts = opts.animation; + var centerX = (chartArea.left + chartArea.right) / 2; + var centerY = (chartArea.top + chartArea.bottom) / 2; + var startAngle = opts.rotation; // non reset case handled later + var endAngle = opts.rotation; // non reset case handled later + var dataset = me.getDataset(); + var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / DOUBLE_PI$1); + var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; + var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; + var options = arc._options || {}; + + helpers$1.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + + // Desired view properties + _model: { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + borderAlign: options.borderAlign, + x: centerX + chart.offsetX, + y: centerY + chart.offsetY, + startAngle: startAngle, + endAngle: endAngle, + circumference: circumference, + outerRadius: outerRadius, + innerRadius: innerRadius, + label: helpers$1.valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index]) + } + }); + + var model = arc._model; + + // Set correct angles if not resetting + if (!reset || !animationOpts.animateRotate) { + if (index === 0) { + model.startAngle = opts.rotation; + } else { + model.startAngle = me.getMeta().data[index - 1]._model.endAngle; + } + + model.endAngle = model.startAngle + model.circumference; + } + + arc.pivot(); + }, + + calculateTotal: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var total = 0; + var value; + + helpers$1.each(meta.data, function(element, index) { + value = dataset.data[index]; + if (!isNaN(value) && !element.hidden) { + total += Math.abs(value); + } + }); + + /* if (total === 0) { + total = NaN; + }*/ + + return total; + }, + + calculateCircumference: function(value) { + var total = this.getMeta().total; + if (total > 0 && !isNaN(value)) { + return DOUBLE_PI$1 * (Math.abs(value) / total); + } + return 0; + }, + + // gets the max border or hover width to properly scale pie charts + getMaxBorderWidth: function(arcs) { + var me = this; + var max = 0; + var chart = me.chart; + var i, ilen, meta, arc, controller, options, borderWidth, hoverWidth; + + if (!arcs) { + // Find the outmost visible dataset + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + meta = chart.getDatasetMeta(i); + arcs = meta.data; + if (i !== me.index) { + controller = meta.controller; + } + break; + } + } + } + + if (!arcs) { + return 0; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + arc = arcs[i]; + if (controller) { + controller._configure(); + options = controller._resolveDataElementOptions(arc, i); + } else { + options = arc._options; + } + if (options.borderAlign !== 'inner') { + borderWidth = options.borderWidth; + hoverWidth = options.hoverBorderWidth; + + max = borderWidth > max ? borderWidth : max; + max = hoverWidth > max ? hoverWidth : max; + } + } + return max; + }, + + /** + * @protected + */ + setHoverStyle: function(arc) { + var model = arc._model; + var options = arc._options; + var getHoverColor = helpers$1.getHoverColor; + + arc.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + }; + + model.backgroundColor = valueOrDefault$5(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$5(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$5(options.hoverBorderWidth, options.borderWidth); + }, + + /** + * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly + * @private + */ + _getRingWeightOffset: function(datasetIndex) { + var ringWeightOffset = 0; + + for (var i = 0; i < datasetIndex; ++i) { + if (this.chart.isDatasetVisible(i)) { + ringWeightOffset += this._getRingWeight(i); + } + } + + return ringWeightOffset; + }, + + /** + * @private + */ + _getRingWeight: function(dataSetIndex) { + return Math.max(valueOrDefault$5(this.chart.data.datasets[dataSetIndex].weight, 1), 0); + }, + + /** + * Returns the sum of all visibile data set weights. This value can be 0. + * @private + */ + _getVisibleDatasetWeightTotal: function() { + return this._getRingWeightOffset(this.chart.data.datasets.length); + } + }); + + core_defaults._set('horizontalBar', { + hover: { + mode: 'index', + axis: 'y' + }, + + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }], + + yAxes: [{ + type: 'category', + position: 'left', + offset: true, + gridLines: { + offsetGridLines: true + } + }] + }, + + elements: { + rectangle: { + borderSkipped: 'left' + } + }, + + tooltips: { + mode: 'index', + axis: 'y' + } + }); + + core_defaults._set('global', { + datasets: { + horizontalBar: { + categoryPercentage: 0.8, + barPercentage: 0.9 + } + } + }); + + var controller_horizontalBar = controller_bar.extend({ + /** + * @private + */ + _getValueScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + _getIndexScaleId: function() { + return this.getMeta().yAxisID; + } + }); + + var valueOrDefault$6 = helpers$1.valueOrDefault; + var resolve$2 = helpers$1.options.resolve; + var isPointInArea = helpers$1.canvas._isPointInArea; + + core_defaults._set('line', { + showLines: true, + spanGaps: false, + + hover: { + mode: 'label' + }, + + scales: { + xAxes: [{ + type: 'category', + id: 'x-axis-0' + }], + yAxes: [{ + type: 'linear', + id: 'y-axis-0' + }] + } + }); + + function scaleClip(scale, halfBorderWidth) { + var tickOpts = scale && scale.options.ticks || {}; + var reverse = tickOpts.reverse; + var min = tickOpts.min === undefined ? halfBorderWidth : 0; + var max = tickOpts.max === undefined ? halfBorderWidth : 0; + return { + start: reverse ? max : min, + end: reverse ? min : max + }; + } + + function defaultClip(xScale, yScale, borderWidth) { + var halfBorderWidth = borderWidth / 2; + var x = scaleClip(xScale, halfBorderWidth); + var y = scaleClip(yScale, halfBorderWidth); + + return { + top: y.end, + right: x.end, + bottom: y.start, + left: x.start + }; + } + + function toClip(value) { + var t, r, b, l; + + if (helpers$1.isObject(value)) { + t = value.top; + r = value.right; + b = value.bottom; + l = value.left; + } else { + t = r = b = l = value; + } + + return { + top: t, + right: r, + bottom: b, + left: l + }; + } + + + var controller_line = core_datasetController.extend({ + + datasetElementType: elements.Line, + + dataElementType: elements.Point, + + /** + * @private + */ + _datasetElementOptions: [ + 'backgroundColor', + 'borderCapStyle', + 'borderColor', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'borderWidth', + 'cubicInterpolationMode', + 'fill' + ], + + /** + * @private + */ + _dataElementOptions: { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var options = me.chart.options; + var config = me._config; + var showLine = me._showLine = valueOrDefault$6(config.showLine, options.showLines); + var i, ilen; + + me._xScale = me.getScaleForId(meta.xAxisID); + me._yScale = me.getScaleForId(meta.yAxisID); + + // Update Line + if (showLine) { + // Compatibility: If the properties are defined with only the old name, use those values + if (config.tension !== undefined && config.lineTension === undefined) { + config.lineTension = config.tension; + } + + // Utility + line._scale = me._yScale; + line._datasetIndex = me.index; + // Data + line._children = points; + // Model + line._model = me._resolveDatasetElementOptions(line); + + line.pivot(); + } + + // Update Points + for (i = 0, ilen = points.length; i < ilen; ++i) { + me.updateElement(points[i], i, reset); + } + + if (showLine && line._model.tension !== 0) { + me.updateBezierControlPoints(); + } + + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; ++i) { + points[i].pivot(); + } + }, + + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var custom = point.custom || {}; + var dataset = me.getDataset(); + var datasetIndex = me.index; + var value = dataset.data[index]; + var xScale = me._xScale; + var yScale = me._yScale; + var lineModel = meta.dataset._model; + var x, y; + + var options = me._resolveDataElementOptions(point, index); + + x = xScale.getPixelForValue(typeof value === 'object' ? value : NaN, index, datasetIndex); + y = reset ? yScale.getBasePixel() : me.calculatePointY(value, index, datasetIndex); + + // Utility + point._xScale = xScale; + point._yScale = yScale; + point._options = options; + point._datasetIndex = datasetIndex; + point._index = index; + + // Desired view properties + point._model = { + x: x, + y: y, + skip: custom.skip || isNaN(x) || isNaN(y), + // Appearance + radius: options.radius, + pointStyle: options.pointStyle, + rotation: options.rotation, + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + tension: valueOrDefault$6(custom.tension, lineModel ? lineModel.tension : 0), + steppedLine: lineModel ? lineModel.steppedLine : false, + // Tooltip + hitRadius: options.hitRadius + }; + }, + + /** + * @private + */ + _resolveDatasetElementOptions: function(element) { + var me = this; + var config = me._config; + var custom = element.custom || {}; + var options = me.chart.options; + var lineOptions = options.elements.line; + var values = core_datasetController.prototype._resolveDatasetElementOptions.apply(me, arguments); + + // The default behavior of lines is to break at null values, according + // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 + // This option gives lines the ability to span gaps + values.spanGaps = valueOrDefault$6(config.spanGaps, options.spanGaps); + values.tension = valueOrDefault$6(config.lineTension, lineOptions.tension); + values.steppedLine = resolve$2([custom.steppedLine, config.steppedLine, lineOptions.stepped]); + values.clip = toClip(valueOrDefault$6(config.clip, defaultClip(me._xScale, me._yScale, values.borderWidth))); + + return values; + }, + + calculatePointY: function(value, index, datasetIndex) { + var me = this; + var chart = me.chart; + var yScale = me._yScale; + var sumPos = 0; + var sumNeg = 0; + var i, ds, dsMeta, stackedRightValue, rightValue, metasets, ilen; + + if (yScale.options.stacked) { + rightValue = +yScale.getRightValue(value); + metasets = chart._getSortedVisibleDatasetMetas(); + ilen = metasets.length; + + for (i = 0; i < ilen; ++i) { + dsMeta = metasets[i]; + if (dsMeta.index === datasetIndex) { + break; + } + + ds = chart.data.datasets[dsMeta.index]; + if (dsMeta.type === 'line' && dsMeta.yAxisID === yScale.id) { + stackedRightValue = +yScale.getRightValue(ds.data[index]); + if (stackedRightValue < 0) { + sumNeg += stackedRightValue || 0; + } else { + sumPos += stackedRightValue || 0; + } + } + } + + if (rightValue < 0) { + return yScale.getPixelForValue(sumNeg + rightValue); + } + return yScale.getPixelForValue(sumPos + rightValue); + } + return yScale.getPixelForValue(value); + }, + + updateBezierControlPoints: function() { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var lineModel = meta.dataset._model; + var area = chart.chartArea; + var points = meta.data || []; + var i, ilen, model, controlPoints; + + // Only consider points that are drawn in case the spanGaps option is used + if (lineModel.spanGaps) { + points = points.filter(function(pt) { + return !pt._model.skip; + }); + } + + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + if (lineModel.cubicInterpolationMode === 'monotone') { + helpers$1.splineCurveMonotone(points); + } else { + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + controlPoints = helpers$1.splineCurve( + helpers$1.previousItem(points, i)._model, + model, + helpers$1.nextItem(points, i)._model, + lineModel.tension + ); + model.controlPointPreviousX = controlPoints.previous.x; + model.controlPointPreviousY = controlPoints.previous.y; + model.controlPointNextX = controlPoints.next.x; + model.controlPointNextY = controlPoints.next.y; + } + } + + if (chart.options.elements.line.capBezierPoints) { + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + if (isPointInArea(model, area)) { + if (i > 0 && isPointInArea(points[i - 1]._model, area)) { + model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right); + model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom); + } + if (i < points.length - 1 && isPointInArea(points[i + 1]._model, area)) { + model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right); + model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom); + } + } + } + } + }, + + draw: function() { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var points = meta.data || []; + var area = chart.chartArea; + var canvas = chart.canvas; + var i = 0; + var ilen = points.length; + var clip; + + if (me._showLine) { + clip = meta.dataset._model.clip; + + helpers$1.canvas.clipArea(chart.ctx, { + left: clip.left === false ? 0 : area.left - clip.left, + right: clip.right === false ? canvas.width : area.right + clip.right, + top: clip.top === false ? 0 : area.top - clip.top, + bottom: clip.bottom === false ? canvas.height : area.bottom + clip.bottom + }); + + meta.dataset.draw(); + + helpers$1.canvas.unclipArea(chart.ctx); + } + + // Draw the points + for (; i < ilen; ++i) { + points[i].draw(area); + } + }, + + /** + * @protected + */ + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + var getHoverColor = helpers$1.getHoverColor; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = valueOrDefault$6(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$6(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$6(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault$6(options.hoverRadius, options.radius); + }, + }); + + var resolve$3 = helpers$1.options.resolve; + + core_defaults._set('polarArea', { + scale: { + type: 'radialLinear', + angleLines: { + display: false + }, + gridLines: { + circular: true + }, + pointLabels: { + display: false + }, + ticks: { + beginAtZero: true + } + }, + + // Boolean - Whether to animate the rotation of the chart + animation: { + animateRotate: true, + animateScale: true + }, + + startAngle: -0.5 * Math.PI, + legendCallback: function(chart) { + var list = document.createElement('ul'); + var data = chart.data; + var datasets = data.datasets; + var labels = data.labels; + var i, ilen, listItem, listItemSpan; + + list.setAttribute('class', chart.id + '-legend'); + if (datasets.length) { + for (i = 0, ilen = datasets[0].data.length; i < ilen; ++i) { + listItem = list.appendChild(document.createElement('li')); + listItemSpan = listItem.appendChild(document.createElement('span')); + listItemSpan.style.backgroundColor = datasets[0].backgroundColor[i]; + if (labels[i]) { + listItem.appendChild(document.createTextNode(labels[i])); + } + } + } + + return list.outerHTML; + }, + legend: { + labels: { + generateLabels: function(chart) { + var data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map(function(label, i) { + var meta = chart.getDatasetMeta(0); + var style = meta.controller.getStyle(i); + + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + lineWidth: style.borderWidth, + hidden: isNaN(data.datasets[0].data[i]) || meta.data[i].hidden, + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick: function(e, legendItem) { + var index = legendItem.index; + var chart = this.chart; + var i, ilen, meta; + + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + meta.data[index].hidden = !meta.data[index].hidden; + } + + chart.update(); + } + }, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title: function() { + return ''; + }, + label: function(item, data) { + return data.labels[item.index] + ': ' + item.yLabel; + } + } + } + }); + + var controller_polarArea = core_datasetController.extend({ + + dataElementType: elements.Arc, + + linkScales: helpers$1.noop, + + /** + * @private + */ + _dataElementOptions: [ + 'backgroundColor', + 'borderColor', + 'borderWidth', + 'borderAlign', + 'hoverBackgroundColor', + 'hoverBorderColor', + 'hoverBorderWidth', + ], + + /** + * @private + */ + _getIndexScaleId: function() { + return this.chart.scale.id; + }, + + /** + * @private + */ + _getValueScaleId: function() { + return this.chart.scale.id; + }, + + update: function(reset) { + var me = this; + var dataset = me.getDataset(); + var meta = me.getMeta(); + var start = me.chart.options.startAngle || 0; + var starts = me._starts = []; + var angles = me._angles = []; + var arcs = meta.data; + var i, ilen, angle; + + me._updateRadius(); + + meta.count = me.countVisibleElements(); + + for (i = 0, ilen = dataset.data.length; i < ilen; i++) { + starts[i] = start; + angle = me._computeAngle(i); + angles[i] = angle; + start += angle; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + arcs[i]._options = me._resolveDataElementOptions(arcs[i], i); + me.updateElement(arcs[i], i, reset); + } + }, + + /** + * @private + */ + _updateRadius: function() { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var opts = chart.options; + var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + + chart.outerRadius = Math.max(minSize / 2, 0); + chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + + me.outerRadius = chart.outerRadius - (chart.radiusLength * me.index); + me.innerRadius = me.outerRadius - chart.radiusLength; + }, + + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var opts = chart.options; + var animationOpts = opts.animation; + var scale = chart.scale; + var labels = chart.data.labels; + + var centerX = scale.xCenter; + var centerY = scale.yCenter; + + // var negHalfPI = -0.5 * Math.PI; + var datasetStartAngle = opts.startAngle; + var distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + var startAngle = me._starts[index]; + var endAngle = startAngle + (arc.hidden ? 0 : me._angles[index]); + + var resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + var options = arc._options || {}; + + helpers$1.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + _scale: scale, + + // Desired view properties + _model: { + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + borderAlign: options.borderAlign, + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius: reset ? resetRadius : distance, + startAngle: reset && animationOpts.animateRotate ? datasetStartAngle : startAngle, + endAngle: reset && animationOpts.animateRotate ? datasetStartAngle : endAngle, + label: helpers$1.valueAtIndexOrDefault(labels, index, labels[index]) + } + }); + + arc.pivot(); + }, + + countVisibleElements: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var count = 0; + + helpers$1.each(meta.data, function(element, index) { + if (!isNaN(dataset.data[index]) && !element.hidden) { + count++; + } + }); + + return count; + }, + + /** + * @protected + */ + setHoverStyle: function(arc) { + var model = arc._model; + var options = arc._options; + var getHoverColor = helpers$1.getHoverColor; + var valueOrDefault = helpers$1.valueOrDefault; + + arc.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + }; + + model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); + }, + + /** + * @private + */ + _computeAngle: function(index) { + var me = this; + var count = this.getMeta().count; + var dataset = me.getDataset(); + var meta = me.getMeta(); + + if (isNaN(dataset.data[index]) || meta.data[index].hidden) { + return 0; + } + + // Scriptable options + var context = { + chart: me.chart, + dataIndex: index, + dataset: dataset, + datasetIndex: me.index + }; + + return resolve$3([ + me.chart.options.elements.arc.angle, + (2 * Math.PI) / count + ], context, index); + } + }); + + core_defaults._set('pie', helpers$1.clone(core_defaults.doughnut)); + core_defaults._set('pie', { + cutoutPercentage: 0 + }); + + // Pie charts are Doughnut chart with different defaults + var controller_pie = controller_doughnut; + + var valueOrDefault$7 = helpers$1.valueOrDefault; + + core_defaults._set('radar', { + spanGaps: false, + scale: { + type: 'radialLinear' + }, + elements: { + line: { + fill: 'start', + tension: 0 // no bezier in radar + } + } + }); + + var controller_radar = core_datasetController.extend({ + datasetElementType: elements.Line, + + dataElementType: elements.Point, + + linkScales: helpers$1.noop, + + /** + * @private + */ + _datasetElementOptions: [ + 'backgroundColor', + 'borderWidth', + 'borderColor', + 'borderCapStyle', + 'borderDash', + 'borderDashOffset', + 'borderJoinStyle', + 'fill' + ], + + /** + * @private + */ + _dataElementOptions: { + backgroundColor: 'pointBackgroundColor', + borderColor: 'pointBorderColor', + borderWidth: 'pointBorderWidth', + hitRadius: 'pointHitRadius', + hoverBackgroundColor: 'pointHoverBackgroundColor', + hoverBorderColor: 'pointHoverBorderColor', + hoverBorderWidth: 'pointHoverBorderWidth', + hoverRadius: 'pointHoverRadius', + pointStyle: 'pointStyle', + radius: 'pointRadius', + rotation: 'pointRotation' + }, + + /** + * @private + */ + _getIndexScaleId: function() { + return this.chart.scale.id; + }, + + /** + * @private + */ + _getValueScaleId: function() { + return this.chart.scale.id; + }, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var scale = me.chart.scale; + var config = me._config; + var i, ilen; + + // Compatibility: If the properties are defined with only the old name, use those values + if (config.tension !== undefined && config.lineTension === undefined) { + config.lineTension = config.tension; + } + + // Utility + line._scale = scale; + line._datasetIndex = me.index; + // Data + line._children = points; + line._loop = true; + // Model + line._model = me._resolveDatasetElementOptions(line); + + line.pivot(); + + // Update Points + for (i = 0, ilen = points.length; i < ilen; ++i) { + me.updateElement(points[i], i, reset); + } + + // Update bezier control points + me.updateBezierControlPoints(); + + // Now pivot the point for animation + for (i = 0, ilen = points.length; i < ilen; ++i) { + points[i].pivot(); + } + }, + + updateElement: function(point, index, reset) { + var me = this; + var custom = point.custom || {}; + var dataset = me.getDataset(); + var scale = me.chart.scale; + var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); + var options = me._resolveDataElementOptions(point, index); + var lineModel = me.getMeta().dataset._model; + var x = reset ? scale.xCenter : pointPosition.x; + var y = reset ? scale.yCenter : pointPosition.y; + + // Utility + point._scale = scale; + point._options = options; + point._datasetIndex = me.index; + point._index = index; + + // Desired view properties + point._model = { + x: x, // value not used in dataset scale, but we want a consistent API between scales + y: y, + skip: custom.skip || isNaN(x) || isNaN(y), + // Appearance + radius: options.radius, + pointStyle: options.pointStyle, + rotation: options.rotation, + backgroundColor: options.backgroundColor, + borderColor: options.borderColor, + borderWidth: options.borderWidth, + tension: valueOrDefault$7(custom.tension, lineModel ? lineModel.tension : 0), + + // Tooltip + hitRadius: options.hitRadius + }; + }, + + /** + * @private + */ + _resolveDatasetElementOptions: function() { + var me = this; + var config = me._config; + var options = me.chart.options; + var values = core_datasetController.prototype._resolveDatasetElementOptions.apply(me, arguments); + + values.spanGaps = valueOrDefault$7(config.spanGaps, options.spanGaps); + values.tension = valueOrDefault$7(config.lineTension, options.elements.line.tension); + + return values; + }, + + updateBezierControlPoints: function() { + var me = this; + var meta = me.getMeta(); + var area = me.chart.chartArea; + var points = meta.data || []; + var i, ilen, model, controlPoints; + + // Only consider points that are drawn in case the spanGaps option is used + if (meta.dataset._model.spanGaps) { + points = points.filter(function(pt) { + return !pt._model.skip; + }); + } + + function capControlPoint(pt, min, max) { + return Math.max(Math.min(pt, max), min); + } + + for (i = 0, ilen = points.length; i < ilen; ++i) { + model = points[i]._model; + controlPoints = helpers$1.splineCurve( + helpers$1.previousItem(points, i, true)._model, + model, + helpers$1.nextItem(points, i, true)._model, + model.tension + ); + + // Prevent the bezier going outside of the bounds of the graph + model.controlPointPreviousX = capControlPoint(controlPoints.previous.x, area.left, area.right); + model.controlPointPreviousY = capControlPoint(controlPoints.previous.y, area.top, area.bottom); + model.controlPointNextX = capControlPoint(controlPoints.next.x, area.left, area.right); + model.controlPointNextY = capControlPoint(controlPoints.next.y, area.top, area.bottom); + } + }, + + setHoverStyle: function(point) { + var model = point._model; + var options = point._options; + var getHoverColor = helpers$1.getHoverColor; + + point.$previousStyle = { + backgroundColor: model.backgroundColor, + borderColor: model.borderColor, + borderWidth: model.borderWidth, + radius: model.radius + }; + + model.backgroundColor = valueOrDefault$7(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); + model.borderColor = valueOrDefault$7(options.hoverBorderColor, getHoverColor(options.borderColor)); + model.borderWidth = valueOrDefault$7(options.hoverBorderWidth, options.borderWidth); + model.radius = valueOrDefault$7(options.hoverRadius, options.radius); + } + }); + + core_defaults._set('scatter', { + hover: { + mode: 'single' + }, + + scales: { + xAxes: [{ + id: 'x-axis-1', // need an ID so datasets can reference the scale + type: 'linear', // scatter should not use a category axis + position: 'bottom' + }], + yAxes: [{ + id: 'y-axis-1', + type: 'linear', + position: 'left' + }] + }, + + tooltips: { + callbacks: { + title: function() { + return ''; // doesn't make sense for scatter since data are formatted as a point + }, + label: function(item) { + return '(' + item.xLabel + ', ' + item.yLabel + ')'; + } + } + } + }); + + core_defaults._set('global', { + datasets: { + scatter: { + showLine: false + } + } + }); + + // Scatter charts use line controllers + var controller_scatter = controller_line; + + // NOTE export a map in which the key represents the controller type, not + // the class, and so must be CamelCase in order to be correctly retrieved + // by the controller in core.controller.js (`controllers[meta.type]`). + + var controllers = { + bar: controller_bar, + bubble: controller_bubble, + doughnut: controller_doughnut, + horizontalBar: controller_horizontalBar, + line: controller_line, + polarArea: controller_polarArea, + pie: controller_pie, + radar: controller_radar, + scatter: controller_scatter + }; + + /** + * Helper function to get relative position for an event + * @param {Event|IEvent} event - The event to get the position for + * @param {Chart} chart - The chart + * @returns {object} the event position + */ + function getRelativePosition(e, chart) { + if (e.native) { + return { + x: e.x, + y: e.y + }; + } + + return helpers$1.getRelativePosition(e, chart); + } + + /** + * Helper function to traverse all of the visible elements in the chart + * @param {Chart} chart - the chart + * @param {function} handler - the callback to execute for each visible item + */ + function parseVisibleItems(chart, handler) { + var metasets = chart._getSortedVisibleDatasetMetas(); + var metadata, i, j, ilen, jlen, element; + + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + metadata = metasets[i].data; + for (j = 0, jlen = metadata.length; j < jlen; ++j) { + element = metadata[j]; + if (!element._view.skip) { + handler(element); + } + } + } + } + + /** + * Helper function to get the items that intersect the event position + * @param {ChartElement[]} items - elements to filter + * @param {object} position - the point to be nearest to + * @return {ChartElement[]} the nearest items + */ + function getIntersectItems(chart, position) { + var elements = []; + + parseVisibleItems(chart, function(element) { + if (element.inRange(position.x, position.y)) { + elements.push(element); + } + }); + + return elements; + } + + /** + * Helper function to get the items nearest to the event position considering all visible items in teh chart + * @param {Chart} chart - the chart to look at elements from + * @param {object} position - the point to be nearest to + * @param {boolean} intersect - if true, only consider items that intersect the position + * @param {function} distanceMetric - function to provide the distance between points + * @return {ChartElement[]} the nearest items + */ + function getNearestItems(chart, position, intersect, distanceMetric) { + var minDistance = Number.POSITIVE_INFINITY; + var nearestItems = []; + + parseVisibleItems(chart, function(element) { + if (intersect && !element.inRange(position.x, position.y)) { + return; + } + + var center = element.getCenterPoint(); + var distance = distanceMetric(position, center); + if (distance < minDistance) { + nearestItems = [element]; + minDistance = distance; + } else if (distance === minDistance) { + // Can have multiple items at the same distance in which case we sort by size + nearestItems.push(element); + } + }); + + return nearestItems; + } + + /** + * Get a distance metric function for two points based on the + * axis mode setting + * @param {string} axis - the axis mode. x|y|xy + */ + function getDistanceMetricForAxis(axis) { + var useX = axis.indexOf('x') !== -1; + var useY = axis.indexOf('y') !== -1; + + return function(pt1, pt2) { + var deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0; + var deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0; + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + }; + } + + function indexMode(chart, e, options) { + var position = getRelativePosition(e, chart); + // Default axis for index mode is 'x' to match old behaviour + options.axis = options.axis || 'x'; + var distanceMetric = getDistanceMetricForAxis(options.axis); + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric); + var elements = []; + + if (!items.length) { + return []; + } + + chart._getSortedVisibleDatasetMetas().forEach(function(meta) { + var element = meta.data[items[0]._index]; + + // don't count items that are skipped (null data) + if (element && !element._view.skip) { + elements.push(element); + } + }); + + return elements; + } + + /** + * @interface IInteractionOptions + */ + /** + * If true, only consider items that intersect the point + * @name IInterfaceOptions#boolean + * @type Boolean + */ + + /** + * Contains interaction related functions + * @namespace Chart.Interaction + */ + var core_interaction = { + // Helper function for different modes + modes: { + single: function(chart, e) { + var position = getRelativePosition(e, chart); + var elements = []; + + parseVisibleItems(chart, function(element) { + if (element.inRange(position.x, position.y)) { + elements.push(element); + return elements; + } + }); + + return elements.slice(0, 1); + }, + + /** + * @function Chart.Interaction.modes.label + * @deprecated since version 2.4.0 + * @todo remove at version 3 + * @private + */ + label: indexMode, + + /** + * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item + * @function Chart.Interaction.modes.index + * @since v2.4.0 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use during interaction + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + index: indexMode, + + /** + * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect is false, we find the nearest item and return the items in that dataset + * @function Chart.Interaction.modes.dataset + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use during interaction + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + dataset: function(chart, e, options) { + var position = getRelativePosition(e, chart); + options.axis = options.axis || 'xy'; + var distanceMetric = getDistanceMetricForAxis(options.axis); + var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false, distanceMetric); + + if (items.length > 0) { + items = chart.getDatasetMeta(items[0]._datasetIndex).data; + } + + return items; + }, + + /** + * @function Chart.Interaction.modes.x-axis + * @deprecated since version 2.4.0. Use index mode and intersect == true + * @todo remove at version 3 + * @private + */ + 'x-axis': function(chart, e) { + return indexMode(chart, e, {intersect: false}); + }, + + /** + * Point mode returns all elements that hit test based on the event position + * of the event + * @function Chart.Interaction.modes.intersect + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + point: function(chart, e) { + var position = getRelativePosition(e, chart); + return getIntersectItems(chart, position); + }, + + /** + * nearest mode returns the element closest to the point + * @function Chart.Interaction.modes.intersect + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + nearest: function(chart, e, options) { + var position = getRelativePosition(e, chart); + options.axis = options.axis || 'xy'; + var distanceMetric = getDistanceMetricForAxis(options.axis); + return getNearestItems(chart, position, options.intersect, distanceMetric); + }, + + /** + * x mode returns the elements that hit-test at the current x coordinate + * @function Chart.Interaction.modes.x + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + x: function(chart, e, options) { + var position = getRelativePosition(e, chart); + var items = []; + var intersectsItem = false; + + parseVisibleItems(chart, function(element) { + if (element.inXRange(position.x)) { + items.push(element); + } + + if (element.inRange(position.x, position.y)) { + intersectsItem = true; + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (options.intersect && !intersectsItem) { + items = []; + } + return items; + }, + + /** + * y mode returns the elements that hit-test at the current y coordinate + * @function Chart.Interaction.modes.y + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {IInteractionOptions} options - options to use + * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned + */ + y: function(chart, e, options) { + var position = getRelativePosition(e, chart); + var items = []; + var intersectsItem = false; + + parseVisibleItems(chart, function(element) { + if (element.inYRange(position.y)) { + items.push(element); + } + + if (element.inRange(position.x, position.y)) { + intersectsItem = true; + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (options.intersect && !intersectsItem) { + items = []; + } + return items; + } + } + }; + + var extend = helpers$1.extend; + + function filterByPosition(array, position) { + return helpers$1.where(array, function(v) { + return v.pos === position; + }); + } + + function sortByWeight(array, reverse) { + return array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0.index - v1.index : + v0.weight - v1.weight; + }); + } + + function wrapBoxes(boxes) { + var layoutBoxes = []; + var i, ilen, box; + + for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { + box = boxes[i]; + layoutBoxes.push({ + index: i, + box: box, + pos: box.position, + horizontal: box.isHorizontal(), + weight: box.weight + }); + } + return layoutBoxes; + } + + function setLayoutDims(layouts, params) { + var i, ilen, layout; + for (i = 0, ilen = layouts.length; i < ilen; ++i) { + layout = layouts[i]; + // store width used instead of chartArea.w in fitBoxes + layout.width = layout.horizontal + ? layout.box.fullWidth && params.availableWidth + : params.vBoxMaxWidth; + // store height used instead of chartArea.h in fitBoxes + layout.height = layout.horizontal && params.hBoxMaxHeight; + } + } + + function buildLayoutBoxes(boxes) { + var layoutBoxes = wrapBoxes(boxes); + var left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); + var right = sortByWeight(filterByPosition(layoutBoxes, 'right')); + var top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); + var bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + + return { + leftAndTop: left.concat(top), + rightAndBottom: right.concat(bottom), + chartArea: filterByPosition(layoutBoxes, 'chartArea'), + vertical: left.concat(right), + horizontal: top.concat(bottom) + }; + } + + function getCombinedMax(maxPadding, chartArea, a, b) { + return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]); + } + + function updateDims(chartArea, params, layout) { + var box = layout.box; + var maxPadding = chartArea.maxPadding; + var newWidth, newHeight; + + if (layout.size) { + // this layout was already counted for, lets first reduce old size + chartArea[layout.pos] -= layout.size; + } + layout.size = layout.horizontal ? box.height : box.width; + chartArea[layout.pos] += layout.size; + + if (box.getPadding) { + var boxPadding = box.getPadding(); + maxPadding.top = Math.max(maxPadding.top, boxPadding.top); + maxPadding.left = Math.max(maxPadding.left, boxPadding.left); + maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom); + maxPadding.right = Math.max(maxPadding.right, boxPadding.right); + } + + newWidth = params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right'); + newHeight = params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom'); + + if (newWidth !== chartArea.w || newHeight !== chartArea.h) { + chartArea.w = newWidth; + chartArea.h = newHeight; + + // return true if chart area changed in layout's direction + var sizes = layout.horizontal ? [newWidth, chartArea.w] : [newHeight, chartArea.h]; + return sizes[0] !== sizes[1] && (!isNaN(sizes[0]) || !isNaN(sizes[1])); + } + } + + function handleMaxPadding(chartArea) { + var maxPadding = chartArea.maxPadding; + + function updatePos(pos) { + var change = Math.max(maxPadding[pos] - chartArea[pos], 0); + chartArea[pos] += change; + return change; + } + chartArea.y += updatePos('top'); + chartArea.x += updatePos('left'); + updatePos('right'); + updatePos('bottom'); + } + + function getMargins(horizontal, chartArea) { + var maxPadding = chartArea.maxPadding; + + function marginForPositions(positions) { + var margin = {left: 0, top: 0, right: 0, bottom: 0}; + positions.forEach(function(pos) { + margin[pos] = Math.max(chartArea[pos], maxPadding[pos]); + }); + return margin; + } + + return horizontal + ? marginForPositions(['left', 'right']) + : marginForPositions(['top', 'bottom']); + } + + function fitBoxes(boxes, chartArea, params) { + var refitBoxes = []; + var i, ilen, layout, box, refit, changed; + + for (i = 0, ilen = boxes.length; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + + box.update( + layout.width || chartArea.w, + layout.height || chartArea.h, + getMargins(layout.horizontal, chartArea) + ); + if (updateDims(chartArea, params, layout)) { + changed = true; + if (refitBoxes.length) { + // Dimensions changed and there were non full width boxes before this + // -> we have to refit those + refit = true; + } + } + if (!box.fullWidth) { // fullWidth boxes don't need to be re-fitted in any case + refitBoxes.push(layout); + } + } + + return refit ? fitBoxes(refitBoxes, chartArea, params) || changed : changed; + } + + function placeBoxes(boxes, chartArea, params) { + var userPadding = params.padding; + var x = chartArea.x; + var y = chartArea.y; + var i, ilen, layout, box; + + for (i = 0, ilen = boxes.length; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + if (layout.horizontal) { + box.left = box.fullWidth ? userPadding.left : chartArea.left; + box.right = box.fullWidth ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w; + box.top = y; + box.bottom = y + box.height; + box.width = box.right - box.left; + y = box.bottom; + } else { + box.left = x; + box.right = x + box.width; + box.top = chartArea.top; + box.bottom = chartArea.top + chartArea.h; + box.height = box.bottom - box.top; + x = box.right; + } + } + + chartArea.x = x; + chartArea.y = y; + } + + core_defaults._set('global', { + layout: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + } + }); + + /** + * @interface ILayoutItem + * @prop {string} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {function} update - Takes two parameters: width and height. Returns size of item + * @prop {function} getPadding - Returns an object with padding on the edges + * @prop {number} width - Width of item. Must be valid after update() + * @prop {number} height - Height of item. Must be valid after update() + * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + + // The layout service is very self explanatory. It's responsible for the layout within a chart. + // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need + // It is this service's responsibility of carrying out that layout. + var core_layouts = { + defaults: {}, + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} item - the item to add to be layed out + */ + addBox: function(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullWidth = item.fullWidth || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + item._layers = item._layers || function() { + return [{ + z: 0, + draw: function() { + item.draw.apply(item, arguments); + } + }]; + }; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {ILayoutItem} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { + var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {ILayoutItem} item - the item to configure with the given options + * @param {object} options - the new item options. + */ + configure: function(chart, item, options) { + var props = ['fullWidth', 'position', 'weight']; + var ilen = props.length; + var i = 0; + var prop; + + for (; i < ilen; ++i) { + prop = props[i]; + if (options.hasOwnProperty(prop)) { + item[prop] = options[prop]; + } + } + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {number} width - the width to fit into + * @param {number} height - the height to fit into + */ + update: function(chart, width, height) { + if (!chart) { + return; + } + + var layoutOptions = chart.options.layout || {}; + var padding = helpers$1.options.toPadding(layoutOptions.padding); + + var availableWidth = width - padding.width; + var availableHeight = height - padding.height; + var boxes = buildLayoutBoxes(chart.boxes); + var verticalBoxes = boxes.vertical; + var horizontalBoxes = boxes.horizontal; + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + + var params = Object.freeze({ + outerWidth: width, + outerHeight: height, + padding: padding, + availableWidth: availableWidth, + vBoxMaxWidth: availableWidth / 2 / verticalBoxes.length, + hBoxMaxHeight: availableHeight / 2 + }); + var chartArea = extend({ + maxPadding: extend({}, padding), + w: availableWidth, + h: availableHeight, + x: padding.left, + y: padding.top + }, padding); + + setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); + + // First fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params); + + // Then fit horizontal boxes + if (fitBoxes(horizontalBoxes, chartArea, params)) { + // if the area changed, re-fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params); + } + + handleMaxPadding(chartArea); + + // Finally place the boxes to correct coordinates + placeBoxes(boxes.leftAndTop, chartArea, params); + + // Move to opposite side of chart + chartArea.x += chartArea.w; + chartArea.y += chartArea.h; + + placeBoxes(boxes.rightAndBottom, chartArea, params); + + chart.chartArea = { + left: chartArea.left, + top: chartArea.top, + right: chartArea.left + chartArea.w, + bottom: chartArea.top + chartArea.h + }; + + // Finally update boxes in chartArea (radial scale for example) + helpers$1.each(boxes.chartArea, function(layout) { + var box = layout.box; + extend(box, chart.chartArea); + box.update(chartArea.w, chartArea.h); + }); + } + }; + + /** + * Platform fallback implementation (minimal). + * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 + */ + + var platform_basic = { + acquireContext: function(item) { + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + return item && item.getContext('2d') || null; + } + }; + + var platform_dom = "/*\r\n * DOM element rendering detection\r\n * https://davidwalsh.name/detect-node-insertion\r\n */\r\n@keyframes chartjs-render-animation {\r\n\tfrom { opacity: 0.99; }\r\n\tto { opacity: 1; }\r\n}\r\n\r\n.chartjs-render-monitor {\r\n\tanimation: chartjs-render-animation 0.001s;\r\n}\r\n\r\n/*\r\n * DOM element resizing detection\r\n * https://github.com/marcj/css-element-queries\r\n */\r\n.chartjs-size-monitor,\r\n.chartjs-size-monitor-expand,\r\n.chartjs-size-monitor-shrink {\r\n\tposition: absolute;\r\n\tdirection: ltr;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tbottom: 0;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\tvisibility: hidden;\r\n\tz-index: -1;\r\n}\r\n\r\n.chartjs-size-monitor-expand > div {\r\n\tposition: absolute;\r\n\twidth: 1000000px;\r\n\theight: 1000000px;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n\r\n.chartjs-size-monitor-shrink > div {\r\n\tposition: absolute;\r\n\twidth: 200%;\r\n\theight: 200%;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n"; + + var platform_dom$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + 'default': platform_dom + }); + + var stylesheet = getCjsExportFromNamespace(platform_dom$1); + + var EXPANDO_KEY = '$chartjs'; + var CSS_PREFIX = 'chartjs-'; + var CSS_SIZE_MONITOR = CSS_PREFIX + 'size-monitor'; + var CSS_RENDER_MONITOR = CSS_PREFIX + 'render-monitor'; + var CSS_RENDER_ANIMATION = CSS_PREFIX + 'render-animation'; + var ANIMATION_START_EVENTS = ['animationstart', 'webkitAnimationStart']; + + /** + * DOM event types -> Chart.js event types. + * Note: only events with different types are mapped. + * @see https://developer.mozilla.org/en-US/docs/Web/Events + */ + var EVENT_TYPES = { + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' + }; + + /** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {number} Size in pixels or undefined if unknown. + */ + function readUsedSize(element, property) { + var value = helpers$1.getStyle(element, property); + var matches = value && value.match(/^(\d+)(\.\d+)?px$/); + return matches ? Number(matches[1]) : undefined; + } + + /** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + */ + function initCanvas(canvas, config) { + var style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas[EXPANDO_KEY] = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; + } + + /** + * Detects support for options object argument in addEventListener. + * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support + * @private + */ + var supportsEventListenerOptions = (function() { + var supports = false; + try { + var options = Object.defineProperty({}, 'passive', { + // eslint-disable-next-line getter-return + get: function() { + supports = true; + } + }); + window.addEventListener('e', null, options); + } catch (e) { + // continue regardless of error + } + return supports; + }()); + + // Default passive to true as expected by Chrome for 'touchstart' and 'touchend' events. + // https://github.com/chartjs/Chart.js/issues/4287 + var eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false; + + function addListener(node, type, listener) { + node.addEventListener(type, listener, eventListenerOptions); + } + + function removeListener(node, type, listener) { + node.removeEventListener(type, listener, eventListenerOptions); + } + + function createEvent(type, chart, x, y, nativeEvent) { + return { + type: type, + chart: chart, + native: nativeEvent || null, + x: x !== undefined ? x : null, + y: y !== undefined ? y : null, + }; + } + + function fromNativeEvent(event, chart) { + var type = EVENT_TYPES[event.type] || event.type; + var pos = helpers$1.getRelativePosition(event, chart); + return createEvent(type, chart, pos.x, pos.y, event); + } + + function throttled(fn, thisArg) { + var ticking = false; + var args = []; + + return function() { + args = Array.prototype.slice.call(arguments); + thisArg = thisArg || this; + + if (!ticking) { + ticking = true; + helpers$1.requestAnimFrame.call(window, function() { + ticking = false; + fn.apply(thisArg, args); + }); + } + }; + } + + function createDiv(cls) { + var el = document.createElement('div'); + el.className = cls || ''; + return el; + } + + // Implementation based on https://github.com/marcj/css-element-queries + function createResizer(handler) { + var maxSize = 1000000; + + // NOTE(SB) Don't use innerHTML because it could be considered unsafe. + // https://github.com/chartjs/Chart.js/issues/5902 + var resizer = createDiv(CSS_SIZE_MONITOR); + var expand = createDiv(CSS_SIZE_MONITOR + '-expand'); + var shrink = createDiv(CSS_SIZE_MONITOR + '-shrink'); + + expand.appendChild(createDiv()); + shrink.appendChild(createDiv()); + + resizer.appendChild(expand); + resizer.appendChild(shrink); + resizer._reset = function() { + expand.scrollLeft = maxSize; + expand.scrollTop = maxSize; + shrink.scrollLeft = maxSize; + shrink.scrollTop = maxSize; + }; + + var onScroll = function() { + resizer._reset(); + handler(); + }; + + addListener(expand, 'scroll', onScroll.bind(expand, 'expand')); + addListener(shrink, 'scroll', onScroll.bind(shrink, 'shrink')); + + return resizer; + } + + // https://davidwalsh.name/detect-node-insertion + function watchForRender(node, handler) { + var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {}); + var proxy = expando.renderProxy = function(e) { + if (e.animationName === CSS_RENDER_ANIMATION) { + handler(); + } + }; + + helpers$1.each(ANIMATION_START_EVENTS, function(type) { + addListener(node, type, proxy); + }); + + // #4737: Chrome might skip the CSS animation when the CSS_RENDER_MONITOR class + // is removed then added back immediately (same animation frame?). Accessing the + // `offsetParent` property will force a reflow and re-evaluate the CSS animation. + // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#box-metrics + // https://github.com/chartjs/Chart.js/issues/4737 + expando.reflow = !!node.offsetParent; + + node.classList.add(CSS_RENDER_MONITOR); + } + + function unwatchForRender(node) { + var expando = node[EXPANDO_KEY] || {}; + var proxy = expando.renderProxy; + + if (proxy) { + helpers$1.each(ANIMATION_START_EVENTS, function(type) { + removeListener(node, type, proxy); + }); + + delete expando.renderProxy; + } + + node.classList.remove(CSS_RENDER_MONITOR); + } + + function addResizeListener(node, listener, chart) { + var expando = node[EXPANDO_KEY] || (node[EXPANDO_KEY] = {}); + + // Let's keep track of this added resizer and thus avoid DOM query when removing it. + var resizer = expando.resizer = createResizer(throttled(function() { + if (expando.resizer) { + var container = chart.options.maintainAspectRatio && node.parentNode; + var w = container ? container.clientWidth : 0; + listener(createEvent('resize', chart)); + if (container && container.clientWidth < w && chart.canvas) { + // If the container size shrank during chart resize, let's assume + // scrollbar appeared. So we resize again with the scrollbar visible - + // effectively making chart smaller and the scrollbar hidden again. + // Because we are inside `throttled`, and currently `ticking`, scroll + // events are ignored during this whole 2 resize process. + // If we assumed wrong and something else happened, we are resizing + // twice in a frame (potential performance issue) + listener(createEvent('resize', chart)); + } + } + })); + + // The resizer needs to be attached to the node parent, so we first need to be + // sure that `node` is attached to the DOM before injecting the resizer element. + watchForRender(node, function() { + if (expando.resizer) { + var container = node.parentNode; + if (container && container !== resizer.parentNode) { + container.insertBefore(resizer, container.firstChild); + } + + // The container size might have changed, let's reset the resizer state. + resizer._reset(); + } + }); + } + + function removeResizeListener(node) { + var expando = node[EXPANDO_KEY] || {}; + var resizer = expando.resizer; + + delete expando.resizer; + unwatchForRender(node); + + if (resizer && resizer.parentNode) { + resizer.parentNode.removeChild(resizer); + } + } + + /** + * Injects CSS styles inline if the styles are not already present. + * @param {HTMLDocument|ShadowRoot} rootNode - the node to contain the
                              $str_month $y
                              ' . mb_substr(DI::l10n()->getDay($dn[$a]), 0, 3, 'UTF-8') . '