Merge branch '2021.06-rc' of https://github.com/friendica/friendica into 2021.06-CHANGELOG

This commit is contained in:
Tobias Diekershoff 2021-06-02 19:58:28 +02:00
commit e4553ad156
113 changed files with 13160 additions and 12097 deletions

View file

@ -8,7 +8,7 @@ coverage:
round: down
range: "70...100"
status:
project: off
patch: off
project: false
patch: false
comment: off
comment: false

View file

@ -52,21 +52,81 @@ trigger:
- pull_request
steps:
- name: Restore cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
restore: true
cache_key: '{{ .Repo.Name }}_phpcs_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Install dependencies
image: composer
commands:
- export COMPOSER_HOME=.composer
- ./bin/composer.phar run cs:install
- name: Rebuild cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
rebuild: true
cache_key: '{{ .Repo.Name }}_phpcs_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Run coding standards check
image: friendicaci/php-cs
commands:
- export CHANGED_FILES="$(git diff --name-status ${DRONE_COMMIT_BEFORE}..${DRONE_COMMIT_AFTER} | grep ^A | cut -f2)"
- /check-php-cs.sh
volumes:
- name: cache
host:
path: /tmp/drone-cache
---
kind: pipeline
type: docker
name: php7.3-mariadb
steps:
- name: Restore cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
restore: true
cache_key: '{{ .Repo.Name }}_php73_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Composer install
image: friendicaci/php7.3:php7.3.28
commands:
- export COMPOSER_HOME=.composer
- ./bin/composer.phar validate
- ./bin/composer.phar install --prefer-dist
- name: Rebuild cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
rebuild: true
cache_key: '{{ .Repo.Name }}_php73_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Test Friendica
image: friendicaci/php7.3:php7.3.28
environment:
@ -79,8 +139,6 @@ steps:
MEMCACHED_HOST: "memcached"
MEMCACHE_HOST: "memcached"
commands:
- composer validate
- composer install --prefer-dist
- 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
@ -102,12 +160,47 @@ services:
- name: redis
image: redis
volumes:
- name: cache
host:
path: /tmp/drone-cache
---
kind: pipeline
type: docker
name: php7.4-mariadb
steps:
- name: Restore cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
restore: true
cache_key: '{{ .Repo.Name }}_php74_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Composer install
image: friendicaci/php7.4:php7.4.18
commands:
- export COMPOSER_HOME=.composer
- ./bin/composer.phar validate
- ./bin/composer.phar install --prefer-dist
- name: Rebuild cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
rebuild: true
cache_key: '{{ .Repo.Name }}_php74_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Test Friendica
image: friendicaci/php7.4:php7.4.18
environment:
@ -122,8 +215,6 @@ steps:
XDEBUG_MODE: "coverage"
commands:
- phpenmod xdebug
- composer validate
- composer install --prefer-dist
- 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
@ -155,12 +246,47 @@ services:
- name: redis
image: redis
volumes:
- name: cache
host:
path: /tmp/drone-cache
---
kind: pipeline
type: docker
name: php8.0-mariadb
steps:
- name: Restore cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
restore: true
cache_key: '{{ .Repo.Name }}_php80_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Composer install
image: friendicaci/php8.0:php8.0.5
commands:
- export COMPOSER_HOME=.composer
- ./bin/composer.phar validate
- ./bin/composer.phar install --prefer-dist
- name: Rebuild cache
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
rebuild: true
cache_key: '{{ .Repo.Name }}_php80_{{ arch }}_{{ os }}'
archive_format: "gzip"
mount:
- '.composer'
volumes:
- name: cache
path: /tmp/cache
- name: Test Friendica
image: friendicaci/php8.0:php8.0.5
environment:
@ -173,8 +299,6 @@ steps:
MEMCACHED_HOST: "memcached"
MEMCACHE_HOST: "memcached"
commands:
- composer validate
- composer install --prefer-dist
- 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
@ -196,3 +320,8 @@ services:
- name: redis
image: redis
volumes:
- name: cache
host:
path: /tmp/drone-cache

3
.gitignore vendored
View file

@ -16,7 +16,8 @@ robots.txt
/config/addon.ini.php
#ignore documentation, it should be newly built
/doc/html
/doc/api
/doc/cache
#ignore reports, should be generated with every build
report/

6
Vagrantfile vendored
View file

@ -34,10 +34,14 @@ Vagrant.configure(2) do |config|
#
# # Customize the amount of memory on the VM:
vb.memory = server_memory
unless Vagrant.has_plugin?("vagrant-vbguest")
raise 'vagrant-vbguest plugin is not installed! Install with "vagrant plugin install vagrant-vbguest"'
end
end
# Enable provisioning with a shell script.
config.vm.provision "shell", path: "./bin/dev/vagrant_provision.sh"
config.vm.provision "shell", path: "./bin/dev/vagrant_provision.sh", privileged: true
# run: "always"
# run: "once"
end

Binary file not shown.

View file

@ -1,13 +1,23 @@
#!/bin/bash
#Script to setup the vagrant instance for running friendica
# Script to setup the vagrant instance for running friendica
#
#DO NOT RUN on your physical machine as this won't be of any use
#and f.e. deletes your /var/www/ folder!
echo "Friendica configuration settings"
sudo apt-get update
# DO NOT RUN on your physical machine as this won't be of any use
# and f.e. deletes your /var/www/ folder!
#
# Run as root by vagrant
#
##
# Install virtualbox guest additions
sudo apt-get install virtualbox-guest-x11
ADMIN_NICK="admin"
ADMIN_PASSW="admin"
USER_NICK="user"
USER_PASSW="user"
##
echo "Friendica configuration settings"
apt-get update
#Selfsigned cert
echo ">>> Installing *.xip.io self-signed SSL"
@ -23,32 +33,32 @@ localityName=New Haven/
commonName=$DOMAIN/
subjectAltName=DNS:$EXTRADOMAIN
"
sudo mkdir -p "$SSL_DIR"
sudo openssl genrsa -out "$SSL_DIR/xip.io.key" 4096
sudo openssl req -new -subj "$(echo -n "$SUBJ" | tr "\n" "/")" -key "$SSL_DIR/xip.io.key" -out "$SSL_DIR/xip.io.csr" -passin pass:$PASSPHRASE
sudo openssl x509 -req -days 365 -in "$SSL_DIR/xip.io.csr" -signkey "$SSL_DIR/xip.io.key" -out "$SSL_DIR/xip.io.crt"
mkdir -p "$SSL_DIR"
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 apache2
echo ">>> Installing Apache2 webserver"
sudo apt-get install -y apache2
sudo a2enmod rewrite actions ssl
sudo cp /vagrant/bin/dev/vagrant_vhost.sh /usr/local/bin/vhost
sudo chmod guo+x /usr/local/bin/vhost
sudo vhost -s 192.168.22.10.xip.io -d /var/www -p /etc/ssl/xip.io -c xip.io -a friendica.local
sudo a2dissite 000-default
sudo service apache2 restart
apt-get install -qq apache2
a2enmod rewrite actions ssl
cp /vagrant/bin/dev/vagrant_vhost.sh /usr/local/bin/vhost
chmod guo+x /usr/local/bin/vhost
vhost -s 192.168.22.10.xip.io -d /var/www -p /etc/ssl/xip.io -c xip.io -a friendica.local
a2dissite 000-default
service apache2 restart
#Install php
echo ">>> Installing PHP7"
sudo apt-get install -y php libapache2-mod-php php-cli php-mysql php-curl php-gd php-mbstring php-xml imagemagick php-imagick php-zip
sudo systemctl restart apache2
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
systemctl restart apache2
#Install mysql
echo ">>> Installing Mysql"
sudo debconf-set-selections <<< "mariadb-server mariadb-server/root_password password root"
sudo debconf-set-selections <<< "mariadb-server mariadb-server/root_password_again password root"
sudo apt-get install -qq mariadb-server
debconf-set-selections <<< "mariadb-server mariadb-server/root_password password root"
debconf-set-selections <<< "mariadb-server mariadb-server/root_password_again password root"
apt-get install -qq mariadb-server
# enable remote access
# setting the mysql bind-address to allow connections from everywhere
sed -i "s/bind-address.*/bind-address = 0.0.0.0/" /etc/mysql/my.cnf
@ -66,41 +76,60 @@ $MYSQL -uroot -proot -e "FLUSH PRIVILEGES"
systemctl restart mysql
#configure rudimentary mail server (local delivery only)
#add Friendica accounts for local user accounts, use email address like vagrant@friendica.local, read the email with 'mail'.
echo ">>> Installing 'Local Only' postfix"
debconf-set-selections <<< "postfix postfix/mailname string friendica.local"
debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Local Only'"
sudo apt-get install -y postfix mailutils libmailutils-dev
sudo echo -e "friendica1: vagrant\nfriendica2: vagrant\nfriendica3: vagrant\nfriendica4: vagrant\nfriendica5: vagrant" >> /etc/aliases && sudo newaliases
apt-get install -qq postfix mailutils libmailutils-dev
echo -e "friendica1: vagrant\nfriendica2: vagrant\nfriendica3: vagrant\nfriendica4: vagrant\nfriendica5: vagrant" >> /etc/aliases && newaliases
# Friendica needs git for fetching some dependencies
sudo apt-get install -y git
echo ">>> Installing git"
apt-get install -qq git
#make the vagrant directory the docroot
sudo rm -rf /var/www/
sudo ln -fs /vagrant /var/www
echo ">>> Symlink /var/www to /vagrant"
rm -rf /var/www/
ln -fs /vagrant /var/www
# install deps with composer
sudo apt install unzip
echo ">>> Installing php requirements"
apt install unzip
cd /var/www
sudo -u www-data php bin/composer.phar install
php bin/composer.phar install
# initial config file for friendica in vagrant
cp /vagrant/mods/local.config.vagrant.php /vagrant/config/local.config.php
echo ">>> Setup Friendica"
# copy the .htaccess-dist file to .htaccess so that rewrite rules work
cp /vagrant/.htaccess-dist /vagrant/.htaccess
# create the friendica database
echo "create database friendica DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" | $MYSQL -u root -proot
# import test database
$MYSQL -uroot -proot friendica < /vagrant/friendica_test_data.sql
# import test database (disabled because too old)
#$MYSQL -uroot -proot friendica < /vagrant/friendica_test_data.sql
# install friendica
bin/console autoinstall -f /vagrant/mods/local.config.vagrant.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@friendica.local" en
bin/console user password "$ADMIN_NICK" "$ADMIN_PASSW"
bin/console user add "$USER_NICK" "$USER_NICK" "$USER_NICK@friendica.local" en
bin/console user password "$USER_NICK" "$USER_PASSW"
# set the admin
bin/console config config admin_email ""$ADMIN_NICK@friendica.local""
# create cronjob - activate if you have enough memory in you dev VM
echo "*/10 * * * * cd /vagrant; /usr/bin/php bin/worker.php" >> friendicacron
sudo crontab friendicacron
sudo rm friendicacron
# 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
# friendica needs write access to /tmp
sudo chmod 777 /tmp
chmod 777 /tmp

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="friendica" default="test">
<!-- ====================================================== -->
<!-- Target: clean-test -->
<!-- deletes directories with old test reports -->
<!-- ====================================================== -->
<target name="clean-test">
<delete dir="report" />
</target>
<!-- ====================================================== -->
<!-- Target: prepare-test -->
<!-- creates directories for test reports -->
<!-- ====================================================== -->
<target name="prepare-test" depends="clean-test">
<mkdir dir="report" />
</target>
<!-- =================================== -->
<!-- Target: test -->
<!-- this target runs all test files -->
<!-- =================================== -->
<target name="test" depends="prepare-test">
<!-- coverage-setup database="./report/coverage-database">
<fileset dir=".">
<include name="**/*.php" />
<exclude name="*test.php"/>
<exclude name="index.php"/>
<exclude name="library/**"/>
<exclude name="doc/**"/>
<exclude name=".."/>
</fileset>
</coverage-setup -->
<phpunit printsummary="true">
<batchtest>
<fileset dir="tests">
<include name="*test.php" />
</fileset>
</batchtest>
<formatter type="xml" todir="report" outfile="testlog.xml" />
</phpunit>
<phpunitreport infile="report/testlog.xml" todir="report" />
<!-- coverage-report outfile="report/coverage-database">
<report todir="report" styledir="/home/phing/etc" />
</coverage-report -->
</target>
<!-- ===================================================== -->
<!-- Target: clean-doc -->
<!-- this target removes documentation from a previous run -->
<!-- ===================================================== -->
<target name="doc-clean">
<echo msg="Removing old documentation..." />
<delete dir="./doc/api/" />
<echo msg="Generate documentation directory..." />
<mkdir dir="./doc/api/" />
</target>
<!-- ====================================== -->
<!-- Target: doc -->
<!-- this target builds all documentation -->
<!-- ====================================== -->
<target name="doc" depends="doc-clean">
<echo msg="Building documentation..." />
<docblox title="Friendica API" destdir="./doc/api">
<fileset dir=".">
<include name="**/*.php" />
<include name="README"/>
<include name="INSTALL.txt"/>
<include name="LICENSE"/>
</fileset>
</docblox>
</target>
</project>

91
composer.lock generated
View file

@ -736,16 +736,16 @@
},
{
"name": "guzzlehttp/psr7",
"version": "1.8.1",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "35ea11d335fd638b5882ff1725228b3d35496ab1"
"reference": "dc960a912984efb74d0a90222870c72c87f10c91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1",
"reference": "35ea11d335fd638b5882ff1725228b3d35496ab1",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
"reference": "dc960a912984efb74d0a90222870c72c87f10c91",
"shasum": ""
},
"require": {
@ -803,7 +803,7 @@
"uri",
"url"
],
"time": "2021-03-21T16:25:00+00:00"
"time": "2021-04-26T09:17:50+00:00"
},
{
"name": "league/html-to-markdown",
@ -1119,16 +1119,16 @@
},
{
"name": "monolog/monolog",
"version": "1.26.0",
"version": "1.26.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33"
"reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/2209ddd84e7ef1256b7af205d0717fb62cfc9c33",
"reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5",
"reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5",
"shasum": ""
},
"require": {
@ -1197,7 +1197,7 @@
"type": "tidelift"
}
],
"time": "2020-12-14T12:56:38+00:00"
"time": "2021-05-28T08:32:12+00:00"
},
{
"name": "nikic/fast-route",
@ -1591,19 +1591,11 @@
},
{
"name": "npm-asset/jgrowl",
"version": "1.4.6",
"version": "1.4.8",
"dist": {
"type": "tar",
"url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.6.tgz",
"shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3"
},
"require-dev": {
"npm-asset/grunt": "~0.4.2",
"npm-asset/grunt-contrib-cssmin": "~0.9.0",
"npm-asset/grunt-contrib-jshint": "~0.6.3",
"npm-asset/grunt-contrib-less": "~0.11.0",
"npm-asset/grunt-contrib-uglify": "~0.4.0",
"npm-asset/grunt-contrib-watch": "~0.6.1"
"url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.8.tgz",
"shasum": "4ba40ffb93757a7e1d9b262d916be299d03df3a4"
},
"type": "npm-asset-library",
"extra": {
@ -1616,8 +1608,13 @@
"type": "git",
"url": "git+ssh://git@github.com/stanlemon/jGrowl.git"
},
"npm-asset-scripts": []
"npm-asset-scripts": {
"build": "grunt"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Stan Lemon",
@ -1627,7 +1624,7 @@
],
"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": "2017-07-21T02:36:34+00:00"
"time": "2021-05-20T17:11:40+00:00"
},
{
"name": "npm-asset/jquery",
@ -2224,16 +2221,16 @@
},
{
"name": "paragonie/certainty",
"version": "v2.8.0",
"version": "v2.8.1",
"source": {
"type": "git",
"url": "https://github.com/paragonie/certainty.git",
"reference": "94fc99f8c1f5bdce960713d6b63a108a64d90dfa"
"reference": "e2a1da558f95074545ad811d60359c74500a5e24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/certainty/zipball/94fc99f8c1f5bdce960713d6b63a108a64d90dfa",
"reference": "94fc99f8c1f5bdce960713d6b63a108a64d90dfa",
"url": "https://api.github.com/repos/paragonie/certainty/zipball/e2a1da558f95074545ad811d60359c74500a5e24",
"reference": "e2a1da558f95074545ad811d60359c74500a5e24",
"shasum": ""
},
"require": {
@ -2241,12 +2238,12 @@
"ext-json": "*",
"guzzlehttp/guzzle": "^6|^7",
"paragonie/constant_time_encoding": "^1|^2",
"paragonie/sodium_compat": "^1.11",
"paragonie/sodium_compat": "^1.13",
"php": "^5.5|^7|^8"
},
"require-dev": {
"composer/composer": "^1",
"phpunit/phpunit": "^4|^5|^6|^7"
"composer/composer": "^1|>=2.0.14",
"phpunit/phpunit": "^4|^5|^6|^7|^8|^9"
},
"bin": [
"bin/certainty-cert-symlink"
@ -2282,7 +2279,7 @@
"ssl",
"tls"
],
"time": "2020-10-15T08:10:12+00:00"
"time": "2021-05-25T18:27:41+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@ -2442,16 +2439,16 @@
},
{
"name": "paragonie/sodium_compat",
"version": "v1.14.0",
"version": "v1.16.1",
"source": {
"type": "git",
"url": "https://github.com/paragonie/sodium_compat.git",
"reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3"
"reference": "2e856afe80bfc968b47da1f4a7e1ea8f03d06b38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3",
"reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/2e856afe80bfc968b47da1f4a7e1ea8f03d06b38",
"reference": "2e856afe80bfc968b47da1f4a7e1ea8f03d06b38",
"shasum": ""
},
"require": {
@ -2520,7 +2517,7 @@
"secret-key cryptography",
"side-channel resistant"
],
"time": "2020-12-03T16:26:19+00:00"
"time": "2021-05-25T12:58:14+00:00"
},
{
"name": "patrickschur/language-detection",
@ -2625,16 +2622,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "2.0.30",
"version": "2.0.31",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36"
"reference": "233a920cb38636a43b18d428f9a8db1f0a1a08f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/136b9ca7eebef78be14abf90d65c5e57b6bc5d36",
"reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/233a920cb38636a43b18d428f9a8db1f0a1a08f4",
"reference": "233a920cb38636a43b18d428f9a8db1f0a1a08f4",
"shasum": ""
},
"require": {
@ -2726,7 +2723,7 @@
"type": "tidelift"
}
],
"time": "2020-12-17T05:42:04+00:00"
"time": "2021-04-06T13:56:45+00:00"
},
{
"name": "pragmarx/google2fa",
@ -3058,16 +3055,16 @@
},
{
"name": "psr/log",
"version": "1.1.3",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"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/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
@ -3091,7 +3088,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
@ -3101,7 +3098,7 @@
"psr",
"psr-3"
],
"time": "2020-03-23T09:12:05+00:00"
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "ralouphie/getallheaders",

View file

@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2021.06-rc (Siberian Iris)
-- DB_UPDATE_VERSION 1419
-- DB_UPDATE_VERSION 1421
-- ------------------------------------------
@ -819,6 +819,33 @@ CREATE TABLE IF NOT EXISTS `manage` (
FOREIGN KEY (`mid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='table of accounts that can manage each other';
--
-- TABLE notification
--
CREATE TABLE IF NOT EXISTS `notification` (
`id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID',
`uid` mediumint unsigned COMMENT 'Owner User id',
`vid` smallint unsigned COMMENT 'Id of the verb table entry that contains the activity verbs',
`type` tinyint unsigned COMMENT '',
`actor-id` int unsigned COMMENT 'Link to the contact table with uid=0 of the actor that caused the notification',
`target-uri-id` int unsigned COMMENT 'Item-uri id of the related post',
`parent-uri-id` int unsigned COMMENT 'Item-uri id of the parent of the related post',
`created` datetime COMMENT '',
`seen` boolean DEFAULT '0' COMMENT '',
PRIMARY KEY(`id`),
UNIQUE INDEX `uid_vid_type_actor-id_target-uri-id` (`uid`,`vid`,`type`,`actor-id`,`target-uri-id`),
INDEX `vid` (`vid`),
INDEX `actor-id` (`actor-id`),
INDEX `target-uri-id` (`target-uri-id`),
INDEX `parent-uri-id` (`parent-uri-id`),
INDEX `seen_uid` (`seen`,`uid`),
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`vid`) REFERENCES `verb` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT,
FOREIGN KEY (`actor-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`target-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`parent-uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='notifications';
--
-- TABLE notify
--
@ -1685,7 +1712,9 @@ CREATE VIEW `post-user-view` AS SELECT
`parent-post`.`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`.`network` AS `parent-author-network`
`parent-post-author`.`network` AS `parent-author-network`,
`parent-post-author`.`blocked` AS `parent-author-blocked`,
`parent-post-author`.`hidden` AS `parent-author-hidden`
FROM `post-user`
STRAIGHT_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`

View file

@ -122,7 +122,6 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en
These emdpoints are planned to be implemented somewhere in the future.
- [`PATCH /api/v1/accounts/update_credentials`](https://docs.joinmastodon.org/methods/accounts/)
- [`GET /api/v1/instance/activity`](https://docs.joinmastodon.org/methods/instance#weekly-activity)
## Dummy endpoints
@ -139,7 +138,7 @@ They refer to features that don't exist in Friendica yet.
## Non supportable endpoints
These endpoints won't be implemented at the moment.
They refer to features that don't exist in Friendica yet.
They refer to features or data that don't exist in Friendica yet.
- [`POST /api/v1/accounts`](https://docs.joinmastodon.org/methods/accounts/)
- [`POST /api/v1/accounts/:id/pin`](https://docs.joinmastodon.org/methods/accounts/)
@ -164,6 +163,7 @@ They refer to features that don't exist in Friendica yet.
- [`POST /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/)
- [`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/)
- [`GET /api/v1/polls/:id`](https://docs.joinmastodon.org/methods/statuses/polls/)
- [`POST /api/v1/polls/:id/votes`](https://docs.joinmastodon.org/methods/statuses/polls/)

View file

@ -33,8 +33,9 @@ User
If this FAQ does not answer your question you can always reach out to the community via the following options:
* Friendica Support Forum: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
* 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: support(at)forum.friendi.ca
* IRC: [#friendica at freenode.net](https://webchat.freenode.net/?settings=#friendica)
* IRC: #friendica at libera.chat or [#friendica at freenode.net](https://webchat.freenode.net/?settings=#friendica)
* Matrix: [#friendica-en:matrix.org](https://matrix.to/#/#friendica-en:matrix.org) or [#friendi.ca:matrix.org](https://matrix.to/#/#friendi.ca:matrix.org)
* [Mailing List](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
<!--- * [XMPP](xmpp:support@forum.friendi.ca?join)
@ -182,6 +183,7 @@ Example: Friendica Support
### What friendica clients can I use?
Friendica is using a [Twitter/GNU Social compatible API](help/api), which means you can use any Twitter/GNU Social client for your platform as long as you can change the API path in its settings.
Since the 2021.06 release, Friendica also supports the Mastodon API.
Here is a list of known working clients:
* Android
@ -195,6 +197,11 @@ Here is a list of known working clients:
* [Choqok](https://choqok.kde.org)
* Windows
* [Friendica Mobile](https://www.microsoft.com/de-DE/store/p/friendica-mobile/9nblggh0fhmn?rtc=1) (Windows 10)
* [Husky](https://husky.fwgs.ru)
* [Subway Tooter](https://github.com/tateisu/SubwayTooter)
* [Tusky](https://tusky.app)
* [twitlatte](https://github.com/moko256/twitlatte)
* [Yuito](https://github.com/accelforce/Yuito)
Depending on the features of the client you might encounter some glitches in usability, like being limited in the length of your postings to 140 characters and having no access to the [permission settings](help/Groups-and-Privacy).

View file

@ -68,6 +68,7 @@ Friendica Documentation and Resources
* Ways to get Support
* Friendica Support Forum: [@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 bridget) 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 freenode.net](https://webchat.freenode.net/?settings=#friendica)
* Matrix: [#friendi.ca](https://matrix.to/#/#friendi.ca:matrix.org) or [#friendica-en](https://matrix.to/#/#friendica-en:matrix.org) at matrix.org

View file

@ -36,8 +36,9 @@ Wenn Du Deinen Account nicht nutzen kannst, kannst Du einen Account auf einer ö
Wenn du dir keinen weiteren Friendica Account einrichten willst, kannst du auch gerne über einen der folgenden alternativen Kanäle Hilfe suchen:
* Friendica Support Forum: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers)
* 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: support(at)forum.friendi.ca
* IRC: [#friendica at freenode.net](https://webchat.freenode.net/?settings=#friendica)
* IRC: #friendica aut libera.chat oder [#friendica auf freenode.net](https://webchat.freenode.net/?settings=#friendica)
* Matrix: [#friendica-en:matrix.org](https://matrix.to/#/#friendica-en:matrix.org) or [#friendi.ca:matrix.org](https://matrix.to/#/#friendi.ca:matrix.org)
* [Mailing List](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
<!--- * [XMPP](xmpp:support@forum.friendi.ca?join)
@ -192,6 +193,7 @@ Beispiel: Friendica Support
### Gibt es Clients für Friendica?
Friendica verwendet eine [Twitter/GNU Social](help/api) kompatible API.
Seit der Version 2021.06 unterstützt Friendica außerdem die Mastodon API.
Das bedeutet, dass du jeden Twitter/GNU Social Client verwenden kannst in dem du den API Pfad entsprechend änderst.
Hier ist eine Liste von Clients bei denen dies möglich ist, bzw. die speziell für Friendica entwickelt werden:
@ -205,6 +207,11 @@ Hier ist eine Liste von Clients bei denen dies möglich ist, bzw. die speziell f
* Mustard and Mustard-Mod
* SailfishOS
* [Friendly](https://openrepos.net/content/fabrixxm/friendly#comment-form)
* [Husky](https://husky.fwgs.ru)
* [Subway Tooter](https://github.com/tateisu/SubwayTooter)
* [Tusky](https://tusky.app)
* [twitlatte](https://github.com/moko256/twitlatte)
* [Yuito](https://github.com/accelforce/Yuito)
* Linux
* Hotot
* Choqok

View file

@ -64,8 +64,9 @@ Friendica - Dokumentation und Ressourcen
* Support Kanäle
* Friendica Support Forum: [@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 irc.freenode.net](https://webchat.freenode.net/?settings=#friendica)
* IRC: #friendica auf libera.chat oder [#friendica auf irc.freenode.net](https://webchat.freenode.net/?settings=#friendica)
* Matrix: [#friendi.ca](https://matrix.to/#/#friendi.ca:matrix.org) oder [#friendica-en](https://matrix.to/#/#friendica-en:matrix.org) auf matrix.org
**Über diese Seite**

View file

@ -737,8 +737,6 @@ function conversation_fetch_comments($thread_items, bool $pinned, array $activit
}
}
$name = $row['causer-contact-type'] == Contact::TYPE_RELAY ? $row['causer-link'] : $row['causer-name'];
switch ($row['post-reason']) {
case Item::PR_TO:
$row['direction'] = ['direction' => 7, 'title' => DI::l10n()->t('You had been addressed (%s).', 'to')];
@ -769,9 +767,9 @@ function conversation_fetch_comments($thread_items, bool $pinned, array $activit
if (($row['gravity'] == GRAVITY_PARENT) && !empty($row['causer-id'])) {
$causer = ['uid' => 0, 'id' => $row['causer-id'],
'network' => $row['causer-network'], 'url' => $row['causer-link']];
$row['reshared'] = DI::l10n()->t('%s reshared this.', '<a href="'. htmlentities(Contact::magicLinkByContact($causer)) .'">' . htmlentities($name) . '</a>');
$row['reshared'] = DI::l10n()->t('%s reshared this.', '<a href="'. htmlentities(Contact::magicLinkByContact($causer)) .'">' . htmlentities($row['causer-name']) . '</a>');
}
$row['direction'] = ['direction' => 3, 'title' => (empty($row['causer-id']) ? DI::l10n()->t('Reshared') : DI::l10n()->t('Reshared by %s', $name))];
$row['direction'] = ['direction' => 3, 'title' => (empty($row['causer-id']) ? DI::l10n()->t('Reshared') : DI::l10n()->t('Reshared by %s <%s>', $row['causer-name'], $row['causer-link']))];
break;
case Item::PR_COMMENT:
$row['direction'] = ['direction' => 5, 'title' => DI::l10n()->t('%s is participating in this thread.', $row['author-name'])];
@ -783,10 +781,10 @@ function conversation_fetch_comments($thread_items, bool $pinned, array $activit
$row['direction'] = ['direction' => 9, 'title' => DI::l10n()->t('Global')];
break;
case Item::PR_RELAY:
$row['direction'] = ['direction' => 10, 'title' => (empty($row['causer-id']) ? DI::l10n()->t('Relayed') : DI::l10n()->t('Relayed by %s.', $name))];
$row['direction'] = ['direction' => 10, 'title' => (empty($row['causer-id']) ? DI::l10n()->t('Relayed') : DI::l10n()->t('Relayed by %s <%s>', $row['causer-name'], $row['causer-link']))];
break;
case Item::PR_FETCHED:
$row['direction'] = ['direction' => 2, 'title' => (empty($row['causer-id']) ? DI::l10n()->t('Fetched') : DI::l10n()->t('Fetched because of %s', $name))];
$row['direction'] = ['direction' => 2, 'title' => (empty($row['causer-id']) ? DI::l10n()->t('Fetched') : DI::l10n()->t('Fetched because of %s <%s>', $row['causer-name'], $row['causer-link']))];
break;
}

View file

@ -184,9 +184,9 @@ function notification($params)
// First go for the general message
// "George Bull's post"
if ($params['activity']['origin_comment']) {
if (!empty($params['activity']['origin_comment'])) {
$message = $l10n->t('%1$s replied to you on %2$s\'s %3$s %4$s');
} elseif ($params['activity']['explicit_tagged']) {
} elseif (!empty($params['activity']['explicit_tagged'])) {
$message = $l10n->t('%1$s tagged you on %2$s\'s %3$s %4$s');
} else {
$message = $l10n->t('%1$s commented on %2$s\'s %3$s %4$s');
@ -197,10 +197,10 @@ function notification($params)
// Then look for the special cases
// "your post"
if ($params['activity']['origin_thread']) {
if ($params['activity']['origin_comment']) {
if (!empty($params['activity']['origin_thread'])) {
if (!empty($params['activity']['origin_comment'])) {
$message = $l10n->t('%1$s replied to you on your %2$s %3$s');
} elseif ($params['activity']['explicit_tagged']) {
} elseif (!empty($params['activity']['explicit_tagged'])) {
$message = $l10n->t('%1$s tagged you on your %2$s %3$s');
} else {
$message = $l10n->t('%1$s commented on your %2$s %3$s');
@ -209,9 +209,9 @@ function notification($params)
$dest_str = sprintf($message, $params['source_name'], $item_post_type, $title);
// "their post"
} elseif ($item['author-link'] == $params['source_link']) {
if ($params['activity']['origin_comment']) {
if (!empty($params['activity']['origin_comment'])) {
$message = $l10n->t('%1$s replied to you on their %2$s %3$s');
} elseif ($params['activity']['explicit_tagged']) {
} elseif (!empty($params['activity']['explicit_tagged'])) {
$message = $l10n->t('%1$s tagged you on their %2$s %3$s');
} else {
$message = $l10n->t('%1$s commented on their %2$s %3$s');
@ -224,7 +224,7 @@ function notification($params)
// So, we cannot have different subjects for notifications of the same thread.
// Before this we have the name of the replier on the subject rendering
// different subjects for messages on the same thread.
if ($params['activity']['explicit_tagged']) {
if (!empty($params['activity']['explicit_tagged'])) {
$subject = $l10n->t('%s %s tagged you', $subjectPrefix, $params['source_name']);
$preamble = $l10n->t('%1$s tagged you at %2$s', $params['source_name'], $sitename);

View file

@ -84,7 +84,7 @@ function cal_init(App $a)
'$about' => BBCode::convert($a->profile['about']),
]);
$cal_widget = Widget\CalendarExport::getHTML();
$cal_widget = Widget\CalendarExport::getHTML($user['uid']);
if (empty(DI::page()['aside'])) {
DI::page()['aside'] = '';

View file

@ -128,6 +128,7 @@ function dfrn_poll_init(App $a)
$_SESSION['visitor_handle'] = $r[0]['addr'];
$_SESSION['visitor_visiting'] = $r[0]['uid'];
$_SESSION['my_url'] = $r[0]['url'];
$_SESSION['remote_comment'] = $r[0]['subscribe'];
Session::setVisitorsContacts();
@ -497,8 +498,10 @@ function dfrn_poll_content(App $a)
$_SESSION['authenticated'] = 1;
$_SESSION['visitor_id'] = $r[0]['id'];
$_SESSION['visitor_home'] = $r[0]['url'];
$_SESSION['visitor_handle'] = $r[0]['addr'];
$_SESSION['visitor_visiting'] = $r[0]['uid'];
$_SESSION['my_url'] = $r[0]['url'];
$_SESSION['remote_comment'] = $r[0]['subscribe'];
Session::setVisitorsContacts();

View file

@ -236,6 +236,7 @@ function display_content(App $a, $update = false, $update_uid = 0)
}
if (!DI::pConfig()->get(local_user(), 'system', 'detailed_notif')) {
DBA::update('notification', ['seen' => true], ['parent-uri-id' => $item['parent-uri-id'], 'uid' => local_user()]);
DBA::update('notify', ['seen' => true], ['parent-uri-id' => $item['parent-uri-id'], 'uid' => local_user()]);
}

View file

@ -59,7 +59,7 @@ function events_init(App $a)
DI::page()['aside'] = '';
}
$cal_widget = CalendarExport::getHTML();
$cal_widget = CalendarExport::getHTML(local_user());
DI::page()['aside'] .= $cal_widget;

View file

@ -410,7 +410,7 @@ function item_post(App $a) {
}
}
$success = ItemHelper::replaceTag($body, $inform, local_user() ? local_user() : $profile_uid, $tag, $network);
if ($success = ItemHelper::replaceTag($body, $inform, local_user() ? local_user() : $profile_uid, $tag, $network)) {
if ($success['replaced']) {
$tagged[] = $tag;
}
@ -427,6 +427,7 @@ function item_post(App $a) {
$forum_contact = $success['contact'];
}
}
}
return $body;
});
@ -713,13 +714,6 @@ function item_post(App $a) {
unset($datarray['self']);
unset($datarray['api_source']);
if ($origin) {
$signed = Diaspora::createCommentSignature($uid, $datarray);
if (!empty($signed)) {
$datarray['diaspora_signed_text'] = json_encode($signed);
}
}
$post_id = Item::insert($datarray);
if (!$post_id) {

View file

@ -1275,7 +1275,7 @@ function photos_content(App $a)
}
if (!empty($link_item['parent']) && !empty($link_item['uid'])) {
$condition = ["`parent` = ? AND `gravity` != ?", $link_item['parent'], GRAVITY_PARENT];
$condition = ["`parent` = ? AND `gravity` = ?", $link_item['parent'], GRAVITY_COMMENT];
$total = Post::count($condition);
$pager = new Pager(DI::l10n(), DI::args()->getQueryString());

View file

@ -81,25 +81,27 @@ function tagrm_content(App $a)
{
$o = '';
$photo_return = $_SESSION['photo_return'] ?? '';
if (!local_user()) {
DI::baseUrl()->redirect($_SESSION['photo_return']);
DI::baseUrl()->redirect($photo_return);
// NOTREACHED
}
if ($a->argc == 3) {
update_tags($a->argv[1], [Strings::escapeTags(trim(hex2bin($a->argv[2])))]);
DI::baseUrl()->redirect($_SESSION['photo_return']);
DI::baseUrl()->redirect($photo_return);
}
$item_id = (($a->argc > 1) ? intval($a->argv[1]) : 0);
if (!$item_id) {
DI::baseUrl()->redirect($_SESSION['photo_return']);
DI::baseUrl()->redirect($photo_return);
// NOTREACHED
}
$item = Post::selectFirst(['uri-id'], ['id' => $item_id, 'uid' => local_user()]);
if (!DBA::isResult($item)) {
DI::baseUrl()->redirect($_SESSION['photo_return']);
DI::baseUrl()->redirect($photo_return);
}
$tag_text = Tag::getCSVByURIId($item['uri-id']);
@ -107,7 +109,7 @@ function tagrm_content(App $a)
$arr = explode(',', $tag_text);
if (empty($arr)) {
DI::baseUrl()->redirect($_SESSION['photo_return']);
DI::baseUrl()->redirect($photo_return);
}
$o .= '<h3>' . DI::l10n()->t('Remove Item Tag') . '</h3>';

View file

@ -29,6 +29,7 @@ return [
// ****************************************************************
'config' => [
'hostname' => 'friendica.local',
'admin_email' => 'admin@friendica.local',
'sitename' => 'Friendica Social Network',
'register_policy' => \Friendica\Module\Register::OPEN,
@ -38,5 +39,6 @@ return [
'default_timezone' => 'UTC',
'language' => 'en',
'basepath' => '/vagrant',
'ssl_policy' => \Friendica\App\BaseURL::SSL_POLICY_SELFSIGN,
],
];

24
mods/phpdoc-config.xml Normal file
View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpdocumentor xmlns="https://www.phpdoc.org" configVersion="3.0">
<paths>
<output>../doc/api</output>
<cache>../doc/cache</cache>
</paths>
<version number="3.0">
<api>
<source dsn="../">
<path>src</path>
<path>mod</path>
<path>include</path>
<path>static</path>
<path>bin</path>
<path>view</path>
</source>
<ignore>
<path>vendor/**/*</path>
<path>asset/**/*</path>
<path>bin/dev/**/*</path>
</ignore>
</api>
</version>
</phpdocumentor>

View file

@ -34,6 +34,7 @@ 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;
@ -464,6 +465,11 @@ class App
if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
Core\Session::set('my_url', $_GET['zrl']);
Core\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);

View file

@ -30,7 +30,6 @@ use Friendica\Core\Installer;
use Friendica\Core\Theme;
use Friendica\Database\Database;
use Friendica\Util\BasePath;
use Friendica\Util\ConfigFileLoader;
use RuntimeException;
class AutomaticInstallation extends Console
@ -112,7 +111,7 @@ HELP;
protected function doExecute()
{
// Initialise the app
$this->out("Initializing setup...\n");
$this->out("Initializing setup...");
$installer = new Installer();
@ -121,10 +120,10 @@ HELP;
$basepath = new BasePath($basePathConf);
$installer->setUpCache($configCache, $basepath->getPath());
$this->out(" Complete!\n\n");
$this->out(" Complete!\n");
// Check Environment
$this->out("Checking environment...\n");
$this->out("Checking environment...");
$installer->resetChecks();
@ -133,24 +132,26 @@ HELP;
throw new RuntimeException($errorMessage);
}
$this->out(" Complete!\n\n");
$this->out(" Complete!\n");
// if a config file is set,
$config_file = $this->getOption(['f', 'file']);
if (!empty($config_file)) {
$this->out("Loading config file '$config_file'...");
if (!file_exists($config_file)) {
throw new RuntimeException("ERROR: Config file does not exist.\n");
throw new RuntimeException("ERROR: Config file does not exist.");
}
//reload the config cache
$loader = new ConfigFileLoader($config_file);
$loader->setupCache($configCache);
//append config file to the config cache
$config = include($config_file);
if (!is_array($config)) {
throw new Exception('Error loading config file ' . $config_file);
}
$configCache->load($config, Cache::SOURCE_FILE);
} else {
// Creating config file
$this->out("Creating config file...\n");
$this->out("Creating config file...");
$save_db = $this->getOption(['s', 'savedb'], false);
@ -202,10 +203,10 @@ HELP;
$installer->createConfig($configCache);
}
$this->out("Complete!\n\n");
$this->out(" Complete!\n");
// Check database connection
$this->out("Checking database...\n");
$this->out("Checking database...");
$installer->resetChecks();
@ -214,7 +215,7 @@ HELP;
throw new RuntimeException($errorMessage);
}
$this->out(" Complete!\n\n");
$this->out(" Complete!\n");
// Install database
$this->out("Inserting data into database...\n");
@ -228,24 +229,24 @@ HELP;
if (!empty($config_file) && $config_file != 'config' . DIRECTORY_SEPARATOR . 'local.config.php') {
// Copy config file
$this->out("Copying config file...\n");
if (!copy($basePathConf . DIRECTORY_SEPARATOR . $config_file, $basePathConf . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.config.php')) {
$this->out("Copying config file...");
if (!copy($config_file, $basePathConf . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.config.php')) {
throw new RuntimeException("ERROR: Saving config file failed. Please copy '$config_file' to '" . $basePathConf . "'" . DIRECTORY_SEPARATOR . "config" . DIRECTORY_SEPARATOR . "local.config.php' manually.\n");
}
}
$this->out(" Complete!\n\n");
$this->out(" Complete!\n");
// Install theme
$this->out("Installing theme\n");
$this->out("Installing theme");
if (!empty($this->config->get('system', 'theme'))) {
Theme::install($this->config->get('system', 'theme'));
$this->out(" Complete\n\n");
$this->out(" Complete\n");
} else {
$this->out(" Theme setting is empty. Please check the file 'config/local.config.php'\n\n");
$this->out(" Theme setting is empty. Please check the file 'config/local.config.php'\n");
}
$this->out("\nInstallation is finished\n");
$this->out("\nInstallation is finished");
return 0;
}

View file

@ -127,16 +127,6 @@ class Item
$tag_type = substr($tag, 0, 1);
//is it already replaced?
if (strpos($tag, '[url=')) {
// Checking for the alias that is used for OStatus
$pattern = '/[@!]\[url\=(.*?)\](.*?)\[\/url\]/ism';
if (preg_match($pattern, $tag, $matches)) {
$data = Contact::getByURL($matches[1], false, ['alias', 'nick']);
if ($data['alias'] != '') {
$newtag = '@[url=' . $data['alias'] . ']' . $data['nick'] . '[/url]';
}
}
return $replaced;
}

View file

@ -2196,9 +2196,7 @@ class BBCode
}
}
$success = Item::replaceTag($body, $inform, $profile_uid, $tag, $network);
if ($success['replaced']) {
if (($success = Item::replaceTag($body, $inform, $profile_uid, $tag, $network)) && $success['replaced']) {
$tagged[] = $tag;
}
}

View file

@ -21,9 +21,9 @@
namespace Friendica\Content\Widget;
use Friendica\Content\Feature;
use Friendica\Core\Renderer;
use Friendica\DI;
use Friendica\Model\User;
/**
* TagCloud widget
@ -34,36 +34,27 @@ class CalendarExport
{
/**
* Get the events widget.
* @param int $uid
*
* @return string Formated HTML of the calendar widget.
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getHTML() {
$a = DI::app();
if (empty($a->data['user'])) {
return;
public static function getHTML(int $uid = 0) {
if (empty($uid)) {
return '';
}
$owner_uid = intval($a->data['user']['uid']);
// The permission testing is a little bit tricky because we have to respect many cases.
// It's not the private events page (we don't get the $owner_uid for /events).
if (!local_user() && !$owner_uid) {
return;
$user = User::getById($uid, ['nickname']);
if (empty($user['nickname'])) {
return '';
}
// $a->data is only available if the profile page is visited. If the visited page is not part
// of the profile page it should be the personal /events page. So we can use $a->user.
$user = ($a->data['user']['nickname'] ?? '') ?: $a->user['nickname'];
$tpl = Renderer::getMarkupTemplate("widget/events.tpl");
$return = Renderer::replaceMacros($tpl, [
'$etitle' => DI::l10n()->t("Export"),
'$export_ical' => DI::l10n()->t("Export calendar as ical"),
'$export_csv' => DI::l10n()->t("Export calendar as csv"),
'$user' => $user
'$user' => $user['nickname']
]);
return $return;

View file

@ -127,8 +127,8 @@ class TagCloud
private static function tagCalc(array $arr)
{
$tags = [];
$min = 1e9;
$max = -1e9;
$min = 1000000000.0;
$max = -1000000000.0;
$x = 0;
if (!$arr) {
@ -145,7 +145,7 @@ class TagCloud
}
usort($tags, 'self::tagsSort');
$range = max(.01, $max - $min) * 1.0001;
$range = max(0.01, $max - $min) * 1.0001;
for ($x = 0; $x < count($tags); $x ++) {
$tags[$x][2] = 1 + floor(9 * ($tags[$x][1] - $min) / $range);

View file

@ -465,7 +465,7 @@ class Installer
$status = $this->checkFunction('proc_open',
DI::l10n()->t('Program execution functions'),
DI::l10n()->t('Error: Program execution functions required but not enabled.'),
DI::l10n()->t('Error: Program execution functions (proc_open) required but not enabled.'),
true
);
$returnVal = $returnVal ? $status : false;

View file

@ -241,6 +241,12 @@ class DBStructure
// Assign all field that are present in the table
foreach ($fieldnames as $field) {
if (isset($data[$field])) {
// Limit the length of varchar, varbinary, char and binrary fields
if (is_string($data[$field]) && preg_match("/char\((\d*)\)/", $definition[$table]['fields'][$field]['type'], $result)) {
$data[$field] = mb_substr($data[$field], 0, $result[1]);
} elseif (is_string($data[$field]) && preg_match("/binary\((\d*)\)/", $definition[$table]['fields'][$field]['type'], $result)) {
$data[$field] = substr($data[$field], 0, $result[1]);
}
$fields[$field] = $data[$field];
}
}

View file

@ -25,22 +25,18 @@ use Friendica\BaseFactory;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Notification as ModelNotification;
use Friendica\Model\Post;
use Friendica\Model\Verb;
use Friendica\Protocol\Activity;
class Notification extends BaseFactory
{
public function createFromNotifyId(int $id)
public function createFromNotificationId(int $id)
{
$notification = DBA::selectFirst('notify', [], ['id' => $id]);
$notification = DBA::selectFirst('notification', [], ['id' => $id]);
if (!DBA::isResult($notification)) {
return null;
}
$cid = Contact::getIdForURL($notification['url'], 0, false);
if (empty($cid)) {
return null;
}
/*
follow = Someone followed you
follow_request = Someone requested to follow you
@ -51,32 +47,30 @@ class Notification extends BaseFactory
status = Someone you enabled notifications for has posted a status
*/
switch ($notification['type']) {
case ModelNotification\Type::INTRO:
$type = 'follow_request';
break;
case ModelNotification\Type::WALL:
case ModelNotification\Type::COMMENT:
case ModelNotification\Type::MAIL:
case ModelNotification\Type::TAG_SELF:
case ModelNotification\Type::POKE:
$type = 'mention';
break;
case ModelNotification\Type::SHARE:
if (($notification['vid'] == Verb::getID(Activity::FOLLOW)) && ($notification['type'] == Post\UserNotification::NOTIF_NONE)) {
$contact = Contact::getById($notification['actor-id'], ['pending']);
$type = $contact['pending'] ? $type = 'follow_request' : 'follow';
} elseif (($notification['vid'] == Verb::getID(Activity::ANNOUNCE)) &&
in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) {
$type = 'reblog';
} elseif (in_array($notification['vid'], [Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE)]) &&
in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) {
$type = 'favourite';
} elseif ($notification['type'] == Post\UserNotification::NOTIF_SHARED) {
$type = 'status';
break;
default:
} elseif (in_array($notification['type'], [Post\UserNotification::NOTIF_EXPLICIT_TAGGED,
Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT,
Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT])) {
$type = 'mention';
} else {
return null;
}
$account = DI::mstdnAccount()->createFromContactId($cid);
$account = DI::mstdnAccount()->createFromContactId($notification['actor-id'], $notification['uid']);
if (!empty($notification['uri-id'])) {
if (!empty($notification['target-uri-id'])) {
try {
$status = DI::mstdnStatus()->createFromUriId($notification['uri-id'], $notification['uid']);
$status = DI::mstdnStatus()->createFromUriId($notification['target-uri-id'], $notification['uid']);
} catch (\Throwable $th) {
$status = null;
}
@ -84,6 +78,6 @@ class Notification extends BaseFactory
$status = null;
}
return new \Friendica\Object\Api\Mastodon\Notification($id, $type, $notification['date'], $account, $status);
return new \Friendica\Object\Api\Mastodon\Notification($id, $type, $notification['created'], $account, $status);
}
}

View file

@ -81,7 +81,7 @@ class Status extends BaseFactory
Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE), 'deleted' => false]),
Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE), 'deleted' => false]),
Post\ThreadUser::getIgnored($uriId, $uid),
(bool)$item['starred'],
(bool)($item['starred'] && ($item['gravity'] == GRAVITY_PARENT)),
Post\ThreadUser::getPinned($uriId, $uid)
);

View file

@ -26,6 +26,7 @@ use Friendica\Core\Cache\Duration;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\DI;
use Friendica\Network\Probe;
use Friendica\Protocol\ActivityNamespace;
@ -349,6 +350,9 @@ class APContact
DBA::delete('apcontact', ['url' => $url]);
}
// Limit the length on incoming fields
$apcontact = DBStructure::getFieldsForTable('apcontact', $apcontact);
if (DBA::exists('apcontact', ['url' => $apcontact['url']])) {
DBA::update('apcontact', $apcontact, ['url' => $apcontact['url']]);
} else {
@ -357,7 +361,7 @@ class APContact
Logger::info('Updated profile', ['url' => $url]);
return $apcontact;
return DBA::selectFirst('apcontact', [], ['url' => $apcontact['url']]) ?: [];
}
/**

View file

@ -271,7 +271,7 @@ class Contact
// Update the contact in the background if needed
$updated = max($contact['success_update'], $contact['created'], $contact['updated'], $contact['last-update'], $contact['failure_update']);
if (($updated < DateTimeFormat::utc('now -7 days')) && in_array($contact['network'], Protocol::FEDERATED)) {
if (($updated < DateTimeFormat::utc('now -7 days')) && in_array($contact['network'], Protocol::FEDERATED) && !self::isLocalById($contact['id'])) {
Worker::add(PRIORITY_LOW, "UpdateContact", $contact['id']);
}
@ -566,18 +566,13 @@ class Contact
*/
public static function createSelfFromUserId($uid)
{
// Only create the entry if it doesn't exist yet
if (DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
return true;
}
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'pubkey', 'prvkey'],
['uid' => $uid, 'account_expired' => false]);
if (!DBA::isResult($user)) {
return false;
}
$return = DBA::insert('contact', [
$contact = [
'uid' => $user['uid'],
'created' => DateTimeFormat::utcNow(),
'self' => 1,
@ -602,7 +597,23 @@ class Contact
'uri-date' => DateTimeFormat::utcNow(),
'avatar-date' => DateTimeFormat::utcNow(),
'closeness' => 0
]);
];
$return = true;
// Only create the entry if it doesn't exist yet
if (!DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
$return = DBA::insert('contact', $contact);
}
// Create the public contact
if (!DBA::exists('contact', ['nurl' => $contact['nurl'], 'uid' => 0])) {
$contact['self'] = false;
$contact['uid'] = 0;
$contact['prvkey'] = null;
DBA::insert('contact', $contact, Database::INSERT_IGNORE);
}
return $return;
}
@ -704,6 +715,8 @@ class Contact
DBA::update('contact', $fields, ['id' => $self['id']]);
// Update the public contact as well
$fields['prvkey'] = null;
$fields['self'] = false;
DBA::update('contact', $fields, ['uid' => 0, 'nurl' => $self['nurl']]);
// Update the profile
@ -1941,7 +1954,7 @@ class Contact
return false;
}
if (Contact::isLocal($ret['url'])) {
if (self::isLocal($ret['url'])) {
Logger::info('Local contacts are not updated here.');
return true;
}
@ -2523,6 +2536,8 @@ class Contact
// Ensure to always have the correct network type, independent from the connection request method
self::updateFromProbe($contact['id']);
Post\UserNotification::insertNotication($contact['id'], Verb::getID(Activity::FOLLOW), $importer['uid']);
return true;
} else {
// send email notification to owner?
@ -2554,6 +2569,8 @@ class Contact
self::updateAvatar($contact_id, $photo, true);
Post\UserNotification::insertNotication($contact_id, Verb::getID(Activity::FOLLOW), $importer['uid']);
$contact_record = DBA::selectFirst('contact', ['id', 'network', 'name', 'url', 'photo'], ['id' => $contact_id]);
/// @TODO Encapsulate this into a function/method

View file

@ -78,19 +78,22 @@ class Relation
{
$contact = Contact::getByURL($url);
if (empty($contact)) {
Logger::info('Contact not found', ['url' => $url]);
return;
}
if (!self::isDiscoverable($url, $contact)) {
Logger::info('Contact is not discoverable', ['url' => $url]);
return;
}
$uid = User::getIdForURL($url);
if (!empty($uid)) {
// Fetch the followers/followings locally
Logger::info('Fetch the followers/followings locally', ['url' => $url]);
$followers = self::getContacts($uid, [Contact::FOLLOWER, Contact::FRIEND]);
$followings = self::getContacts($uid, [Contact::SHARING, Contact::FRIEND]);
} else {
} elseif (!Contact::isLocal($url)) {
Logger::info('Fetch the followers/followings by polling the endpoints', ['url' => $url]);
$apcontact = APContact::getByURL($url, false);
if (!empty($apcontact['followers']) && is_string($apcontact['followers'])) {
@ -104,6 +107,10 @@ class Relation
} else {
$followings = [];
}
} else {
Logger::notice('Contact seems to be local but could not be found here', ['url' => $url]);
$followers = [];
$followings = [];
}
if (empty($followers) && empty($followings)) {

View file

@ -802,6 +802,7 @@ class GServer
/**
* Parses Nodeinfo 2
*
* @see https://git.feneas.org/jaywink/nodeinfo2
* @param string $nodeinfo_url address of the nodeinfo path
* @return array Server data
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
@ -850,8 +851,10 @@ class GServer
if (!empty($nodeinfo['protocols'])) {
$protocols = [];
foreach ($nodeinfo['protocols'] as $protocol) {
if (is_string($protocol)) {
$protocols[$protocol] = true;
}
}
if (!empty($protocols['dfrn'])) {
$server['network'] = Protocol::DFRN;

View file

@ -180,16 +180,18 @@ class Item
if (!empty($fields['body'])) {
Post\Media::insertFromAttachmentData($item['uri-id'], $fields['body']);
if ($item['author-network'] != Protocol::DFRN) {
Post\Media::insertFromRelevantUrl($item['uri-id'], $fields['body']);
}
$content_fields = ['raw-body' => trim($fields['raw-body'] ?? $fields['body'])];
// Remove all media attachments from the body and store them in the post-media table
// @todo On shared postings (Diaspora style and commented reshare) don't fetch content from the shared part
$content_fields['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']);
$content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']);
if ($item['author-network'] != Protocol::DFRN) {
Post\Media::insertFromRelevantUrl($item['uri-id'], $content_fields['raw-body']);
}
Post\Content::update($item['uri-id'], $content_fields);
}
if (!empty($fields['file'])) {
@ -991,14 +993,14 @@ class Item
Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']);
if (!DBA::exists('contact', ['id' => $item['author-id'], 'network' => Protocol::DFRN])) {
Post\Media::insertFromRelevantUrl($item['uri-id'], $item['body']);
}
// Remove all media attachments from the body and store them in the post-media table
$item['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $item['raw-body']);
$item['raw-body'] = self::setHashtags($item['raw-body']);
if (!DBA::exists('contact', ['id' => $item['author-id'], 'network' => Protocol::DFRN])) {
Post\Media::insertFromRelevantUrl($item['uri-id'], $item['raw-body']);
}
// Check for hashtags in the body and repair or add hashtag links
$item['body'] = self::setHashtags($item['body']);
@ -1018,6 +1020,30 @@ class Item
if (empty($item['event-id'])) {
unset($item['event-id']);
$ev = Event::fromBBCode($item['body']);
if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
Logger::info('Event found.');
$ev['cid'] = $item['contact-id'];
$ev['uid'] = $item['uid'];
$ev['uri'] = $item['uri'];
$ev['edited'] = $item['edited'];
$ev['private'] = $item['private'];
$ev['guid'] = $item['guid'];
$ev['plink'] = $item['plink'];
$ev['network'] = $item['network'];
$ev['protocol'] = $item['protocol'];
$ev['direction'] = $item['direction'];
$ev['source'] = $item['source'];
$event = DBA::selectFirst('event', ['id'], ['uri' => $item['uri'], 'uid' => $item['uid']]);
if (DBA::isResult($event)) {
$ev['id'] = $event['id'];
}
$item['event-id'] = Event::store($ev);
Logger::info('Event was stored', ['id' => $item['event-id']]);
}
}
if (empty($item['causer-id'])) {
@ -1034,7 +1060,14 @@ class Item
Post\Content::insert($item['uri-id'], $item);
}
// Diaspora signature
// Create Diaspora signature
if ($item['origin'] && empty($item['diaspora_signed_text'])) {
$signed = Diaspora::createCommentSignature($uid, $item);
if (!empty($signed)) {
$item['diaspora_signed_text'] = json_encode($signed);
}
}
if (!empty($item['diaspora_signed_text'])) {
DBA::replace('diaspora-interaction', ['uri-id' => $item['uri-id'], 'interaction' => $item['diaspora_signed_text']]);
}
@ -1325,16 +1358,23 @@ class Item
* @param integer $uri_id URI-ID of the given item
* @param integer $uid The user that will receive the item entry
* @param array $fields Additional fields to be stored
* @param integer $source_uid User id of the source post
* @return integer stored item id
*/
public static function storeForUserByUriId(int $uri_id, int $uid, array $fields = [])
public static function storeForUserByUriId(int $uri_id, int $uid, array $fields = [], int $source_uid = 0)
{
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['uri-id' => $uri_id, 'uid' => 0]);
if (!DBA::isResult($item)) {
if ($uid == $source_uid) {
Logger::warning('target UID must not be be equal to the source UID', ['uri-id' => $uri_id, 'uid' => $uid]);
return 0;
}
if (($item['private'] == self::PRIVATE) || !in_array($item['network'], Protocol::FEDERATED)) {
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['uri-id' => $uri_id, 'uid' => $source_uid]);
if (!DBA::isResult($item)) {
Logger::warning('Item could not be fetched', ['uri-id' => $uri_id, 'uid' => $source_uid]);
return 0;
}
if (($source_uid == 0) && (($item['private'] == self::PRIVATE) || !in_array($item['network'], Protocol::FEDERATED))) {
Logger::notice('Item is private or not from a federated network. It will not be stored for the user.', ['uri-id' => $uri_id, 'uid' => $uid, 'private' => $item['private'], 'network' => $item['network']]);
return 0;
}
@ -1343,8 +1383,25 @@ class Item
$item = array_merge($item, $fields);
$is_reshare = ($item['gravity'] == GRAVITY_ACTIVITY) && ($item['verb'] == Activity::ANNOUNCE);
if ((($item['gravity'] == GRAVITY_PARENT) || $is_reshare) &&
DI::pConfig()->get($uid, 'system', 'accept_only_sharer') &&
!Contact::isSharingByURL($item['author-link'], $uid) &&
!Contact::isSharingByURL($item['owner-link'], $uid)) {
Logger::info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid]);
return 0;
}
if ((($item['gravity'] == GRAVITY_COMMENT) || $is_reshare) && !Post::exists(['uri-id' => $item['thr-parent-id'], 'uid' => $uid])) {
// Only do an auto complete with the source uid "0" to prevent privavy problems
$causer = $item['causer-id'] ?: $item['author-id'];
$result = self::storeForUserByUriId($item['thr-parent-id'], $uid, ['causer-id' => $causer, 'post-reason' => self::PR_FETCHED]);
Logger::info('Fetched thread parent', ['uri-id' => $item['thr-parent-id'], 'uid' => $uid, 'causer' => $causer, 'result' => $result]);
}
$stored = self::storeForUser($item, $uid);
Logger::info('Public item stored for user', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'stored' => $stored]);
Logger::info('Item stored for user', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'source-uid' => $source_uid, 'stored' => $stored]);
return $stored;
}
@ -1364,11 +1421,18 @@ class Item
}
unset($item['id']);
unset($item['parent']);
unset($item['mention']);
unset($item['starred']);
unset($item['unseen']);
unset($item['psid']);
unset($item['pinned']);
unset($item['ignored']);
unset($item['pubmail']);
unset($item['forum_mode']);
unset($item['event-id']);
unset($item['hidden']);
unset($item['notification-type']);
$item['uid'] = $uid;
$item['origin'] = 0;
@ -1394,8 +1458,6 @@ class Item
$item['contact-id'] = $self['id'];
}
/// @todo Handling of "event-id"
$notify = false;
if ($item['gravity'] == GRAVITY_PARENT) {
$contact = DBA::selectFirst('contact', [], ['id' => $item['contact-id'], 'self' => false]);
@ -1407,9 +1469,9 @@ class Item
$distributed = self::insert($item, $notify, true);
if (!$distributed) {
Logger::info("Distributed public item wasn't stored", ['uri-id' => $item['uri-id'], 'user' => $uid]);
Logger::info("Distributed item wasn't stored", ['uri-id' => $item['uri-id'], 'user' => $uid]);
} else {
Logger::info('Distributed public item was stored', ['uri-id' => $item['uri-id'], 'user' => $uid, 'stored' => $distributed]);
Logger::info('Distributed item was stored', ['uri-id' => $item['uri-id'], 'user' => $uid, 'stored' => $distributed]);
}
return $distributed;
}
@ -2739,9 +2801,10 @@ class Item
*
* @param string $body
* @param string $url
* @param int $type
* @return bool
*/
public static function containsLink(string $body, string $url)
public static function containsLink(string $body, string $url, int $type = 0)
{
// Make sure that for example site parameters aren't used when testing if the link is contained in the body
$urlparts = parse_url($url);
@ -2749,6 +2812,12 @@ class Item
unset($urlparts['fragment']);
$url = Network::unparseURL($urlparts);
// Remove media links to only search in embedded content
// @todo Check images for image link, audio for audio links, ...
if (in_array($type, [Post\Media::AUDIO, Post\Media::VIDEO, Post\Media::IMAGE])) {
$body = preg_replace("/\[url=[^\[\]]*\](.*)\[\/url\]/Usi", ' $1 ', $body);
}
if (strpos($body, $url)) {
return true;
}
@ -2777,7 +2846,7 @@ class Item
// @todo In the future we should make a single for the template engine with all media in it. This allows more flexibilty.
foreach ($attachments['visual'] as $attachment) {
if (self::containsLink($item['body'], $attachment['url'])) {
if (self::containsLink($item['body'], $attachment['url'], $attachment['type'])) {
continue;
}
@ -2802,7 +2871,7 @@ class Item
'mime' => $attachment['mimetype'],
],
]);
if ($item['post-type'] == Item::PT_VIDEO) {
if (($item['post-type'] ?? null) == Item::PT_VIDEO) {
$leading .= $media;
} else {
$trailing .= $media;
@ -2955,7 +3024,7 @@ class Item
// @todo Use a template
$rendered = BBCode::convertAttachment('', BBCode::INTERNAL, false, $data);
} elseif (!self::containsLink($content, $data['url'])) {
} elseif (!self::containsLink($content, $data['url'], Post\Media::HTML)) {
$rendered = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/link.tpl'), [
'$url' => $data['url'],
'$title' => $data['title'],

View file

@ -36,15 +36,14 @@ use Friendica\Worker\Delivery;
class Mail
{
/**
* Insert received private message
* Insert private message
*
* @param array $msg
* @param bool $notifiction
* @return int|boolean Message ID or false on error
*/
public static function insert($msg)
public static function insert($msg, $notifiction = true)
{
$user = User::getById($msg['uid']);
if (!isset($msg['reply'])) {
$msg['reply'] = DBA::exists('mail', ['parent-uri' => $msg['parent-uri']]);
}
@ -63,6 +62,10 @@ class Mail
$msg['created'] = (!empty($msg['created']) ? DateTimeFormat::utc($msg['created']) : DateTimeFormat::utcNow());
$msg['author-id'] = Contact::getIdForURL($msg['from-url'], 0, false);
$msg['uri-id'] = ItemURI::insert(['uri' => $msg['uri'], 'guid' => $msg['guid']]);
$msg['parent-uri-id'] = ItemURI::getIdByURI($msg['parent-uri']);
DBA::lock('mail');
if (DBA::exists('mail', ['uri' => $msg['uri'], 'uid' => $msg['uid']])) {
@ -71,12 +74,8 @@ class Mail
return false;
}
$msg['author-id'] = Contact::getIdForURL($msg['from-url'], 0, false);
$msg['uri-id'] = ItemURI::insert(['uri' => $msg['uri'], 'guid' => $msg['guid']]);
$msg['parent-uri-id'] = ItemURI::getIdByURI($msg['parent-uri']);
if ($msg['reply']) {
$reply = DBA::selectFirst('mail', ['uri', 'uri-id'], ['parent-uri' => $mail['parent-uri'], 'reply' => false]);
$reply = DBA::selectFirst('mail', ['uri', 'uri-id'], ['parent-uri' => $msg['parent-uri'], 'reply' => false]);
$msg['thr-parent'] = $reply['uri'];
$msg['thr-parent-id'] = $reply['uri-id'];
@ -95,6 +94,8 @@ class Mail
DBA::update('conv', ['updated' => DateTimeFormat::utcNow()], ['id' => $msg['convid']]);
}
if ($notifiction) {
$user = User::getById($msg['uid']);
// send notifications.
$notif_params = [
'type' => Notification\Type::MAIL,
@ -108,6 +109,7 @@ class Mail
notification($notif_params);
Logger::info('Mail is processed, notification was sent.', ['id' => $msg['id'], 'uri' => $msg['uri']]);
}
return $msg['id'];
}
@ -195,9 +197,7 @@ class Mail
$replyto = $convuri;
}
$post_id = null;
$success = DBA::insert(
'mail',
$post_id = self::insert(
[
'uid' => local_user(),
'guid' => $guid,
@ -214,13 +214,9 @@ class Mail
'uri' => $uri,
'parent-uri' => $replyto,
'created' => DateTimeFormat::utcNow()
]
], false
);
if ($success) {
$post_id = DBA::lastInsertId();
}
/**
*
* When a photo was uploaded into the message using the (profile wall) ajax
@ -301,8 +297,7 @@ class Mail
return -4;
}
DBA::insert(
'mail',
self::insert(
[
'uid' => $recipient['uid'],
'guid' => $guid,
@ -320,7 +315,7 @@ class Mail
'parent-uri' => $me['url'],
'created' => DateTimeFormat::utcNow(),
'unknown' => 1
]
], false
);
return 0;

View file

@ -164,15 +164,16 @@ class Post
* @param array $fields
* @param array $condition
* @param array $params
* @param bool $user_mode true = post-user-view, false = post-view
* @return bool|array
* @throws \Exception
* @see DBA::select
*/
public static function selectFirst(array $fields = [], array $condition = [], $params = [])
public static function selectFirst(array $fields = [], array $condition = [], $params = [], bool $user_mode = true)
{
$params['limit'] = 1;
$result = self::select($fields, $condition, $params);
$result = self::select($fields, $condition, $params, $user_mode);
if (is_bool($result)) {
return $result;

View file

@ -351,7 +351,7 @@ class Media
foreach ($attachments as $attachment) {
// Only store attachments that are part of the unshared body
if (strpos($unshared_body, $attachment['url']) !== false) {
if (Item::containsLink($unshared_body, $attachment['url'], $attachment['type'])) {
self::insert($attachment);
}
}
@ -600,7 +600,7 @@ class Media
$body = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $body);
foreach (self::getByURIId($uriid, [self::IMAGE, self::AUDIO, self::VIDEO]) as $media) {
if (Item::containsLink($body, $media['url'])) {
if (Item::containsLink($body, $media['url'], $media['type'])) {
continue;
}

View file

@ -33,7 +33,7 @@ use Friendica\Model\Post;
use Friendica\Util\Strings;
use Friendica\Model\Tag;
use Friendica\Protocol\Activity;
use Friendica\Util\DateTimeFormat;
class UserNotification
{
@ -128,8 +128,8 @@ class UserNotification
*/
public static function setNotification(int $uri_id, int $uid)
{
$fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity',
'private', 'contact-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'author-id', 'verb'];
$fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', 'vid', 'gravity',
'private', 'contact-id', 'thr-parent', 'thr-parent-id', 'parent-uri-id', 'parent-uri', 'author-id', 'verb'];
$item = Post::selectFirst($fields, ['uri-id' => $uri_id, 'uid' => $uid, 'origin' => false]);
if (!DBA::isResult($item)) {
return;
@ -177,6 +177,10 @@ class UserNotification
if (self::checkShared($item, $uid)) {
$notification_type = $notification_type | self::NOTIF_SHARED;
self::insertNoticationByItem(self::NOTIF_SHARED, $uid, $item);
$notified = true;
} else {
$notified = false;
}
$profiles = self::getProfileForUser($uid);
@ -194,38 +198,64 @@ class UserNotification
return;
}
// Only create notifications for posts and comments, not for activities
if (in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT])) {
if (self::checkImplicitMention($item, $profiles)) {
$notification_type = $notification_type | self::NOTIF_IMPLICIT_TAGGED;
}
if (self::checkExplicitMention($item, $profiles)) {
$notification_type = $notification_type | self::NOTIF_EXPLICIT_TAGGED;
if (!$notified) {
self::insertNoticationByItem( self::NOTIF_EXPLICIT_TAGGED, $uid, $item);
$notified = true;
}
}
if (self::checkCommentedThread($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_THREAD_COMMENT;
if (self::checkImplicitMention($item, $profiles)) {
$notification_type = $notification_type | self::NOTIF_IMPLICIT_TAGGED;
if (!$notified) {
self::insertNoticationByItem(self::NOTIF_IMPLICIT_TAGGED, $uid, $item);
$notified = true;
}
}
if (self::checkDirectComment($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_DIRECT_COMMENT;
if (!$notified) {
self::insertNoticationByItem(self::NOTIF_DIRECT_COMMENT, $uid, $item);
$notified = true;
}
}
if (self::checkDirectCommentedThread($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_DIRECT_THREAD_COMMENT;
if (!$notified) {
self::insertNoticationByItem(self::NOTIF_DIRECT_THREAD_COMMENT, $uid, $item);
$notified = true;
}
}
if (self::checkCommentedThread($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_THREAD_COMMENT;
if (!$notified) {
self::insertNoticationByItem(self::NOTIF_THREAD_COMMENT, $uid, $item);
$notified = true;
}
}
if (self::checkCommentedParticipation($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_COMMENT_PARTICIPATION;
if (!$notified) {
self::insertNoticationByItem(self::NOTIF_COMMENT_PARTICIPATION, $uid, $item);
$notified = true;
}
}
if (self::checkActivityParticipation($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_ACTIVITY_PARTICIPATION;
if (!$notified) {
self::insertNoticationByItem(self::NOTIF_ACTIVITY_PARTICIPATION, $uid, $item);
$notified = true;
}
}
if (empty($notification_type)) {
// Only create notifications for posts and comments, not for activities
if (empty($notification_type) || !in_array($item['gravity'], [GRAVITY_PARENT, GRAVITY_COMMENT])) {
return;
}
@ -236,6 +266,61 @@ class UserNotification
self::update($item['uri-id'], $uid, $fields, true);
}
/**
* Add a notification entry for a given item array
*
* @param int $type User notification type
* @param int $uid User ID
* @param array $item Item array
* @return boolean
*/
private static function insertNoticationByItem(int $type, int $uid, array $item)
{
if (($item['gravity'] == GRAVITY_ACTIVITY) &&
!in_array($type, [self::NOTIF_DIRECT_COMMENT, self::NOTIF_DIRECT_THREAD_COMMENT])) {
// Activities are only stored when performed on the user's post or comment
return;
}
$fields = [
'uid' => $uid,
'vid' => $item['vid'],
'type' => $type,
'actor-id' => $item['author-id'],
'parent-uri-id' => $item['parent-uri-id'],
'created' => DateTimeFormat::utcNow(),
];
if ($item['gravity'] == GRAVITY_ACTIVITY) {
$fields['target-uri-id'] = $item['thr-parent-id'];
} else {
$fields['target-uri-id'] = $item['uri-id'];
}
return DBA::insert('notification', $fields);
}
/**
* Add a notification entry
*
* @param int $actor Contact ID of the actor
* @param int $vid Verb ID
* @param int $uid User ID
* @return boolean
*/
public static function insertNotication(int $actor, int $vid, int $uid)
{
$fields = [
'uid' => $uid,
'vid' => $vid,
'type' => self::NOTIF_NONE,
'actor-id' => $actor,
'created' => DateTimeFormat::utcNow(),
];
return DBA::insert('notification', $fields);
}
/**
* Fetch all profiles (contact URL) of a given user
* @param int $uid User ID

View file

@ -145,7 +145,7 @@ class Profile
*/
public static function load(App $a, $nickname, array $profiledata = [], $show_connect = true)
{
$user = User::getByNickname($nickname);
$user = DBA::selectFirst('user', ['uid'], ['nickname' => $nickname, 'account_removed' => false]);
if (!DBA::isResult($user) && empty($profiledata)) {
Logger::log('profile error: ' . DI::args()->getQueryString(), Logger::DEBUG);
@ -263,8 +263,20 @@ class Profile
$o = '';
$location = false;
// This function can also use contact information in $profile
$is_contact = !empty($profile['cid']);
// This function can also use contact information in $profile, but the 'cid'
// value is going to be coming from 'owner-view', which means it's the wrong
// contact ID for the user viewing this page. Use 'nurl' to look up the
// correct contact table entry for the logged-in user.
$profile_contact = [];
if (!empty($profile['nurl'] ?? '')) {
if (local_user() && ($profile['uid'] ?? '') != local_user()) {
$profile_contact = Contact::getById(Contact::getIdForURL($profile['nurl'], local_user()));
}
if (!empty($profile['cid']) && self::getMyURL()) {
$profile_contact = Contact::selectFirst(['rel'], ['id' => $profile['cid']]);
}
}
if (empty($profile['nickname'])) {
Logger::warning('Received profile with no nickname', ['profile' => $profile, 'callstack' => System::callstack(10)]);
@ -292,16 +304,12 @@ class Profile
$subscribe_feed_link = null;
$wallmessage_link = null;
// Who is the logged-in user to this profile?
$visitor_contact = [];
if (!empty($profile['uid']) && self::getMyURL()) {
$visitor_contact = Contact::selectFirst(['rel'], ['uid' => $profile['uid'], 'nurl' => Strings::normaliseLink(self::getMyURL())]);
}
$profile_contact = [];
if (!empty($profile['cid']) && self::getMyURL()) {
$profile_contact = Contact::selectFirst(['rel'], ['id' => $profile['cid']]);
}
$profile_is_dfrn = $profile['network'] == Protocol::DFRN;
$profile_is_native = in_array($profile['network'], Protocol::NATIVE_SUPPORT);
$local_user_is_self = self::getMyURL() && ($profile['url'] == self::getMyURL());
@ -332,17 +340,19 @@ class Profile
$subscribe_feed_link = 'dfrn_poll/' . $profile['nickname'];
}
if (Contact::canReceivePrivateMessages($profile)) {
if (Contact::canReceivePrivateMessages($profile_contact)) {
if ($visitor_is_followed || $visitor_is_following) {
$wallmessage_link = $visitor_base_path . '/message/new/' . base64_encode($profile['addr'] ?? '');
$wallmessage_link = $visitor_base_path . '/message/new/' . $profile_contact['id'];
} elseif ($visitor_is_authenticated && !empty($profile['unkmail'])) {
$wallmessage_link = 'wallmessage/' . $profile['nickname'];
}
}
}
// show edit profile to yourself
if (!$is_contact && $local_user_is_self) {
// show edit profile to yourself, but only if this is not meant to be
// rendered as a "contact". i.e., if 'self' (a "contact" table column) isn't
// set in $profile.
if (!isset($profile['self']) && $local_user_is_self) {
$profile['edit'] = [DI::baseUrl() . '/settings/profile', DI::l10n()->t('Edit profile'), '', DI::l10n()->t('Edit profile')];
$profile['menu'] = [
'chg_photo' => DI::l10n()->t('Change profile photo'),

View file

@ -312,8 +312,8 @@ class User
*/
public static function getIdForURL(string $url)
{
// Avoid any database requests when the hostname isn't even part of the url.
if (!strpos($url, DI::baseUrl()->getHostname())) {
// Avoid database queries when the local node hostname isn't even part of the url.
if (!Contact::isLocal($url)) {
return 0;
}
@ -1123,6 +1123,8 @@ class User
Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
}
}
Contact::updateSelfFromUserID($uid, true);
}
Hook::callAll('register_account', $uid);

View file

@ -21,8 +21,9 @@
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\Logger;
use Friendica\Module\BaseApi;
use Friendica\Util\Network;
use Friendica\Util\HTTPInputData;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
@ -34,9 +35,10 @@ class UpdateCredentials extends BaseApi
self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID();
$data = Network::postdata();
$data = HTTPInputData::process();
Logger::info('Patch data', ['data' => $data]);
// @todo Parse the raw data that is in the "multipart/form-data" format
self::unsupported('patch');
}
}

View file

@ -50,7 +50,7 @@ class Apps extends BaseApi
if (!empty($postdata)) {
$postrequest = json_decode($postdata, true);
if (!empty($postrequest) && is_array($postrequest)) {
$request = array_merge($request, $$postrequest);
$request = array_merge($request, $postrequest);
}
}

View file

@ -52,7 +52,7 @@ class Bookmarks extends BaseApi
$params = ['order' => ['uri-id' => true], 'limit' => $request['limit']];
$condition = ['pinned' => true, 'uid' => $uid];
$condition = ['starred' => true, 'uid' => $uid];
if (!empty($request['max_id'])) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $request['max_id']]);

View file

@ -78,13 +78,16 @@ class Lists extends BaseApi
public static function put(array $parameters = [])
{
$data = self::getPutData();
$request = self::getRequest([
'title' => '', // The title of the list to be updated.
'replies_policy' => '', // One of: "followed", "list", or "none".
]);
if (empty($data['title']) || empty($parameters['id'])) {
if (empty($request['title']) || empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
Group::update($parameters['id'], $data['title']);
Group::update($parameters['id'], $request['title']);
}
/**

View file

@ -58,7 +58,12 @@ class Media extends BaseApi
self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID();
$data = self::getPutData();
$request = self::getRequest([
'file' => [], // The file to be attached, using multipart form data.
'thumbnail' => [], // The custom thumbnail of the media to be attached, using multipart form data.
'description' => '', // A plain-text description of the media, for accessibility purposes.
'focus' => '', // Two floating points (x,y), comma-delimited ranging from -1.0 to 1.0
]);
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
@ -69,7 +74,7 @@ class Media extends BaseApi
DI::mstdnError()->RecordNotFound();
}
Photo::update(['desc' => $data['description'] ?? ''], ['resource-id' => $photo['resource-id']]);
Photo::update(['desc' => $request['description']], ['resource-id' => $photo['resource-id']]);
System::jsonExit(DI::mstdnAttachment()->createFromPhoto($parameters['id']));
}

View file

@ -25,8 +25,10 @@ use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Notification;
use Friendica\Model\Post;
use Friendica\Model\Verb;
use Friendica\Module\BaseApi;
use Friendica\Protocol\Activity;
/**
* @see https://docs.joinmastodon.org/methods/notifications/
@ -44,10 +46,10 @@ class Notifications extends BaseApi
if (!empty($parameters['id'])) {
$id = $parameters['id'];
if (!DBA::exists('notify', ['id' => $id, 'uid' => $uid])) {
if (!DBA::exists('notification', ['id' => $id, 'uid' => $uid])) {
DI::mstdnError()->RecordNotFound();
}
System::jsonExit(DI::mstdnNotification()->createFromNotifyId($id));
System::jsonExit(DI::mstdnNotification()->createFromNotificationId($id));
}
$request = self::getRequest([
@ -63,7 +65,7 @@ class Notifications extends BaseApi
$params = ['order' => ['id' => true], 'limit' => $request['limit']];
$condition = ['uid' => $uid, 'seen' => false, 'type' => []];
$condition = ['uid' => $uid, 'seen' => false];
if (!empty($request['account_id'])) {
$contact = Contact::getById($request['account_id'], ['url']);
@ -72,17 +74,40 @@ class Notifications extends BaseApi
}
}
if (!in_array('follow_request', $request['exclude_types'])) {
$condition['type'] = array_merge($condition['type'], [Notification\Type::INTRO]);
if (in_array('follow_request', $request['exclude_types'])) {
$condition = DBA::mergeConditions($condition,
["(`vid` != ? OR `type` != ? OR NOT EXISTS (SELECT `id` FROM `contact` WHERE `id` = `actor-id` AND `pending`))",
Verb::getID(Activity::FOLLOW), Post\UserNotification::NOTIF_NONE]);
}
if (!in_array('mention', $request['exclude_types'])) {
$condition['type'] = array_merge($condition['type'],
[Notification\Type::WALL, Notification\Type::COMMENT, Notification\Type::MAIL, Notification\Type::TAG_SELF, Notification\Type::POKE]);
if (in_array('follow', $request['exclude_types'])) {
$condition = DBA::mergeConditions($condition,
["(`vid` != ? OR `type` != ? OR NOT EXISTS (SELECT `id` FROM `contact` WHERE `id` = `actor-id` AND NOT `pending`))",
Verb::getID(Activity::FOLLOW), Post\UserNotification::NOTIF_NONE]);
}
if (!in_array('status', $request['exclude_types'])) {
$condition['type'] = array_merge($condition['type'], [Notification\Type::SHARE]);
if (in_array('favourite', $request['exclude_types'])) {
$condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?, ?) OR NOT `type` IN (?, ?))",
Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE),
Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT]);
}
if (in_array('reblog', $request['exclude_types'])) {
$condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?) OR NOT `type` IN (?, ?))",
Verb::getID(Activity::ANNOUNCE),
Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT]);
}
if (in_array('mention', $request['exclude_types'])) {
$condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?) OR NOT `type` IN (?, ?, ?, ?, ?))",
Verb::getID(Activity::POST), Post\UserNotification::NOTIF_EXPLICIT_TAGGED,
Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT,
Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT]);
}
if (in_array('status', $request['exclude_types'])) {
$condition = DBA::mergeConditions($condition, ["(NOT `vid` IN (?) OR NOT `type` IN (?))",
Verb::getID(Activity::POST), Post\UserNotification::NOTIF_SHARED]);
}
if (!empty($request['max_id'])) {
@ -101,9 +126,12 @@ class Notifications extends BaseApi
$notifications = [];
$notify = DBA::select('notify', ['id'], $condition, $params);
$notify = DBA::select('notification', ['id'], $condition, $params);
while ($notification = DBA::fetch($notify)) {
$notifications[] = DI::mstdnNotification()->createFromNotifyId($notification['id']);
$entry = DI::mstdnNotification()->createFromNotificationId($notification['id']);
if (!empty($entry)) {
$notifications[] = $entry;
}
}
if (!empty($request['min_id'])) {

View file

@ -35,7 +35,7 @@ class Clear extends BaseApi
self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID();
DBA::update('notify', ['seen' => true], ['uid' => $uid]);
DBA::update('notification', ['seen' => true], ['uid' => $uid]);
System::jsonExit([]);
}

View file

@ -40,7 +40,7 @@ class Dismiss extends BaseApi
DI::mstdnError()->UnprocessableEntity();
}
DBA::update('notify', ['seen' => true], ['uid' => $uid, 'id' => $parameters['id']]);
DBA::update('notification', ['seen' => true], ['uid' => $uid, 'id' => $parameters['id']]);
System::jsonExit([]);
}

View file

@ -46,21 +46,22 @@ class Statuses extends BaseApi
self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID();
$data = self::getJsonPostData();
$status = $data['status'] ?? '';
$media_ids = $data['media_ids'] ?? [];
$in_reply_to_id = $data['in_reply_to_id'] ?? 0;
$sensitive = $data['sensitive'] ?? false; // @todo Possibly trigger "nsfw" flag?
$spoiler_text = $data['spoiler_text'] ?? '';
$visibility = $data['visibility'] ?? '';
$scheduled_at = $data['scheduled_at'] ?? ''; // Currently unsupported, but maybe in the future
$language = $data['language'] ?? '';
$request = self::getRequest([
'status' => '', // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.
'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used.
'poll' => [], // Poll data. If provided, media_ids cannot be used, and poll[expires_in] must be provided.
'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply
'sensitive' => false, // Mark status and attached media as sensitive?
'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.
'visibility' => '', // Visibility of the posted status. One of: "public", "unlisted", "private" or "direct".
'scheduled_at' => '', // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future.
'language' => '', // ISO 639 language code for this status.
]);
$owner = User::getOwnerDataById($uid);
// The imput is defined as text. So we can use Markdown for some enhancements
$body = Markdown::toBBCode($status);
$body = Markdown::toBBCode($request['status']);
$body = BBCode::expandTags($body);
@ -69,7 +70,7 @@ class Statuses extends BaseApi
$item['verb'] = Activity::POST;
$item['contact-id'] = $owner['id'];
$item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid);
$item['title'] = $spoiler_text;
$item['title'] = $request['spoiler_text'];
$item['body'] = $body;
if (!empty(self::getCurrentApplication()['name'])) {
@ -80,7 +81,7 @@ class Statuses extends BaseApi
$item['app'] = 'API';
}
switch ($visibility) {
switch ($request['visibility']) {
case 'public':
$item['allow_cid'] = '';
$item['allow_gid'] = '';
@ -129,12 +130,12 @@ class Statuses extends BaseApi
break;
}
if (!empty($language)) {
$item['language'] = json_encode([$language => 1]);
if (!empty($request['language'])) {
$item['language'] = json_encode([$request['language'] => 1]);
}
if ($in_reply_to_id) {
$parent = Post::selectFirst(['uri'], ['uri-id' => $in_reply_to_id, 'uid' => [0, $uid]]);
if ($request['in_reply_to_id']) {
$parent = Post::selectFirst(['uri'], ['uri-id' => $request['in_reply_to_id'], 'uid' => [0, $uid]]);
$item['thr-parent'] = $parent['uri'];
$item['gravity'] = GRAVITY_COMMENT;
$item['object-type'] = Activity\ObjectType::COMMENT;
@ -143,12 +144,12 @@ class Statuses extends BaseApi
$item['object-type'] = Activity\ObjectType::NOTE;
}
if (!empty($media_ids)) {
if (!empty($request['media_ids'])) {
$item['object-type'] = Activity\ObjectType::IMAGE;
$item['post-type'] = Item::PT_IMAGE;
$item['attachments'] = [];
foreach ($media_ids as $id) {
foreach ($request['media_ids'] as $id) {
$media = DBA::toArray(DBA::p("SELECT `resource-id`, `scale`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo`
WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = ?) AND `photo`.`uid` = ?
ORDER BY `photo`.`width` DESC LIMIT 2", $id, $uid));

View file

@ -44,6 +44,10 @@ class Context extends BaseApi
DI::mstdnError()->UnprocessableEntity();
}
$request = self::getRequest([
'limit' => 40, // Maximum number of results to return. Defaults to 40.
]);
$id = $parameters['id'];
$parent = Post::selectFirst(['parent-uri-id'], ['uri-id' => $id]);
@ -68,24 +72,20 @@ class Context extends BaseApi
$statuses = ['ancestors' => [], 'descendants' => []];
$ancestors = [];
foreach (self::getParents($id, $parents) as $ancestor) {
$ancestors[$ancestor] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
$ancestors = self::getParents($id, $parents);
asort($ancestors);
foreach (array_slice($ancestors, 0, $request['limit']) as $ancestor) {
$statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);;
}
ksort($ancestors);
foreach ($ancestors as $ancestor) {
$statuses['ancestors'][] = $ancestor;
}
$descendants = self::getChildren($id, $children);
$descendants = [];
foreach (self::getChildren($id, $children) as $descendant) {
$descendants[] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
}
asort($descendants);
ksort($descendants);
foreach ($descendants as $descendant) {
$statuses['descendants'][] = $descendant;
foreach (array_slice($descendants, 0, $request['limit']) as $descendant) {
$statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
}
System::jsonExit($statuses);

View file

@ -41,6 +41,8 @@ class PublicTimeline extends BaseApi
*/
public static function rawContent(array $parameters = [])
{
$uid = self::getCurrentUserID();
$request = self::getRequest([
'local' => false, // Show only local statuses? Defaults to false.
'remote' => false, // Show only remote statuses? Defaults to false.
@ -56,7 +58,7 @@ class PublicTimeline extends BaseApi
$params = ['order' => ['uri-id' => true], 'limit' => $request['limit']];
$condition = ['gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => Item::PUBLIC,
'uid' => 0, 'network' => Protocol::FEDERATED];
'uid' => 0, 'network' => Protocol::FEDERATED, 'parent-author-blocked' => false, 'parent-author-hidden' => false];
if ($request['local']) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]);
@ -88,7 +90,12 @@ class PublicTimeline extends BaseApi
$condition = DBA::mergeConditions($condition, ['gravity' => GRAVITY_PARENT]);
}
$items = Post::selectForUser(0, ['uri-id', 'uid'], $condition, $params);
if (!empty($uid)) {
$condition = DBA::mergeConditions($condition,
["NOT EXISTS (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `parent-author-id` AND (`blocked` OR `ignored`))", $uid]);
}
$items = Post::selectForUser($uid, ['uri-id', 'uid'], $condition, $params);
$statuses = [];
while ($item = Post::fetch($items)) {

View file

@ -29,7 +29,7 @@ use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Network\HTTPException;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
use Friendica\Util\HTTPInputData;
require_once __DIR__ . '/../../include/api.php';
@ -129,7 +129,7 @@ class BaseApi extends BaseModule
public static function unsupported(string $method = 'all')
{
$path = DI::args()->getQueryString();
Logger::info('Unimplemented API call', ['method' => $method, 'path' => $path, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'request' => $_REQUEST ?? []]);
Logger::info('Unimplemented API call', ['method' => $method, 'path' => $path, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'request' => HTTPInputData::process()]);
$error = DI::l10n()->t('API endpoint %s %s is not implemented', strtoupper($method), $path);
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');
$errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
@ -141,26 +141,30 @@ class BaseApi extends BaseModule
*
* @return array request data
*/
public static function getRequest(array $defaults) {
public static function getRequest(array $defaults)
{
$httpinput = HTTPInputData::process();
$input = array_merge($httpinput['variables'], $httpinput['files'], $_REQUEST);
$request = [];
foreach ($defaults as $parameter => $defaultvalue) {
if (is_string($defaultvalue)) {
$request[$parameter] = $_REQUEST[$parameter] ?? $defaultvalue;
$request[$parameter] = $input[$parameter] ?? $defaultvalue;
} elseif (is_int($defaultvalue)) {
$request[$parameter] = (int)($_REQUEST[$parameter] ?? $defaultvalue);
$request[$parameter] = (int)($input[$parameter] ?? $defaultvalue);
} elseif (is_float($defaultvalue)) {
$request[$parameter] = (float)($_REQUEST[$parameter] ?? $defaultvalue);
$request[$parameter] = (float)($input[$parameter] ?? $defaultvalue);
} elseif (is_array($defaultvalue)) {
$request[$parameter] = $_REQUEST[$parameter] ?? [];
$request[$parameter] = $input[$parameter] ?? [];
} elseif (is_bool($defaultvalue)) {
$request[$parameter] = in_array(strtolower($_REQUEST[$parameter] ?? ''), ['true', '1']);
$request[$parameter] = in_array(strtolower($input[$parameter] ?? ''), ['true', '1']);
} else {
Logger::notice('Unhandled default value type', ['parameter' => $parameter, 'type' => gettype($defaultvalue)]);
}
}
foreach ($_REQUEST ?? [] as $parameter => $value) {
foreach ($input ?? [] as $parameter => $value) {
if ($parameter == 'pagename') {
continue;
}
@ -173,45 +177,6 @@ class BaseApi extends BaseModule
return $request;
}
/**
* Get post data that is transmitted as JSON
*
* @return array request data
*/
public static function getJsonPostData()
{
$postdata = Network::postdata();
if (empty($postdata)) {
return [];
}
return json_decode($postdata, true);
}
/**
* Get request data for put requests
*
* @return array request data
*/
public static function getPutData()
{
$rawdata = Network::postdata();
if (empty($rawdata)) {
return [];
}
$putdata = [];
foreach (explode('&', $rawdata) as $value) {
$data = explode('=', $value);
if (count($data) == 2) {
$putdata[$data[0]] = urldecode($data[1]);
}
}
return $putdata;
}
/**
* Log in user via OAuth1 or Simple HTTP Auth.
*

View file

@ -31,7 +31,6 @@ use Friendica\Core\Session;
use Friendica\Database\DBA;
use Friendica\Model\Post;
use Friendica\Network\HTTPException;
use Friendica\Util\Strings;
/**
* Performs an activity (like, dislike, announce, attendyes, attendno, attendmaybe)
@ -90,7 +89,7 @@ class Activity extends BaseModule
{
$fields = ['uri-id', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink'];
$item = Post::selectFirst($fields, ['id' => $itemId, 'private' => [Item::PUBLIC, Item::UNLISTED]]);
if (!DBA::isResult($item) || ($item['body'] == '')) {
if (!DBA::isResult($item)) {
return;
}

View file

@ -50,20 +50,20 @@ class NoScrape extends BaseModule
System::jsonError(403, 'Authentication required');
}
Profile::load($a, $which);
$profile = Profile::getByNickname($which, local_user());
if (empty($a->profile['uid'])) {
if (empty($profile['uid'])) {
System::jsonError(404, 'Profile not found');
}
$json_info = [
'addr' => $a->profile['addr'],
'addr' => $profile['addr'],
'nick' => $which,
'guid' => $a->profile['guid'],
'key' => $a->profile['upubkey'],
'guid' => $profile['guid'],
'key' => $profile['upubkey'],
'homepage' => DI::baseUrl() . "/profile/{$which}",
'comm' => ($a->profile['account-type'] == User::ACCOUNT_TYPE_COMMUNITY),
'account-type' => $a->profile['account-type'],
'comm' => ($profile['account-type'] == User::ACCOUNT_TYPE_COMMUNITY),
'account-type' => $profile['account-type'],
];
$dfrn_pages = ['request', 'confirm', 'notify', 'poll'];
@ -71,30 +71,30 @@ class NoScrape extends BaseModule
$json_info["dfrn-{$dfrn}"] = DI::baseUrl() . "/dfrn_{$dfrn}/{$which}";
}
if (!$a->profile['net-publish']) {
if (!$profile['net-publish']) {
$json_info['hide'] = true;
System::jsonExit($json_info);
}
$keywords = $a->profile['pub_keywords'] ?? '';
$keywords = $profile['pub_keywords'] ?? '';
$keywords = str_replace(['#', ',', ' ', ',,'], ['', ' ', ',', ','], $keywords);
$keywords = explode(',', $keywords);
$contactPhoto = DBA::selectFirst('contact', ['photo'], ['self' => true, 'uid' => $a->profile['uid']]);
$contactPhoto = DBA::selectFirst('contact', ['photo'], ['self' => true, 'uid' => $profile['uid']]);
$json_info['fn'] = $a->profile['name'];
$json_info['fn'] = $profile['name'];
$json_info['photo'] = $contactPhoto["photo"];
$json_info['tags'] = $keywords;
$json_info['language'] = $a->profile['language'];
$json_info['language'] = $profile['language'];
if (!empty($a->profile['last-item'])) {
$json_info['updated'] = date("c", strtotime($a->profile['last-item']));
if (!empty($profile['last-item'])) {
$json_info['updated'] = date("c", strtotime($profile['last-item']));
}
if (!($a->profile['hide-friends'] ?? false)) {
if (!($profile['hide-friends'] ?? false)) {
$json_info['contacts'] = DBA::count('contact',
[
'uid' => $a->profile['uid'],
'uid' => $profile['uid'],
'self' => 0,
'blocked' => 0,
'pending' => 0,
@ -106,13 +106,13 @@ class NoScrape extends BaseModule
// We display the last activity (post or login), reduced to year and week number
$last_active = 0;
$condition = ['uid' => $a->profile['uid'], 'self' => true];
$condition = ['uid' => $profile['uid'], 'self' => true];
$contact = DBA::selectFirst('contact', ['last-item'], $condition);
if (DBA::isResult($contact)) {
$last_active = strtotime($contact['last-item']);
}
$condition = ['uid' => $a->profile['uid']];
$condition = ['uid' => $profile['uid']];
$user = DBA::selectFirst('user', ['login_date'], $condition);
if (DBA::isResult($user)) {
if ($last_active < strtotime($user['login_date'])) {
@ -124,8 +124,8 @@ class NoScrape extends BaseModule
//These are optional fields.
$profile_fields = ['about', 'locality', 'region', 'postal-code', 'country-name'];
foreach ($profile_fields as $field) {
if (!empty($a->profile[$field])) {
$json_info["$field"] = $a->profile[$field];
if (!empty($profile[$field])) {
$json_info["$field"] = $profile[$field];
}
}

View file

@ -63,9 +63,9 @@ class Authorize extends BaseApi
// @todo Compare the application scope and requested scope
$request = $_REQUEST;
unset($request['pagename']);
$redirect = 'oauth/authorize?' . http_build_query($request);
$redirect_request = $_REQUEST;
unset($redirect_request['pagename']);
$redirect = 'oauth/authorize?' . http_build_query($redirect_request);
$uid = local_user();
if (empty($uid)) {

View file

@ -170,8 +170,10 @@ class Index extends BaseSearch
}
if (!empty($uriids)) {
$params = ['order' => ['id' => true], 'group_by' => ['uri-id']];
$items = Post::toArray(Post::selectForUser(local_user(), Item::DISPLAY_FIELDLIST, ['uri-id' => $uriids], $params));
$condition = ["(`uid` = ? OR (`uid` = ? AND NOT `global`))", 0, local_user()];
$condition = DBA::mergeConditions($condition, ['uri-id' => $uriids]);
$params = ['order' => ['id' => true]];
$items = Post::toArray(Post::selectForUser(local_user(), Item::DISPLAY_FIELDLIST, $condition, $params));
}
if (empty($items)) {

View file

@ -70,7 +70,7 @@ class HTTPRequest implements IHTTPRequest
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function get(string $url, array $opts = [], int &$redirects = 0)
public function get(string $url, array $opts = [], &$redirects = 0)
{
$stamp1 = microtime(true);
@ -222,7 +222,7 @@ class HTTPRequest implements IHTTPRequest
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function post(string $url, $params, array $headers = [], int $timeout = 0, int &$redirects = 0)
public function post(string $url, $params, array $headers = [], int $timeout = 0, &$redirects = 0)
{
$stamp1 = microtime(true);
@ -447,7 +447,7 @@ class HTTPRequest implements IHTTPRequest
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '', int &$redirects = 0)
public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '', &$redirects = 0)
{
$ret = $this->fetchFull($url, $timeout, $accept_content, $cookiejar, $redirects);
@ -461,7 +461,7 @@ class HTTPRequest implements IHTTPRequest
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '', int &$redirects = 0)
public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '', &$redirects = 0)
{
return $this->get(
$url,

View file

@ -108,7 +108,7 @@ class Account extends BaseDataTransferObject
$userContactCreated = $userContact['created'] ?? DBA::NULL_DATETIME;
$created = $userContactCreated < $publicContactCreated && ($userContactCreated != DBA::NULL_DATETIME) ? $userContactCreated : $publicContactCreated;
$this->created_at = DateTimeFormat::utc($created, DateTimeFormat::ATOM);
$this->created_at = DateTimeFormat::utc($created, DateTimeFormat::JSON);
$this->note = BBCode::convert($publicContact['about'], false);
$this->url = $publicContact['url'];

View file

@ -52,7 +52,7 @@ class Notification extends BaseDataTransferObject
{
$this->id = (string)$id;
$this->type = $type;
$this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::ATOM);
$this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::JSON);
$this->account = $account->toArray();
if (!empty($status)) {

View file

@ -100,7 +100,7 @@ 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 $reblog)
{
$this->id = (string)$item['uri-id'];
$this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM);
$this->created_at = DateTimeFormat::utc($item['created'], DateTimeFormat::JSON);
if ($item['gravity'] == GRAVITY_COMMENT) {
$this->in_reply_to_id = (string)$item['thr-parent-id'];
@ -114,7 +114,12 @@ class Status extends BaseDataTransferObject
$this->visibility = $visibility[$item['private']];
$languages = json_decode($item['language'], true);
$this->language = is_array($languages) ? array_key_first($languages) : null;
if (is_array($languages)) {
reset($languages);
$this->language = key($languages);
} else {
$this->language = null;
}
$this->uri = $item['uri'];
$this->url = $item['plink'] ?? null;

View file

@ -53,6 +53,6 @@ class Token extends BaseDataTransferObject
$this->access_token = $access_token;
$this->token_type = $token_type;
$this->scope = $scope;
$this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::ATOM);
$this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::JSON);
}
}

View file

@ -401,9 +401,13 @@ class Post
}
// Fetching of Diaspora posts doesn't always work. There are issues with reshares and possibly comments
if (($item['network'] != Protocol::DIASPORA) && empty($comment) && !empty(Session::get('remote_comment'))) {
if (!local_user() && ($item['network'] != Protocol::DIASPORA) && !empty(Session::get('remote_comment'))) {
$remote_comment = [DI::l10n()->t('Comment this item on your system'), DI::l10n()->t('Remote comment'),
str_replace('{uri}', urlencode($item['uri']), Session::get('remote_comment'))];
// Ensure to either display the remote comment or the local activities
$buttons = [];
$comment_html = '';
} else {
$remote_comment = '';
}

View file

@ -602,6 +602,12 @@ class Processor
continue;
}
if (!($item['isForum'] ?? false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT) &&
($item['post-reason'] == Item::PR_BCC) && !Contact::isSharingByURL($activity['author'], $receiver)) {
Logger::info('Top level post via BCC from a non sharer, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id']]);
continue;
}
if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
$skip = !Contact::isSharingByURL($activity['author'], $receiver);

View file

@ -748,10 +748,6 @@ class Transmitter
$contacts = DBA::select('contact', ['id', 'url', 'network', 'protocol', 'gsid'], $condition);
while ($contact = DBA::fetch($contacts)) {
if (Contact::isLocal($contact['url'])) {
continue;
}
if (!self::isAPContact($contact, $networks)) {
continue;
}
@ -766,7 +762,7 @@ class Transmitter
$profile = APContact::getByURL($contact['url'], false);
if (!empty($profile)) {
if (empty($profile['sharedinbox']) || $personal) {
if (empty($profile['sharedinbox']) || $personal || Contact::isLocal($contact['url'])) {
$target = $profile['inbox'];
} else {
$target = $profile['sharedinbox'];
@ -829,15 +825,11 @@ class Transmitter
if ($item_profile && ($receiver == $item_profile['followers']) && ($uid == $profile_uid)) {
$inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal, self::isAPPost($last_id)));
} else {
if (Contact::isLocal($receiver)) {
continue;
}
$profile = APContact::getByURL($receiver, false);
if (!empty($profile)) {
$contact = Contact::getByURLForUser($receiver, $uid, false, ['id']);
if (empty($profile['sharedinbox']) || $personal || $blindcopy) {
if (empty($profile['sharedinbox']) || $personal || $blindcopy || Contact::isLocal($receiver)) {
$target = $profile['inbox'];
} else {
$target = $profile['sharedinbox'];
@ -1525,12 +1517,21 @@ class Transmitter
if ($type == 'Note') {
$body = $item['raw-body'] ?? self::removePictures($body);
} elseif (($type == 'Article') && empty($data['summary'])) {
$regexp = "/[@!]\[url\=([^\[\]]*)\].*?\[\/url\]/ism";
$summary = preg_replace_callback($regexp, ['self', 'mentionAddrCallback'], $body);
$data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($summary), 1000));
}
/**
* @todo Improve the automated summary
* This part is currently deactivated. The automated summary seems to be more
* confusing than helping. But possibly we will find a better way.
* So the code is left here for now as a reminder
*
* } elseif (($type == 'Article') && empty($data['summary'])) {
* $regexp = "/[@!]\[url\=([^\[\]]*)\].*?\[\/url\]/ism";
* $summary = preg_replace_callback($regexp, ['self', 'mentionAddrCallback'], $body);
* $data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($summary), 1000));
* }
*/
if (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) {
$body = self::prependMentions($body, $item['uri-id'], $item['author-link']);
}

View file

@ -4049,13 +4049,11 @@ class Diaspora
return false;
}
$parent = Post::selectFirst(['parent-uri'], ['uri' => $item['thr-parent']]);
if (!DBA::isResult($parent)) {
return;
// This is only needed for the automated tests
if (empty($owner['uprvkey'])) {
return false;
}
$item['parent-uri'] = $parent['parent-uri'];
$message = self::constructComment($item, $owner);
if ($message === false) {
return false;

View file

@ -87,8 +87,13 @@ class Notification extends BaseRepository
public function setSeen(bool $seen = true, Model\Notification $notify = null)
{
if (empty($notify)) {
$this->dba->update('notification', ['seen' => $seen], ['uid' => local_user()]);
$conditions = ['uid' => local_user()];
} else {
if (!empty($notify->{'uri-id'})) {
$this->dba->update('notification', ['seen' => $seen], ['uid' => local_user(), 'target-uri-id' => $notify->{'uri-id'}]);
}
$conditions = ['(`link` = ? OR (`parent` != 0 AND `parent` = ? AND `otype` = ?)) AND `uid` = ?',
$notify->link,
$notify->parent,

View file

@ -34,6 +34,7 @@ class DateTimeFormat
const ATOM = 'Y-m-d\TH:i:s\Z';
const MYSQL = 'Y-m-d H:i:s';
const HTTP = 'D, d M Y H:i:s \G\M\T';
const JSON = 'Y-m-d\TH:i:s.v\Z';
/**
* convert() shorthand for UTC.

295
src/Util/HTTPInputData.php Normal file
View file

@ -0,0 +1,295 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Util;
/**
* Derived from the work of Reid Johnson <https://codereview.stackexchange.com/users/4020/reid-johnson>
* @see https://codereview.stackexchange.com/questions/69882/parsing-multipart-form-data-in-php-for-put-requests
*/
class HTTPInputData
{
public static function process()
{
$content_parts = explode(';', static::getContentType());
$boundary = '';
$encoding = '';
$content_type = array_shift($content_parts);
foreach ($content_parts as $part) {
if (strpos($part, 'boundary') !== false) {
$part = explode('=', $part, 2);
if (!empty($part[1])) {
$boundary = '--' . $part[1];
}
} elseif (strpos($part, 'charset') !== false) {
$part = explode('=', $part, 2);
if (!empty($part[1])) {
$encoding = $part[1];
}
}
if ($boundary !== '' && $encoding !== '') {
break;
}
}
if ($content_type == 'multipart/form-data') {
return self::fetchFromMultipart($boundary);
}
// can be handled by built in PHP functionality
$content = static::getPhpInputContent();
$variables = json_decode($content, true);
if (empty($variables)) {
parse_str($content, $variables);
}
return ['variables' => $variables, 'files' => []];
}
private static function fetchFromMultipart(string $boundary)
{
$result = ['variables' => [], 'files' => []];
$stream = static::getPhpInputStream();
$sanity = fgets($stream, strlen($boundary) + 5);
// malformed file, boundary should be first item
if (rtrim($sanity) !== $boundary) {
return $result;
}
$raw_headers = '';
while (($chunk = fgets($stream)) !== false) {
if ($chunk === $boundary) {
continue;
}
if (!empty(trim($chunk))) {
$raw_headers .= $chunk;
continue;
}
$result = self::parseRawHeader($stream, $raw_headers, $boundary, $result);
$raw_headers = '';
}
fclose($stream);
return $result;
}
private static function parseRawHeader($stream, string $raw_headers, string $boundary, array $result)
{
$variables = $result['variables'];
$files = $result['files'];
$headers = [];
foreach (explode("\r\n", $raw_headers) as $header) {
if (strpos($header, ':') === false) {
continue;
}
list($name, $value) = explode(':', $header, 2);
$headers[strtolower($name)] = ltrim($value, ' ');
}
if (!isset($headers['content-disposition'])) {
return ['variables' => $variables, 'files' => $files];
}
if (!preg_match('/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches)) {
return ['variables' => $variables, 'files' => $files];
}
$name = $matches[2];
$filename = $matches[4] ?? '';
if (!empty($filename)) {
$files[$name] = static::fetchFileData($stream, $boundary, $headers, $filename);
return ['variables' => $variables, 'files' => $files];
} else {
$variables = self::fetchVariables($stream, $boundary, $headers, $name, $variables);
}
return ['variables' => $variables, 'files' => $files];
}
protected static function fetchFileData($stream, string $boundary, array $headers, string $filename)
{
$error = UPLOAD_ERR_OK;
if (isset($headers['content-type'])) {
$tmp = explode(';', $headers['content-type']);
$contentType = $tmp[0];
} else {
$contentType = 'unknown';
}
$tmpnam = tempnam(ini_get('upload_tmp_dir'), 'php');
$fileHandle = fopen($tmpnam, 'wb');
if ($fileHandle === false) {
$error = UPLOAD_ERR_CANT_WRITE;
} else {
$lastLine = null;
while (($chunk = fgets($stream, 8096)) !== false && strpos($chunk, $boundary) !== 0) {
if ($lastLine !== null) {
if (!fwrite($fileHandle, $lastLine)) {
$error = UPLOAD_ERR_CANT_WRITE;
break;
}
}
$lastLine = $chunk;
}
if ($lastLine !== null && $error !== UPLOAD_ERR_CANT_WRITE) {
if (!fwrite($fileHandle, rtrim($lastLine, "\r\n"))) {
$error = UPLOAD_ERR_CANT_WRITE;
}
}
}
return [
'name' => $filename,
'type' => $contentType,
'tmp_name' => $tmpnam,
'error' => $error,
'size' => filesize($tmpnam)
];
}
private static function fetchVariables($stream, string $boundary, array $headers, string $name, array $variables)
{
$fullValue = '';
$lastLine = null;
while (($chunk = fgets($stream)) !== false && strpos($chunk, $boundary) !== 0) {
if ($lastLine !== null) {
$fullValue .= $lastLine;
}
$lastLine = $chunk;
}
if ($lastLine !== null) {
$fullValue .= rtrim($lastLine, "\r\n");
}
if (isset($headers['content-type'])) {
$encoding = '';
foreach (explode(';', $headers['content-type']) as $part) {
if (strpos($part, 'charset') !== false) {
$part = explode($part, '=', 2);
if (isset($part[1])) {
$encoding = $part[1];
}
break;
}
}
if ($encoding !== '' && strtoupper($encoding) !== 'UTF-8' && strtoupper($encoding) !== 'UTF8') {
$tmp = mb_convert_encoding($fullValue, 'UTF-8', $encoding);
if ($tmp !== false) {
$fullValue = $tmp;
}
}
}
$fullValue = $name . '=' . $fullValue;
$tmp = [];
parse_str($fullValue, $tmp);
return self::expandVariables(explode('[', $name), $variables, $tmp);
}
private static function expandVariables(array $names, $variables, array $values)
{
if (!is_array($variables)) {
return $values;
}
$name = rtrim(array_shift($names), ']');
if ($name !== '') {
$name = $name . '=p';
$tmp = [];
parse_str($name, $tmp);
$tmp = array_keys($tmp);
$name = reset($tmp);
}
if ($name === '') {
$variables[] = reset($values);
} elseif (isset($variables[$name]) && isset($values[$name])) {
$variables[$name] = self::expandVariables($names, $variables[$name], $values[$name]);
} elseif (isset($values[$name])) {
$variables[$name] = $values[$name];
}
return $variables;
}
/**
* Returns the current PHP input stream
* Mainly used for test doubling
*
* @return false|resource
*/
protected static function getPhpInputStream()
{
return fopen('php://input', 'rb');
}
/**
* Returns the content of the current PHP input
* Mainly used for test doubling
*
* @return false|string
*/
protected static function getPhpInputContent()
{
return file_get_contents('php://input');
}
/**
* Returns the content type string of the current call
* Mainly used for test doubling
*
* @return false|string
*/
protected static function getContentType()
{
return $_SERVER['CONTENT_TYPE'] ?? 'application/x-www-form-urlencoded';
}
}

View file

@ -640,16 +640,17 @@ class HTTPSignature
$profile = APContact::getByURL($url);
if (!empty($profile)) {
Logger::log('Taking key from id ' . $id, Logger::DEBUG);
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::log('Taking key from actor ' . $actor, Logger::DEBUG);
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]);
return false;
}
}

View file

@ -548,4 +548,15 @@ class Network
exit;
}
}
/**
* Check if the given URL is a local link
*
* @param string $url
* @return bool
*/
public static function isLocalLink(string $url)
{
return (strpos(Strings::normaliseLink($url), Strings::normaliseLink(DI::baseUrl())) !== false);
}
}

View file

@ -30,13 +30,11 @@ use Friendica\Protocol\DFRN;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\Email;
use Friendica\Protocol\Activity;
use Friendica\Util\Strings;
use Friendica\Util\Network;
use Friendica\Core\Worker;
use Friendica\Model\Conversation;
use Friendica\Model\FContact;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Protocol\Relay;
class Delivery
@ -216,11 +214,6 @@ class Delivery
$contact['network'] = Protocol::DIASPORA;
}
// Ensure that local contacts are delivered locally
if (Model\Contact::isLocal($contact['url'])) {
$contact['network'] = Protocol::DFRN;
}
Logger::notice('Delivering', ['cmd' => $cmd, 'uri-id' => $post_uriid, 'followup' => $followup, 'network' => $contact['network']]);
switch ($contact['network']) {
@ -316,40 +309,6 @@ class Delivery
Logger::debug('Notifier entry: ' . $contact["url"] . ' ' . (($target_item['guid'] ?? '') ?: $target_item['id']) . ' entry: ' . $atom);
// perform local delivery if we are on the same site
if (Model\Contact::isLocal($contact['url'])) {
$condition = ['nurl' => Strings::normaliseLink($contact['url']), 'self' => true];
$target_self = DBA::selectFirst('contact', ['uid'], $condition);
if (!DBA::isResult($target_self)) {
return;
}
$target_uid = $target_self['uid'];
// Check if the user has got this contact
$cid = Model\Contact::getIdForURL($owner['url'], $target_uid);
if (!$cid) {
// Otherwise there should be a public contact
$cid = Model\Contact::getIdForURL($owner['url']);
if (!$cid) {
return;
}
}
$target_importer = DFRN::getImporter($cid, $target_uid);
if (empty($target_importer)) {
// This should never happen
return;
}
DFRN::import($atom, $target_importer, Conversation::PARCEL_LOCAL_DFRN, Conversation::PUSH);
if (in_array($cmd, [Delivery::POST, Delivery::POKE])) {
Model\Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Model\Post\DeliveryData::DFRN);
}
return;
}
$protocol = Model\Post\DeliveryData::DFRN;
// We don't have a relationship with contacts on a public post.

View file

@ -119,7 +119,7 @@ class ExpirePosts
Logger::notice('Adding missing entries');
$rows = 0;
$userposts = DBA::select('post-user', [], ["`uri-id` not in (select `uri-id` from `post`)"], ['group_by' => ['uri-id']]);
$userposts = DBA::select('post-user', [], ["`uri-id` not in (select `uri-id` from `post`)"]);
while ($fields = DBA::fetch($userposts)) {
$post_fields = DBStructure::getFieldsForTable('post', $fields);
DBA::insert('post', $post_fields, Database::INSERT_IGNORE);
@ -133,7 +133,7 @@ class ExpirePosts
}
$rows = 0;
$userposts = DBA::select('post-user', [], ["`gravity` = ? AND `uri-id` not in (select `uri-id` from `post-thread`)", GRAVITY_PARENT], ['group_by' => ['uri-id']]);
$userposts = DBA::select('post-user', [], ["`gravity` = ? AND `uri-id` not in (select `uri-id` from `post-thread`)", GRAVITY_PARENT]);
while ($fields = DBA::fetch($userposts)) {
$post_fields = DBStructure::getFieldsForTable('post-thread', $fields);
$post_fields['commented'] = $post_fields['changed'] = $post_fields['created'];

View file

@ -41,6 +41,7 @@ use Friendica\Protocol\Diaspora;
use Friendica\Protocol\OStatus;
use Friendica\Protocol\Relay;
use Friendica\Protocol\Salmon;
use Friendica\Util\Network;
/*
* The notifier is typically called with:
@ -514,9 +515,12 @@ class Notifier
$delivery_queue_count = 0;
foreach ($contacts as $contact) {
// Ensure that local contacts are delivered via DFRN
if (Contact::isLocal($contact['url'])) {
$contact['network'] = Protocol::DFRN;
// Direct delivery of local contacts
if ($target_uid = User::getIdForURL($contact['url'])) {
Logger::info('Direct delivery', ['uri-id' => $target_item['uri-id'], 'target' => $target_uid]);
$fields = ['protocol' => Conversation::PARCEL_LOCAL_DFRN, 'direction' => Conversation::PUSH];
Item::storeForUserByUriId($target_item['uri-id'], $target_uid, $fields, $target_item['uid']);
continue;
}
// Deletions are always sent via DFRN as well.
@ -775,6 +779,16 @@ class Notifier
foreach ($inboxes as $inbox => $receivers) {
$contacts = array_merge($contacts, $receivers);
if ((count($receivers) == 1) && Network::isLocalLink($inbox)) {
$contact = Contact::getById($receivers[0], ['url']);
if ($target_uid = User::getIdForURL($contact['url'])) {
$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]);
continue;
}
}
Logger::info('Delivery via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]);
if (Worker::add(['priority' => $priority, 'created' => $created, 'dont_fork' => true],

View file

@ -55,7 +55,7 @@
use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1419);
define('DB_UPDATE_VERSION', 1421);
}
return [
@ -882,6 +882,29 @@ return [
"mid" => ["mid"],
]
],
"notification" => [
"comment" => "notifications",
"fields" => [
"id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"],
"uid" => ["type" => "mediumint unsigned", "foreign" => ["user" => "uid"], "comment" => "Owner User id"],
"vid" => ["type" => "smallint unsigned", "foreign" => ["verb" => "id", "on delete" => "restrict"], "comment" => "Id of the verb table entry that contains the activity verbs"],
"type" => ["type" => "tinyint unsigned", "comment" => ""],
"actor-id" => ["type" => "int unsigned", "foreign" => ["contact" => "id"], "comment" => "Link to the contact table with uid=0 of the actor that caused the notification"],
"target-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Item-uri id of the related post"],
"parent-uri-id" => ["type" => "int unsigned", "foreign" => ["item-uri" => "id"], "comment" => "Item-uri id of the parent of the related post"],
"created" => ["type" => "datetime", "comment" => ""],
"seen" => ["type" => "boolean", "default" => "0", "comment" => ""],
],
"indexes" => [
"PRIMARY" => ["id"],
"uid_vid_type_actor-id_target-uri-id" => ["UNIQUE", "uid", "vid", "type", "actor-id", "target-uri-id"],
"vid" => ["vid"],
"actor-id" => ["actor-id"],
"target-uri-id" => ["target-uri-id"],
"parent-uri-id" => ["parent-uri-id"],
"seen_uid" => ["seen", "uid"],
]
],
"notify" => [
"comment" => "notifications",
"fields" => [

View file

@ -196,6 +196,8 @@
"parent-author-link" => ["parent-post-author", "url"],
"parent-author-name" => ["parent-post-author", "name"],
"parent-author-network" => ["parent-post-author", "network"],
"parent-author-blocked" => ["parent-post-author", "blocked"],
"parent-author-hidden" => ["parent-post-author", "hidden"],
],
"query" => "FROM `post-user`
STRAIGHT_JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid`

View file

@ -34,6 +34,8 @@ trait DatabaseTestTrait
StaticDatabase::statConnect($_SERVER);
// 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();
// Start the first, outer transaction
StaticDatabase::getGlobConnection()->beginTransaction();
}

View file

@ -69,7 +69,7 @@ class ExtendedPDO extends PDO
{
if($this->_transactionDepth <= 0 || !$this->hasSavepoint()) {
parent::beginTransaction();
$this->_transactionDepth = $this->_transactionDepth < 0 ? 0 : $this->_transactionDepth;
$this->_transactionDepth = 0;
} else {
$this->exec("SAVEPOINT LEVEL{$this->_transactionDepth}");
}
@ -84,15 +84,16 @@ class ExtendedPDO extends PDO
*/
public function commit()
{
// We don't want to "really" commit something, so skip the most outer hierarchy
if ($this->_transactionDepth <= 1 && $this->hasSavepoint()) {
$this->_transactionDepth = $this->_transactionDepth <= 0 ? 0 : 1;
return true;
}
$this->_transactionDepth--;
if($this->_transactionDepth <= 0 || !$this->hasSavepoint()) {
parent::commit();
$this->_transactionDepth = $this->_transactionDepth < 0 ? 0 : $this->_transactionDepth;
} else {
$this->exec("RELEASE SAVEPOINT LEVEL{$this->_transactionDepth}");
}
}
/**
* Rollback current transaction,
@ -102,14 +103,15 @@ class ExtendedPDO extends PDO
*/
public function rollBack()
{
if ($this->_transactionDepth <= 0) {
throw new PDOException('Rollback error : There is no transaction started');
}
$this->_transactionDepth--;
if($this->_transactionDepth == 0 || !$this->hasSavepoint()) {
if($this->_transactionDepth <= 0 || !$this->hasSavepoint()) {
$this->_transactionDepth = 0;
try {
parent::rollBack();
} catch (PDOException $e) {
// this shouldn't happen, but it does ...
}
} else {
$this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->_transactionDepth}");
}

View file

@ -39,6 +39,9 @@ class StaticDatabase extends Database
*/
private static $staticConnection;
/** @var bool */
private $_locked = false;
/**
* Override the behaviour of connect, due there is just one, static connection at all
*
@ -77,6 +80,31 @@ class StaticDatabase extends Database
return true;
}
/** Mock for locking tables */
public function lock($table)
{
if ($this->_locked) {
return false;
}
$this->in_transaction = true;
$this->_locked = true;
return true;
}
/** Mock for unlocking tables */
public function unlock()
{
// See here: https://dev.mysql.com/doc/refman/5.7/en/lock-tables-and-transactions.html
$this->performCommit();
$this->in_transaction = false;
$this->_locked = false;
return true;
}
/**
* Does a commit
*
@ -163,18 +191,6 @@ class StaticDatabase extends Database
return self::$staticConnection;
}
/**
* Perform a global commit for every nested transaction of the static connection
*/
public static function statCommit()
{
if (isset(self::$staticConnection)) {
while (self::$staticConnection->getTransactionDepth() > 0) {
self::$staticConnection->commit();
}
}
}
/**
* Perform a global rollback for every nested transaction of the static connection
*/

View file

@ -0,0 +1,97 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\Util;
use Friendica\Util\HTTPInputData;
/**
* This class is used to enable testability for HTTPInputData
* It overrides the two PHP input functionality with custom content
*/
class HTTPInputDataDouble extends HTTPInputData
{
/** @var false|resource */
protected static $injectedStream = false;
/** @var false|string */
protected static $injectedContent = false;
/** @var false|string */
protected static $injectedContentType = false;
/**
* injects the PHP input stream for a test
*
* @param false|resource $stream
*/
public static function setPhpInputStream($stream)
{
self::$injectedStream = $stream;
}
/**
* injects the PHP input content for a test
*
* @param false|string $content
*/
public static function setPhpInputContent($content)
{
self::$injectedContent = $content;
}
/**
* injects the PHP input content type for a test
*
* @param false|string $contentType
*/
public static function setPhpInputContentType($contentType)
{
self::$injectedContentType = $contentType;
}
/** {@inheritDoc} */
protected static function getPhpInputStream()
{
return static::$injectedStream;
}
/** {@inheritDoc} */
protected static function getPhpInputContent()
{
return static::$injectedContent;
}
/** {@inheritDoc} */
protected static function getContentType()
{
return static::$injectedContentType;
}
protected static function fetchFileData($stream, string $boundary, array $headers, string $filename)
{
$data = parent::fetchFileData($stream, $boundary, $headers, $filename);
if (!empty($data['tmp_name'])) {
unlink($data['tmp_name']);
$data['tmp_name'] = $data['name'];
}
return $data;
}
}

View file

@ -0,0 +1 @@
{"media_ids":[],"sensitive":false,"status":"Test Status","visibility":"private","spoiler_text":"Title"}

View file

@ -0,0 +1 @@
title=Test2

Binary file not shown.

View file

@ -0,0 +1,50 @@
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="display_name"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 9
User Name
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="note"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 8
About me
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="locked"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 5
false
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="fields_attributes[0][name]"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 10
variable 1
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="fields_attributes[0][value]"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 7
value 1
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="fields_attributes[1][name]"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 10
variable 2
--43395968-f65c-437e-b536-5b33e3e3c7e5
Content-Disposition: form-data; name="fields_attributes[1][value]"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 7
value 2
--43395968-f65c-437e-b536-5b33e3e3c7e5--

View file

@ -189,7 +189,7 @@ class ApiTest extends FixtureTest
private function assertXml($result = '', $root_element = '')
{
self::assertStringStartsWith('<?xml version="1.0"?>', $result);
self::assertContains('<' . $root_element, $result);
self::assertStringContainsString('<' . $root_element, $result);
// We could probably do more checks here.
}
@ -1505,7 +1505,7 @@ class ApiTest extends FixtureTest
$result = api_search('json');
foreach ($result['status'] as $status) {
self::assertStatus($status);
self::assertContains('reply', $status['text'], '', true);
self::assertStringContainsStringIgnoringCase('reply', $status['text'], '', true);
}
}
@ -1521,7 +1521,7 @@ class ApiTest extends FixtureTest
$result = api_search('json');
foreach ($result['status'] as $status) {
self::assertStatus($status);
self::assertContains('reply', $status['text'], '', true);
self::assertStringContainsStringIgnoringCase('reply', $status['text'], '', true);
}
}
@ -1537,7 +1537,7 @@ class ApiTest extends FixtureTest
$result = api_search('json');
foreach ($result['status'] as $status) {
self::assertStatus($status);
self::assertContains('reply', $status['text'], '', true);
self::assertStringContainsStringIgnoringCase('reply', $status['text'], '', true);
}
}
@ -1551,7 +1551,7 @@ class ApiTest extends FixtureTest
$result = api_search('json');
foreach ($result['status'] as $status) {
self::assertStatus($status);
self::assertContains('#friendica', $status['text'], '', true);
self::assertStringContainsStringIgnoringCase('#friendica', $status['text'], '', true);
}
}
@ -2266,6 +2266,7 @@ class ApiTest extends FixtureTest
[
'network' => 'feed',
'title' => 'item_title',
'uri-id' => 1,
// We need a long string to test that it is correctly cut
'body' => 'perspiciatis impedit voluptatem quis molestiae ea qui ' .
'reiciendis dolorum aut ducimus sunt consequatur inventore dolor ' .
@ -2308,11 +2309,12 @@ class ApiTest extends FixtureTest
[
'network' => 'feed',
'title' => 'item_title',
'uri-id' => -1,
'body' => '',
'plink' => 'item_plink'
]
);
self::assertEquals('item_title', $result['text']);
self::assertEquals("item_title", $result['text']);
self::assertEquals('<h4>item_title</h4><br>item_plink', $result['html']);
}
@ -2326,7 +2328,8 @@ class ApiTest extends FixtureTest
$result = api_convert_item(
[
'title' => 'item_title',
'body' => 'item_title item_body'
'body' => 'item_title item_body',
'uri-id' => 1,
]
);
self::assertEquals('item_title item_body', $result['text']);
@ -2874,7 +2877,7 @@ class ApiTest extends FixtureTest
$_POST['text'] = 'message_text';
$_POST['screen_name'] = $this->friendUser['nick'];
$result = api_direct_messages_new('json');
self::assertContains('message_text', $result['direct_message']['text']);
self::assertStringContainsString('message_text', $result['direct_message']['text']);
self::assertEquals('selfcontact', $result['direct_message']['sender_screen_name']);
self::assertEquals(1, $result['direct_message']['friendica_seen']);
}
@ -2891,8 +2894,8 @@ class ApiTest extends FixtureTest
$_POST['screen_name'] = $this->friendUser['nick'];
$_REQUEST['title'] = 'message_title';
$result = api_direct_messages_new('json');
self::assertContains('message_text', $result['direct_message']['text']);
self::assertContains('message_title', $result['direct_message']['text']);
self::assertStringContainsString('message_text', $result['direct_message']['text']);
self::assertStringContainsString('message_title', $result['direct_message']['text']);
self::assertEquals('selfcontact', $result['direct_message']['sender_screen_name']);
self::assertEquals(1, $result['direct_message']['friendica_seen']);
}

View file

@ -8,7 +8,7 @@
timeoutForLargeTests="900">
<testsuite name='friendica'>
<directory suffix='.php'>functional/</directory>
<directory suffix='.php'>include/</directory>
<directory suffix='.php'>legacy/</directory>
<directory suffix='.php'>src/</directory>
</testsuite>
<!-- Filters for Code Coverage -->
@ -16,14 +16,14 @@
<whitelist>
<directory suffix=".php">..</directory>
<exclude>
<directory suffix=".php">config/</directory>
<directory suffix=".php">doc/</directory>
<directory suffix=".php">images/</directory>
<directory suffix=".php">library/</directory>
<directory suffix=".php">spec/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">view/</directory>
<directory suffix=".php">vendor/</directory>
<directory suffix=".php">../config/</directory>
<directory suffix=".php">../doc/</directory>
<directory suffix=".php">../images/</directory>
<directory suffix=".php">../library/</directory>
<directory suffix=".php">../spec/</directory>
<directory suffix=".php">../tests/</directory>
<directory suffix=".php">../view/</directory>
<directory suffix=".php">../vendor/</directory>
</exclude>
</whitelist>
</filter>

View file

@ -103,7 +103,7 @@ class InstallerTest extends MockedTest
$this->mockL10nT('File Information PHP module', 1);
$this->mockL10nT('Error: File Information PHP module required but not installed.', 1);
$this->mockL10nT('Program execution functions', 1);
$this->mockL10nT('Error: Program execution functions required but not enabled.', 1);
$this->mockL10nT('Error: Program execution functions (proc_open) required but not enabled.', 1);
}
private function assertCheckExist($position, $title, $help, $status, $required, $assertionArray)
@ -248,7 +248,7 @@ class InstallerTest extends MockedTest
self::assertFalse($install->checkFunctions());
self::assertCheckExist(9,
'Program execution functions',
'Error: Program execution functions required but not enabled.',
'Error: Program execution functions (proc_open) required but not enabled.',
false,
true,
$install->getChecks());

View file

@ -81,7 +81,7 @@ class FilesystemStorageTest extends StorageTest
public function testMissingDirPermissions()
{
$this->expectException(StorageException::class);
$this->expectExceptionMessageRegExp("/Filesystem storage failed to create \".*\". Check you write permissions./");
$this->expectExceptionMessageMatches("/Filesystem storage failed to create \".*\". Check you write permissions./");
$this->root->getChild('storage')->chmod(000);
$instance = $this->getInstance();
@ -97,7 +97,7 @@ class FilesystemStorageTest extends StorageTest
static::markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure");
$this->expectException(StorageException::class);
$this->expectExceptionMessageRegExp("/Filesystem storage failed to save data to \".*\". Check your write permissions/");
$this->expectExceptionMessageMatches("/Filesystem storage failed to save data to \".*\". Check your write permissions/");
vfsStream::create(['storage' => ['f0' => ['c0' => ['k0i0' => '']]]], $this->root);

View file

@ -68,7 +68,7 @@ class BasePathTest extends MockedTest
public function testFailedBasePath()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessageRegExp("/(.*) is not a valid basepath/");
$this->expectExceptionMessageMatches("/(.*) is not a valid basepath/");
$basepath = new BasePath('/now23452sgfgas', []);
$basepath->getPath();

View file

@ -59,7 +59,7 @@ class ConfigFileLoaderTest extends MockedTest
*/
public function testLoadConfigWrong()
{
$this->expectExceptionMessageRegExp("/Error loading config file \w+/");
$this->expectExceptionMessageMatches("/Error loading config file \w+/");
$this->expectException(\Exception::class);
$this->delConfigFile('local.config.php');

View file

@ -78,16 +78,16 @@ class EMailerTest extends MockedTest
self::assertTrue($emailer->send($testEmail));
self::assertContains("X-Friendica-Host: friendica.local", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("X-Friendica-Platform: Friendica", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("List-ID: <notification.friendica.local>", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("List-Archive: <http://friendica.local/notifications/system>", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("Reply-To: Sender <sender@friendica.local>", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("MIME-Version: 1.0", EmailerSpy::$MAIL_DATA['headers']);
self::assertStringContainsString("X-Friendica-Host: friendica.local", EmailerSpy::$MAIL_DATA['headers']);
self::assertStringContainsString("X-Friendica-Platform: Friendica", EmailerSpy::$MAIL_DATA['headers']);
self::assertStringContainsString("List-ID: <notification.friendica.local>", EmailerSpy::$MAIL_DATA['headers']);
self::assertStringContainsString("List-Archive: <http://friendica.local/notifications/system>", EmailerSpy::$MAIL_DATA['headers']);
self::assertStringContainsString("Reply-To: Sender <sender@friendica.local>", EmailerSpy::$MAIL_DATA['headers']);
self::assertStringContainsString("MIME-Version: 1.0", EmailerSpy::$MAIL_DATA['headers']);
// Base64 "Test Text"
self::assertContains(chunk_split(base64_encode('Test Text')), EmailerSpy::$MAIL_DATA['body']);
self::assertStringContainsString(chunk_split(base64_encode('Test Text')), EmailerSpy::$MAIL_DATA['body']);
// Base64 "Test Message<b>Bold</b>"
self::assertContains(chunk_split(base64_encode("Test Message<b>Bold</b>")), EmailerSpy::$MAIL_DATA['body']);
self::assertStringContainsString(chunk_split(base64_encode("Test Message<b>Bold</b>")), EmailerSpy::$MAIL_DATA['body']);
self::assertEquals("Test Subject", EmailerSpy::$MAIL_DATA['subject']);
self::assertEquals("recipient@friendica.local", EmailerSpy::$MAIL_DATA['to']);
self::assertEquals("-f sender@friendica.local", EmailerSpy::$MAIL_DATA['parameters']);

View file

@ -0,0 +1,152 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* 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 <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Test\src\Util;
use Friendica\Test\MockedTest;
use Friendica\Test\Util\HTTPInputDataDouble;
use Friendica\Util\HTTPInputData;
/**
* Testing HTTPInputData
*
* @see HTTPInputData
*/
class HTTPInputDataTest extends MockedTest
{
/**
* Returns the data stream for the unit test
* Each array element of the first hierarchy represents one test run
* Each array element of the second hierarchy represents the parameters, passed to the test function
*
* @return array[]
*/
public function dataStream()
{
return [
'multipart' => [
'contenttype' => 'multipart/form-data;boundary=43395968-f65c-437e-b536-5b33e3e3c7e5;charset=utf8',
'input' => file_get_contents(__DIR__ . '/../../datasets/http/multipart.httpinput'),
'expected' => [
'variables' => [
'display_name' => 'User Name',
'note' => 'About me',
'locked' => 'false',
'fields_attributes' => [
0 => [
'name' => 'variable 1',
'value' => 'value 1',
],
1 => [
'name' => 'variable 2',
'value' => 'value 2',
]
]
],
'files' => []
]
],
'multipart-file' => [
'contenttype' => 'multipart/form-data;boundary=6d4d5a40-651a-4468-a62e-5a6ca2bf350d;charset=utf8',
'input' => file_get_contents(__DIR__ . '/../../datasets/http/multipart-file.httpinput'),
'expected' => [
'variables' => [
'display_name' => 'Vorname Nachname',
'note' => 'About me',
'fields_attributes' => [
0 => [
'name' => 'variable 1',
'value' => 'value 1',
],
1 => [
'name' => 'variable 2',
'value' => 'value 2',
]
]
],
'files' => [
'avatar' => [
'name' => '8ZUCS34Y5XNH',
'type' => 'image/png',
'tmp_name' => '8ZUCS34Y5XNH',
'error' => 0,
'size' => 349330
],
'header' => [
'name' => 'V2B6Z1IICGPM',
'type' => 'image/png',
'tmp_name' => 'V2B6Z1IICGPM',
'error' => 0,
'size' => 1323635
]
]
]
],
'form-urlencoded' => [
'contenttype' => 'application/x-www-form-urlencoded;charset=utf8',
'input' => file_get_contents(__DIR__ . '/../../datasets/http/form-urlencoded.httpinput'),
'expected' => [
'variables' => [
'title' => 'Test2',
],
'files' => []
]
],
'form-urlencoded-json' => [
'contenttype' => 'application/x-www-form-urlencoded;charset=utf8',
'input' => file_get_contents(__DIR__ . '/../../datasets/http/form-urlencoded-json.httpinput'),
'expected' => [
'variables' => [
'media_ids' => [],
'sensitive' => false,
'status' => 'Test Status',
'visibility' => 'private',
'spoiler_text' => 'Title'
],
'files' => []
]
]
];
}
/**
* Tests the HTTPInputData::process() method
*
* @param string $contentType The content typer of the transmitted data
* @param string $input The input, we got from the data stream
* @param array $expected The expected output
*
* @dataProvider dataStream
* @see HTTPInputData::process()
*/
public function testHttpInput(string $contentType, string $input, array $expected)
{
HTTPInputDataDouble::setPhpInputContentType($contentType);
HTTPInputDataDouble::setPhpInputContent($input);
$stream = fopen('php://memory', 'r+');
fwrite($stream, $input);
rewind($stream);
HTTPInputDataDouble::setPhpInputStream($stream);
$output = HTTPInputDataDouble::process();
$this->assertEqualsCanonicalizing($expected, $output);
}
}

View file

@ -106,8 +106,8 @@ abstract class AbstractLoggerTest extends MockedTest
$logger->emergency('A {psr} test', ['psr' => 'working']);
$logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]);
$text = $this->getContent();
self::assertContains('A working test', $text);
self::assertContains('An ["it","is","working"] test', $text);
self::assertStringContainsString('A working test', $text);
self::assertStringContainsString('An ["it","is","working"] test', $text);
}
/**
@ -119,9 +119,9 @@ abstract class AbstractLoggerTest extends MockedTest
$logger->emergency('A test');
$text = $this->getContent();
self::assertContains('"file":"' . self::FILE . '"', $text);
self::assertContains('"line":' . self::LINE, $text);
self::assertContains('"function":"' . self::FUNC . '"', $text);
self::assertStringContainsString('"file":"' . self::FILE . '"', $text);
self::assertStringContainsString('"line":' . self::LINE, $text);
self::assertStringContainsString('"function":"' . self::FUNC . '"', $text);
}
/**
@ -157,7 +157,7 @@ abstract class AbstractLoggerTest extends MockedTest
self::assertLogline($text);
self::assertContains(@json_encode($context), $text);
self::assertStringContainsString(@json_encode($context), $text);
}
/**
@ -176,7 +176,7 @@ abstract class AbstractLoggerTest extends MockedTest
self::assertLogline($text);
self::assertContains(@json_encode($assertion), $this->getContent());
self::assertStringContainsString(@json_encode($assertion), $this->getContent());
}
public function testNoObjectHandling()
@ -187,6 +187,6 @@ abstract class AbstractLoggerTest extends MockedTest
self::assertLogline($text);
self::assertContains('test', $this->getContent());
self::assertStringContainsString('test', $this->getContent());
}
}

View file

@ -128,7 +128,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongUrl()
{
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessageRegExp("/The stream or file .* could not be opened: .* /");
$this->expectExceptionMessageMatches("/The stream or file .* could not be opened: .* /");
$logfile = vfsStream::newFile('friendica.log')
->at($this->root)->chmod(0);
@ -144,7 +144,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongDir()
{
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessageRegExp("/Directory .* cannot get created: .* /");
$this->expectExceptionMessageMatches("/Directory .* cannot get created: .* /");
static::markTestIncomplete('We need a platform independent way to set directory to readonly');
@ -159,7 +159,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongMinimumLevel()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageRegExp("/The level \".*\" is not valid./");
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logger = new StreamLogger('test', 'file.text', $this->introspection, $this->fileSystem, 'NOPE');
}
@ -170,7 +170,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongLogLevel()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageRegExp("/The level \".*\" is not valid./");
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logfile = vfsStream::newFile('friendica.log')
->at($this->root);

View file

@ -63,7 +63,7 @@ class SyslogLoggerTest extends AbstractLoggerTest
public function testWrongMinimumLevel()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageRegExp("/The level \".*\" is not valid./");
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logger = new SyslogLoggerWrapper('test', $this->introspection, 'NOPE');
}
@ -74,7 +74,7 @@ class SyslogLoggerTest extends AbstractLoggerTest
public function testWrongLogLevel()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageRegExp("/The level \".*\" is not valid./");
$this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logger = new SyslogLoggerWrapper('test', $this->introspection);
@ -88,7 +88,7 @@ class SyslogLoggerTest extends AbstractLoggerTest
{
if (PHP_MAJOR_VERSION < 8) {
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessageRegExp("/Can\'t open syslog for ident \".*\" and facility \".*\": .* /");
$this->expectExceptionMessageMatches("/Can\'t open syslog for ident \".*\" and facility \".*\": .* /");
} else {
$this->expectException(\TypeError::class);
$this->expectExceptionMessage("openlog(): Argument #3 (\$facility) must be of type int, string given");

Some files were not shown because too many files have changed in this diff Show more