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 round: down
range: "70...100" range: "70...100"
status: status:
project: off project: false
patch: off patch: false
comment: off comment: false

View file

@ -52,21 +52,81 @@ trigger:
- pull_request - pull_request
steps: 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 - name: Install dependencies
image: composer image: composer
commands: commands:
- export COMPOSER_HOME=.composer
- ./bin/composer.phar run cs:install - ./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 - name: Run coding standards check
image: friendicaci/php-cs image: friendicaci/php-cs
commands: commands:
- export CHANGED_FILES="$(git diff --name-status ${DRONE_COMMIT_BEFORE}..${DRONE_COMMIT_AFTER} | grep ^A | cut -f2)" - export CHANGED_FILES="$(git diff --name-status ${DRONE_COMMIT_BEFORE}..${DRONE_COMMIT_AFTER} | grep ^A | cut -f2)"
- /check-php-cs.sh - /check-php-cs.sh
volumes:
- name: cache
host:
path: /tmp/drone-cache
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: php7.3-mariadb name: php7.3-mariadb
steps: 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 - name: Test Friendica
image: friendicaci/php7.3:php7.3.28 image: friendicaci/php7.3:php7.3.28
environment: environment:
@ -79,8 +139,6 @@ steps:
MEMCACHED_HOST: "memcached" MEMCACHED_HOST: "memcached"
MEMCACHE_HOST: "memcached" MEMCACHE_HOST: "memcached"
commands: commands:
- composer validate
- composer install --prefer-dist
- cp config/local-sample.config.php config/local.config.php - 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 - 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 - mysql -h$MYSQL_HOST -P$MYSQL_PORT -p$MYSQL_PASSWORD -u$MYSQL_USER $MYSQL_DATABASE < database.sql
@ -102,12 +160,47 @@ services:
- name: redis - name: redis
image: redis image: redis
volumes:
- name: cache
host:
path: /tmp/drone-cache
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: php7.4-mariadb name: php7.4-mariadb
steps: 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 - name: Test Friendica
image: friendicaci/php7.4:php7.4.18 image: friendicaci/php7.4:php7.4.18
environment: environment:
@ -122,8 +215,6 @@ steps:
XDEBUG_MODE: "coverage" XDEBUG_MODE: "coverage"
commands: commands:
- phpenmod xdebug - phpenmod xdebug
- composer validate
- composer install --prefer-dist
- cp config/local-sample.config.php config/local.config.php - 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 - 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 - mysql -h$MYSQL_HOST -P$MYSQL_PORT -p$MYSQL_PASSWORD -u$MYSQL_USER $MYSQL_DATABASE < database.sql
@ -155,12 +246,47 @@ services:
- name: redis - name: redis
image: redis image: redis
volumes:
- name: cache
host:
path: /tmp/drone-cache
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: php8.0-mariadb name: php8.0-mariadb
steps: 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 - name: Test Friendica
image: friendicaci/php8.0:php8.0.5 image: friendicaci/php8.0:php8.0.5
environment: environment:
@ -173,8 +299,6 @@ steps:
MEMCACHED_HOST: "memcached" MEMCACHED_HOST: "memcached"
MEMCACHE_HOST: "memcached" MEMCACHE_HOST: "memcached"
commands: commands:
- composer validate
- composer install --prefer-dist
- cp config/local-sample.config.php config/local.config.php - 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 - 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 - mysql -h$MYSQL_HOST -P$MYSQL_PORT -p$MYSQL_PASSWORD -u$MYSQL_USER $MYSQL_DATABASE < database.sql
@ -196,3 +320,8 @@ services:
- name: redis - name: redis
image: 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 /config/addon.ini.php
#ignore documentation, it should be newly built #ignore documentation, it should be newly built
/doc/html /doc/api
/doc/cache
#ignore reports, should be generated with every build #ignore reports, should be generated with every build
report/ report/

6
Vagrantfile vendored
View file

@ -34,10 +34,14 @@ Vagrant.configure(2) do |config|
# #
# # Customize the amount of memory on the VM: # # Customize the amount of memory on the VM:
vb.memory = server_memory 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 end
# Enable provisioning with a shell script. # 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: "always"
# run: "once" # run: "once"
end end

Binary file not shown.

View file

@ -3,11 +3,21 @@
# #
# DO NOT RUN on your physical machine as this won't be of any use # DO NOT RUN on your physical machine as this won't be of any use
# and f.e. deletes your /var/www/ folder! # and f.e. deletes your /var/www/ folder!
echo "Friendica configuration settings" #
sudo apt-get update # Run as root by vagrant
#
##
# Install virtualbox guest additions ADMIN_NICK="admin"
sudo apt-get install virtualbox-guest-x11 ADMIN_PASSW="admin"
USER_NICK="user"
USER_PASSW="user"
##
echo "Friendica configuration settings"
apt-get update
#Selfsigned cert #Selfsigned cert
echo ">>> Installing *.xip.io self-signed SSL" echo ">>> Installing *.xip.io self-signed SSL"
@ -23,32 +33,32 @@ localityName=New Haven/
commonName=$DOMAIN/ commonName=$DOMAIN/
subjectAltName=DNS:$EXTRADOMAIN subjectAltName=DNS:$EXTRADOMAIN
" "
sudo mkdir -p "$SSL_DIR" mkdir -p "$SSL_DIR"
sudo openssl genrsa -out "$SSL_DIR/xip.io.key" 4096 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 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" 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 #Install apache2
echo ">>> Installing Apache2 webserver" echo ">>> Installing Apache2 webserver"
sudo apt-get install -y apache2 apt-get install -qq apache2
sudo a2enmod rewrite actions ssl a2enmod rewrite actions ssl
sudo cp /vagrant/bin/dev/vagrant_vhost.sh /usr/local/bin/vhost cp /vagrant/bin/dev/vagrant_vhost.sh /usr/local/bin/vhost
sudo chmod guo+x /usr/local/bin/vhost 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 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 a2dissite 000-default
sudo service apache2 restart service apache2 restart
#Install php #Install php
echo ">>> Installing PHP7" 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 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
sudo systemctl restart apache2 systemctl restart apache2
#Install mysql #Install mysql
echo ">>> Installing Mysql" echo ">>> Installing Mysql"
sudo debconf-set-selections <<< "mariadb-server mariadb-server/root_password password root" debconf-set-selections <<< "mariadb-server mariadb-server/root_password password root"
sudo debconf-set-selections <<< "mariadb-server mariadb-server/root_password_again password root" debconf-set-selections <<< "mariadb-server mariadb-server/root_password_again password root"
sudo apt-get install -qq mariadb-server apt-get install -qq mariadb-server
# enable remote access # enable remote access
# setting the mysql bind-address to allow connections from everywhere # 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 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 systemctl restart mysql
#configure rudimentary mail server (local delivery only) #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'. #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/mailname string friendica.local"
debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Local Only'" debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Local Only'"
sudo apt-get install -y postfix mailutils libmailutils-dev apt-get install -qq postfix mailutils libmailutils-dev
sudo echo -e "friendica1: vagrant\nfriendica2: vagrant\nfriendica3: vagrant\nfriendica4: vagrant\nfriendica5: vagrant" >> /etc/aliases && sudo newaliases echo -e "friendica1: vagrant\nfriendica2: vagrant\nfriendica3: vagrant\nfriendica4: vagrant\nfriendica5: vagrant" >> /etc/aliases && newaliases
# Friendica needs git for fetching some dependencies # 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 #make the vagrant directory the docroot
sudo rm -rf /var/www/ echo ">>> Symlink /var/www to /vagrant"
sudo ln -fs /vagrant /var/www rm -rf /var/www/
ln -fs /vagrant /var/www
# install deps with composer # install deps with composer
sudo apt install unzip echo ">>> Installing php requirements"
apt install unzip
cd /var/www 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 # copy the .htaccess-dist file to .htaccess so that rewrite rules work
cp /vagrant/.htaccess-dist /vagrant/.htaccess cp /vagrant/.htaccess-dist /vagrant/.htaccess
# create the friendica database # create the friendica database
echo "create database friendica DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" | $MYSQL -u root -proot echo "create database friendica DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" | $MYSQL -u root -proot
# import test database # import test database (disabled because too old)
$MYSQL -uroot -proot friendica < /vagrant/friendica_test_data.sql #$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 # create cronjob - activate if you have enough memory in you dev VM
echo "*/10 * * * * cd /vagrant; /usr/bin/php bin/worker.php" >> friendicacron # cronjob runs as www-data user
sudo crontab friendicacron echo ">>> Installing cronjob"
sudo rm friendicacron echo "*/10 * * * * www-data cd /vagrant; /usr/bin/php bin/worker.php" >> /etc/cron.d/friendica
# friendica needs write access to /tmp # 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", "name": "guzzlehttp/psr7",
"version": "1.8.1", "version": "1.8.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/psr7.git", "url": "https://github.com/guzzle/psr7.git",
"reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" "reference": "dc960a912984efb74d0a90222870c72c87f10c91"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
"reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", "reference": "dc960a912984efb74d0a90222870c72c87f10c91",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -803,7 +803,7 @@
"uri", "uri",
"url" "url"
], ],
"time": "2021-03-21T16:25:00+00:00" "time": "2021-04-26T09:17:50+00:00"
}, },
{ {
"name": "league/html-to-markdown", "name": "league/html-to-markdown",
@ -1119,16 +1119,16 @@
}, },
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "1.26.0", "version": "1.26.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Seldaek/monolog.git", "url": "https://github.com/Seldaek/monolog.git",
"reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33" "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/2209ddd84e7ef1256b7af205d0717fb62cfc9c33", "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5",
"reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33", "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1197,7 +1197,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-12-14T12:56:38+00:00" "time": "2021-05-28T08:32:12+00:00"
}, },
{ {
"name": "nikic/fast-route", "name": "nikic/fast-route",
@ -1591,19 +1591,11 @@
}, },
{ {
"name": "npm-asset/jgrowl", "name": "npm-asset/jgrowl",
"version": "1.4.6", "version": "1.4.8",
"dist": { "dist": {
"type": "tar", "type": "tar",
"url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.6.tgz", "url": "https://registry.npmjs.org/jgrowl/-/jgrowl-1.4.8.tgz",
"shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3" "shasum": "4ba40ffb93757a7e1d9b262d916be299d03df3a4"
},
"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"
}, },
"type": "npm-asset-library", "type": "npm-asset-library",
"extra": { "extra": {
@ -1616,8 +1608,13 @@
"type": "git", "type": "git",
"url": "git+ssh://git@github.com/stanlemon/jGrowl.git" "url": "git+ssh://git@github.com/stanlemon/jGrowl.git"
}, },
"npm-asset-scripts": [] "npm-asset-scripts": {
"build": "grunt"
}
}, },
"license": [
"MIT"
],
"authors": [ "authors": [
{ {
"name": "Stan Lemon", "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 ", "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", "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", "name": "npm-asset/jquery",
@ -2224,16 +2221,16 @@
}, },
{ {
"name": "paragonie/certainty", "name": "paragonie/certainty",
"version": "v2.8.0", "version": "v2.8.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paragonie/certainty.git", "url": "https://github.com/paragonie/certainty.git",
"reference": "94fc99f8c1f5bdce960713d6b63a108a64d90dfa" "reference": "e2a1da558f95074545ad811d60359c74500a5e24"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paragonie/certainty/zipball/94fc99f8c1f5bdce960713d6b63a108a64d90dfa", "url": "https://api.github.com/repos/paragonie/certainty/zipball/e2a1da558f95074545ad811d60359c74500a5e24",
"reference": "94fc99f8c1f5bdce960713d6b63a108a64d90dfa", "reference": "e2a1da558f95074545ad811d60359c74500a5e24",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2241,12 +2238,12 @@
"ext-json": "*", "ext-json": "*",
"guzzlehttp/guzzle": "^6|^7", "guzzlehttp/guzzle": "^6|^7",
"paragonie/constant_time_encoding": "^1|^2", "paragonie/constant_time_encoding": "^1|^2",
"paragonie/sodium_compat": "^1.11", "paragonie/sodium_compat": "^1.13",
"php": "^5.5|^7|^8" "php": "^5.5|^7|^8"
}, },
"require-dev": { "require-dev": {
"composer/composer": "^1", "composer/composer": "^1|>=2.0.14",
"phpunit/phpunit": "^4|^5|^6|^7" "phpunit/phpunit": "^4|^5|^6|^7|^8|^9"
}, },
"bin": [ "bin": [
"bin/certainty-cert-symlink" "bin/certainty-cert-symlink"
@ -2282,7 +2279,7 @@
"ssl", "ssl",
"tls" "tls"
], ],
"time": "2020-10-15T08:10:12+00:00" "time": "2021-05-25T18:27:41+00:00"
}, },
{ {
"name": "paragonie/constant_time_encoding", "name": "paragonie/constant_time_encoding",
@ -2442,16 +2439,16 @@
}, },
{ {
"name": "paragonie/sodium_compat", "name": "paragonie/sodium_compat",
"version": "v1.14.0", "version": "v1.16.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paragonie/sodium_compat.git", "url": "https://github.com/paragonie/sodium_compat.git",
"reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3" "reference": "2e856afe80bfc968b47da1f4a7e1ea8f03d06b38"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3", "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/2e856afe80bfc968b47da1f4a7e1ea8f03d06b38",
"reference": "a1cfe0b21faf9c0b61ac0c6188c4af7fd6fd0db3", "reference": "2e856afe80bfc968b47da1f4a7e1ea8f03d06b38",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2520,7 +2517,7 @@
"secret-key cryptography", "secret-key cryptography",
"side-channel resistant" "side-channel resistant"
], ],
"time": "2020-12-03T16:26:19+00:00" "time": "2021-05-25T12:58:14+00:00"
}, },
{ {
"name": "patrickschur/language-detection", "name": "patrickschur/language-detection",
@ -2625,16 +2622,16 @@
}, },
{ {
"name": "phpseclib/phpseclib", "name": "phpseclib/phpseclib",
"version": "2.0.30", "version": "2.0.31",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpseclib/phpseclib.git", "url": "https://github.com/phpseclib/phpseclib.git",
"reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36" "reference": "233a920cb38636a43b18d428f9a8db1f0a1a08f4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/136b9ca7eebef78be14abf90d65c5e57b6bc5d36", "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/233a920cb38636a43b18d428f9a8db1f0a1a08f4",
"reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36", "reference": "233a920cb38636a43b18d428f9a8db1f0a1a08f4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2726,7 +2723,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-12-17T05:42:04+00:00" "time": "2021-04-06T13:56:45+00:00"
}, },
{ {
"name": "pragmarx/google2fa", "name": "pragmarx/google2fa",
@ -3058,16 +3055,16 @@
}, },
{ {
"name": "psr/log", "name": "psr/log",
"version": "1.1.3", "version": "1.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-fig/log.git", "url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3091,7 +3088,7 @@
"authors": [ "authors": [
{ {
"name": "PHP-FIG", "name": "PHP-FIG",
"homepage": "http://www.php-fig.org/" "homepage": "https://www.php-fig.org/"
} }
], ],
"description": "Common interface for logging libraries", "description": "Common interface for logging libraries",
@ -3101,7 +3098,7 @@
"psr", "psr",
"psr-3" "psr-3"
], ],
"time": "2020-03-23T09:12:05+00:00" "time": "2021-05-03T11:20:27+00:00"
}, },
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2021.06-rc (Siberian Iris) -- 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 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'; ) 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 -- TABLE notify
-- --
@ -1685,7 +1712,9 @@ CREATE VIEW `post-user-view` AS SELECT
`parent-post`.`author-id` AS `parent-author-id`, `parent-post`.`author-id` AS `parent-author-id`,
`parent-post-author`.`url` AS `parent-author-link`, `parent-post-author`.`url` AS `parent-author-link`,
`parent-post-author`.`name` AS `parent-author-name`, `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` 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 `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` STRAIGHT_JOIN `contact` 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. These emdpoints are planned to be implemented somewhere in the future.
- [`PATCH /api/v1/accounts/update_credentials`](https://docs.joinmastodon.org/methods/accounts/) - [`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 ## Dummy endpoints
@ -139,7 +138,7 @@ They refer to features that don't exist in Friendica yet.
## Non supportable endpoints ## Non supportable endpoints
These endpoints won't be implemented at the moment. 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`](https://docs.joinmastodon.org/methods/accounts/)
- [`POST /api/v1/accounts/:id/pin`](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/) - [`POST /api/v1/filters/:id`](https://docs.joinmastodon.org/methods/accounts/filters/)
- [`PUT /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/) - [`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/) - [`POST /api/v1/markers`](https://docs.joinmastodon.org/methods/timelines/markers/)
- [`GET /api/v1/polls/:id`](https://docs.joinmastodon.org/methods/statuses/polls/) - [`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/) - [`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: 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) * 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 * 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) * 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) * [Mailing List](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
<!--- * [XMPP](xmpp:support@forum.friendi.ca?join) <!--- * [XMPP](xmpp:support@forum.friendi.ca?join)
@ -182,6 +183,7 @@ Example: Friendica Support
### What friendica clients can I use? ### 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. 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: Here is a list of known working clients:
* Android * Android
@ -195,6 +197,11 @@ Here is a list of known working clients:
* [Choqok](https://choqok.kde.org) * [Choqok](https://choqok.kde.org)
* Windows * Windows
* [Friendica Mobile](https://www.microsoft.com/de-DE/store/p/friendica-mobile/9nblggh0fhmn?rtc=1) (Windows 10) * [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). 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 * Ways to get Support
* Friendica Support Forum: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers) * 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`` * [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 * XMPP/Jabber MUC: support(at)forum.friendi.ca
* IRC: [#friendica at freenode.net](https://webchat.freenode.net/?settings=#friendica) * 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 * 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: 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) * 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 * 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) * 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) * [Mailing List](http://mailman.friendi.ca/mailman/listinfo/support-friendi.ca)
<!--- * [XMPP](xmpp:support@forum.friendi.ca?join) <!--- * [XMPP](xmpp:support@forum.friendi.ca?join)
@ -192,6 +193,7 @@ Beispiel: Friendica Support
### Gibt es Clients für Friendica? ### Gibt es Clients für Friendica?
Friendica verwendet eine [Twitter/GNU Social](help/api) kompatible API. 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. 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: 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 * Mustard and Mustard-Mod
* SailfishOS * SailfishOS
* [Friendly](https://openrepos.net/content/fabrixxm/friendly#comment-form) * [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 * Linux
* Hotot * Hotot
* Choqok * Choqok

View file

@ -64,8 +64,9 @@ Friendica - Dokumentation und Ressourcen
* Support Kanäle * Support Kanäle
* Friendica Support Forum: [@helpers@forum.friendi.ca](https://forum.friendi.ca/~helpers) * 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 * [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 * 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 * 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** **Ü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']) { switch ($row['post-reason']) {
case Item::PR_TO: case Item::PR_TO:
$row['direction'] = ['direction' => 7, 'title' => DI::l10n()->t('You had been addressed (%s).', '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'])) { if (($row['gravity'] == GRAVITY_PARENT) && !empty($row['causer-id'])) {
$causer = ['uid' => 0, 'id' => $row['causer-id'], $causer = ['uid' => 0, 'id' => $row['causer-id'],
'network' => $row['causer-network'], 'url' => $row['causer-link']]; '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; break;
case Item::PR_COMMENT: case Item::PR_COMMENT:
$row['direction'] = ['direction' => 5, 'title' => DI::l10n()->t('%s is participating in this thread.', $row['author-name'])]; $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')]; $row['direction'] = ['direction' => 9, 'title' => DI::l10n()->t('Global')];
break; break;
case Item::PR_RELAY: 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; break;
case Item::PR_FETCHED: 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; break;
} }

View file

@ -184,9 +184,9 @@ function notification($params)
// First go for the general message // First go for the general message
// "George Bull's post" // "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'); $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'); $message = $l10n->t('%1$s tagged you on %2$s\'s %3$s %4$s');
} else { } else {
$message = $l10n->t('%1$s commented on %2$s\'s %3$s %4$s'); $message = $l10n->t('%1$s commented on %2$s\'s %3$s %4$s');
@ -197,10 +197,10 @@ function notification($params)
// Then look for the special cases // Then look for the special cases
// "your post" // "your post"
if ($params['activity']['origin_thread']) { if (!empty($params['activity']['origin_thread'])) {
if ($params['activity']['origin_comment']) { if (!empty($params['activity']['origin_comment'])) {
$message = $l10n->t('%1$s replied to you on your %2$s %3$s'); $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'); $message = $l10n->t('%1$s tagged you on your %2$s %3$s');
} else { } else {
$message = $l10n->t('%1$s commented on your %2$s %3$s'); $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); $dest_str = sprintf($message, $params['source_name'], $item_post_type, $title);
// "their post" // "their post"
} elseif ($item['author-link'] == $params['source_link']) { } 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'); $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'); $message = $l10n->t('%1$s tagged you on their %2$s %3$s');
} else { } else {
$message = $l10n->t('%1$s commented on their %2$s %3$s'); $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. // 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 // Before this we have the name of the replier on the subject rendering
// different subjects for messages on the same thread. // 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']); $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); $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']), '$about' => BBCode::convert($a->profile['about']),
]); ]);
$cal_widget = Widget\CalendarExport::getHTML(); $cal_widget = Widget\CalendarExport::getHTML($user['uid']);
if (empty(DI::page()['aside'])) { if (empty(DI::page()['aside'])) {
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_handle'] = $r[0]['addr'];
$_SESSION['visitor_visiting'] = $r[0]['uid']; $_SESSION['visitor_visiting'] = $r[0]['uid'];
$_SESSION['my_url'] = $r[0]['url']; $_SESSION['my_url'] = $r[0]['url'];
$_SESSION['remote_comment'] = $r[0]['subscribe'];
Session::setVisitorsContacts(); Session::setVisitorsContacts();
@ -497,8 +498,10 @@ function dfrn_poll_content(App $a)
$_SESSION['authenticated'] = 1; $_SESSION['authenticated'] = 1;
$_SESSION['visitor_id'] = $r[0]['id']; $_SESSION['visitor_id'] = $r[0]['id'];
$_SESSION['visitor_home'] = $r[0]['url']; $_SESSION['visitor_home'] = $r[0]['url'];
$_SESSION['visitor_handle'] = $r[0]['addr'];
$_SESSION['visitor_visiting'] = $r[0]['uid']; $_SESSION['visitor_visiting'] = $r[0]['uid'];
$_SESSION['my_url'] = $r[0]['url']; $_SESSION['my_url'] = $r[0]['url'];
$_SESSION['remote_comment'] = $r[0]['subscribe'];
Session::setVisitorsContacts(); 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')) { 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()]); 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'] = ''; DI::page()['aside'] = '';
} }
$cal_widget = CalendarExport::getHTML(); $cal_widget = CalendarExport::getHTML(local_user());
DI::page()['aside'] .= $cal_widget; 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']) { if ($success['replaced']) {
$tagged[] = $tag; $tagged[] = $tag;
} }
@ -427,6 +427,7 @@ function item_post(App $a) {
$forum_contact = $success['contact']; $forum_contact = $success['contact'];
} }
} }
}
return $body; return $body;
}); });
@ -713,13 +714,6 @@ function item_post(App $a) {
unset($datarray['self']); unset($datarray['self']);
unset($datarray['api_source']); 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); $post_id = Item::insert($datarray);
if (!$post_id) { if (!$post_id) {

View file

@ -1275,7 +1275,7 @@ function photos_content(App $a)
} }
if (!empty($link_item['parent']) && !empty($link_item['uid'])) { 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); $total = Post::count($condition);
$pager = new Pager(DI::l10n(), DI::args()->getQueryString()); $pager = new Pager(DI::l10n(), DI::args()->getQueryString());

View file

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

View file

@ -29,6 +29,7 @@ return [
// **************************************************************** // ****************************************************************
'config' => [ 'config' => [
'hostname' => 'friendica.local',
'admin_email' => 'admin@friendica.local', 'admin_email' => 'admin@friendica.local',
'sitename' => 'Friendica Social Network', 'sitename' => 'Friendica Social Network',
'register_policy' => \Friendica\Module\Register::OPEN, 'register_policy' => \Friendica\Module\Register::OPEN,
@ -38,5 +39,6 @@ return [
'default_timezone' => 'UTC', 'default_timezone' => 'UTC',
'language' => 'en', 'language' => 'en',
'basepath' => '/vagrant', '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\System;
use Friendica\Core\Theme; use Friendica\Core\Theme;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Module\Special\HTTPException as ModuleHTTPException; use Friendica\Module\Special\HTTPException as ModuleHTTPException;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
@ -464,6 +465,11 @@ class App
if (Core\Session::get('visitor_home') != $_GET["zrl"]) { if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
Core\Session::set('my_url', $_GET['zrl']); Core\Session::set('my_url', $_GET['zrl']);
Core\Session::set('authenticated', 0); 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); Model\Profile::zrlInit($this);

View file

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

View file

@ -127,16 +127,6 @@ class Item
$tag_type = substr($tag, 0, 1); $tag_type = substr($tag, 0, 1);
//is it already replaced? //is it already replaced?
if (strpos($tag, '[url=')) { 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; return $replaced;
} }

View file

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

View file

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

View file

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

View file

@ -465,7 +465,7 @@ class Installer
$status = $this->checkFunction('proc_open', $status = $this->checkFunction('proc_open',
DI::l10n()->t('Program execution functions'), 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 true
); );
$returnVal = $returnVal ? $status : false; $returnVal = $returnVal ? $status : false;

View file

@ -241,6 +241,12 @@ class DBStructure
// Assign all field that are present in the table // Assign all field that are present in the table
foreach ($fieldnames as $field) { foreach ($fieldnames as $field) {
if (isset($data[$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]; $fields[$field] = $data[$field];
} }
} }

View file

@ -25,22 +25,18 @@ use Friendica\BaseFactory;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; 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 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)) { if (!DBA::isResult($notification)) {
return null; return null;
} }
$cid = Contact::getIdForURL($notification['url'], 0, false);
if (empty($cid)) {
return null;
}
/* /*
follow = Someone followed you follow = Someone followed you
follow_request = Someone requested to follow 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 status = Someone you enabled notifications for has posted a status
*/ */
switch ($notification['type']) { if (($notification['vid'] == Verb::getID(Activity::FOLLOW)) && ($notification['type'] == Post\UserNotification::NOTIF_NONE)) {
case ModelNotification\Type::INTRO: $contact = Contact::getById($notification['actor-id'], ['pending']);
$type = 'follow_request'; $type = $contact['pending'] ? $type = 'follow_request' : 'follow';
break; } elseif (($notification['vid'] == Verb::getID(Activity::ANNOUNCE)) &&
in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) {
case ModelNotification\Type::WALL: $type = 'reblog';
case ModelNotification\Type::COMMENT: } elseif (in_array($notification['vid'], [Verb::getID(Activity::LIKE), Verb::getID(Activity::DISLIKE)]) &&
case ModelNotification\Type::MAIL: in_array($notification['type'], [Post\UserNotification::NOTIF_DIRECT_COMMENT, Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT])) {
case ModelNotification\Type::TAG_SELF: $type = 'favourite';
case ModelNotification\Type::POKE: } elseif ($notification['type'] == Post\UserNotification::NOTIF_SHARED) {
$type = 'mention';
break;
case ModelNotification\Type::SHARE:
$type = 'status'; $type = 'status';
break; } elseif (in_array($notification['type'], [Post\UserNotification::NOTIF_EXPLICIT_TAGGED,
Post\UserNotification::NOTIF_IMPLICIT_TAGGED, Post\UserNotification::NOTIF_DIRECT_COMMENT,
default: Post\UserNotification::NOTIF_DIRECT_THREAD_COMMENT, Post\UserNotification::NOTIF_THREAD_COMMENT])) {
$type = 'mention';
} else {
return null; 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 { try {
$status = DI::mstdnStatus()->createFromUriId($notification['uri-id'], $notification['uid']); $status = DI::mstdnStatus()->createFromUriId($notification['target-uri-id'], $notification['uid']);
} catch (\Throwable $th) { } catch (\Throwable $th) {
$status = null; $status = null;
} }
@ -84,6 +78,6 @@ class Notification extends BaseFactory
$status = null; $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::LIKE), 'deleted' => false]),
Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE), '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), Post\ThreadUser::getIgnored($uriId, $uid),
(bool)$item['starred'], (bool)($item['starred'] && ($item['gravity'] == GRAVITY_PARENT)),
Post\ThreadUser::getPinned($uriId, $uid) Post\ThreadUser::getPinned($uriId, $uid)
); );

View file

@ -26,6 +26,7 @@ use Friendica\Core\Cache\Duration;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Network\Probe; use Friendica\Network\Probe;
use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityNamespace;
@ -349,6 +350,9 @@ class APContact
DBA::delete('apcontact', ['url' => $url]); DBA::delete('apcontact', ['url' => $url]);
} }
// Limit the length on incoming fields
$apcontact = DBStructure::getFieldsForTable('apcontact', $apcontact);
if (DBA::exists('apcontact', ['url' => $apcontact['url']])) { if (DBA::exists('apcontact', ['url' => $apcontact['url']])) {
DBA::update('apcontact', $apcontact, ['url' => $apcontact['url']]); DBA::update('apcontact', $apcontact, ['url' => $apcontact['url']]);
} else { } else {
@ -357,7 +361,7 @@ class APContact
Logger::info('Updated profile', ['url' => $url]); 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 // Update the contact in the background if needed
$updated = max($contact['success_update'], $contact['created'], $contact['updated'], $contact['last-update'], $contact['failure_update']); $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']); Worker::add(PRIORITY_LOW, "UpdateContact", $contact['id']);
} }
@ -566,18 +566,13 @@ class Contact
*/ */
public static function createSelfFromUserId($uid) 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'], $user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'pubkey', 'prvkey'],
['uid' => $uid, 'account_expired' => false]); ['uid' => $uid, 'account_expired' => false]);
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
return false; return false;
} }
$return = DBA::insert('contact', [ $contact = [
'uid' => $user['uid'], 'uid' => $user['uid'],
'created' => DateTimeFormat::utcNow(), 'created' => DateTimeFormat::utcNow(),
'self' => 1, 'self' => 1,
@ -602,7 +597,23 @@ class Contact
'uri-date' => DateTimeFormat::utcNow(), 'uri-date' => DateTimeFormat::utcNow(),
'avatar-date' => DateTimeFormat::utcNow(), 'avatar-date' => DateTimeFormat::utcNow(),
'closeness' => 0 '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; return $return;
} }
@ -704,6 +715,8 @@ class Contact
DBA::update('contact', $fields, ['id' => $self['id']]); DBA::update('contact', $fields, ['id' => $self['id']]);
// Update the public contact as well // Update the public contact as well
$fields['prvkey'] = null;
$fields['self'] = false;
DBA::update('contact', $fields, ['uid' => 0, 'nurl' => $self['nurl']]); DBA::update('contact', $fields, ['uid' => 0, 'nurl' => $self['nurl']]);
// Update the profile // Update the profile
@ -1941,7 +1954,7 @@ class Contact
return false; return false;
} }
if (Contact::isLocal($ret['url'])) { if (self::isLocal($ret['url'])) {
Logger::info('Local contacts are not updated here.'); Logger::info('Local contacts are not updated here.');
return true; return true;
} }
@ -2523,6 +2536,8 @@ class Contact
// Ensure to always have the correct network type, independent from the connection request method // Ensure to always have the correct network type, independent from the connection request method
self::updateFromProbe($contact['id']); self::updateFromProbe($contact['id']);
Post\UserNotification::insertNotication($contact['id'], Verb::getID(Activity::FOLLOW), $importer['uid']);
return true; return true;
} else { } else {
// send email notification to owner? // send email notification to owner?
@ -2554,6 +2569,8 @@ class Contact
self::updateAvatar($contact_id, $photo, true); 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]); $contact_record = DBA::selectFirst('contact', ['id', 'network', 'name', 'url', 'photo'], ['id' => $contact_id]);
/// @TODO Encapsulate this into a function/method /// @TODO Encapsulate this into a function/method

View file

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

View file

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

View file

@ -180,16 +180,18 @@ class Item
if (!empty($fields['body'])) { if (!empty($fields['body'])) {
Post\Media::insertFromAttachmentData($item['uri-id'], $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'])]; $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 // 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 // @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'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']);
$content_fields['raw-body'] = self::setHashtags($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'])) { if (!empty($fields['file'])) {
@ -991,14 +993,14 @@ class Item
Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']); 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 // 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'] = Post\Media::insertFromBody($item['uri-id'], $item['raw-body']);
$item['raw-body'] = self::setHashtags($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 // Check for hashtags in the body and repair or add hashtag links
$item['body'] = self::setHashtags($item['body']); $item['body'] = self::setHashtags($item['body']);
@ -1018,6 +1020,30 @@ class Item
if (empty($item['event-id'])) { if (empty($item['event-id'])) {
unset($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'])) { if (empty($item['causer-id'])) {
@ -1034,7 +1060,14 @@ class Item
Post\Content::insert($item['uri-id'], $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'])) { if (!empty($item['diaspora_signed_text'])) {
DBA::replace('diaspora-interaction', ['uri-id' => $item['uri-id'], 'interaction' => $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 $uri_id URI-ID of the given item
* @param integer $uid The user that will receive the item entry * @param integer $uid The user that will receive the item entry
* @param array $fields Additional fields to be stored * @param array $fields Additional fields to be stored
* @param integer $source_uid User id of the source post
* @return integer stored item id * @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 ($uid == $source_uid) {
if (!DBA::isResult($item)) { Logger::warning('target UID must not be be equal to the source UID', ['uri-id' => $uri_id, 'uid' => $uid]);
return 0; 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']]); 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; return 0;
} }
@ -1343,8 +1383,25 @@ class Item
$item = array_merge($item, $fields); $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); $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; return $stored;
} }
@ -1364,11 +1421,18 @@ class Item
} }
unset($item['id']); unset($item['id']);
unset($item['parent']);
unset($item['mention']); unset($item['mention']);
unset($item['starred']); unset($item['starred']);
unset($item['unseen']); unset($item['unseen']);
unset($item['psid']); 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['uid'] = $uid;
$item['origin'] = 0; $item['origin'] = 0;
@ -1394,8 +1458,6 @@ class Item
$item['contact-id'] = $self['id']; $item['contact-id'] = $self['id'];
} }
/// @todo Handling of "event-id"
$notify = false; $notify = false;
if ($item['gravity'] == GRAVITY_PARENT) { if ($item['gravity'] == GRAVITY_PARENT) {
$contact = DBA::selectFirst('contact', [], ['id' => $item['contact-id'], 'self' => false]); $contact = DBA::selectFirst('contact', [], ['id' => $item['contact-id'], 'self' => false]);
@ -1407,9 +1469,9 @@ class Item
$distributed = self::insert($item, $notify, true); $distributed = self::insert($item, $notify, true);
if (!$distributed) { 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 { } 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; return $distributed;
} }
@ -2739,9 +2801,10 @@ class Item
* *
* @param string $body * @param string $body
* @param string $url * @param string $url
* @param int $type
* @return bool * @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 // Make sure that for example site parameters aren't used when testing if the link is contained in the body
$urlparts = parse_url($url); $urlparts = parse_url($url);
@ -2749,6 +2812,12 @@ class Item
unset($urlparts['fragment']); unset($urlparts['fragment']);
$url = Network::unparseURL($urlparts); $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)) { if (strpos($body, $url)) {
return true; 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. // @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) { foreach ($attachments['visual'] as $attachment) {
if (self::containsLink($item['body'], $attachment['url'])) { if (self::containsLink($item['body'], $attachment['url'], $attachment['type'])) {
continue; continue;
} }
@ -2802,7 +2871,7 @@ class Item
'mime' => $attachment['mimetype'], 'mime' => $attachment['mimetype'],
], ],
]); ]);
if ($item['post-type'] == Item::PT_VIDEO) { if (($item['post-type'] ?? null) == Item::PT_VIDEO) {
$leading .= $media; $leading .= $media;
} else { } else {
$trailing .= $media; $trailing .= $media;
@ -2955,7 +3024,7 @@ class Item
// @todo Use a template // @todo Use a template
$rendered = BBCode::convertAttachment('', BBCode::INTERNAL, false, $data); $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'), [ $rendered = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/link.tpl'), [
'$url' => $data['url'], '$url' => $data['url'],
'$title' => $data['title'], '$title' => $data['title'],

View file

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

View file

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

View file

@ -351,7 +351,7 @@ class Media
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
// Only store attachments that are part of the unshared body // 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); self::insert($attachment);
} }
} }
@ -600,7 +600,7 @@ class Media
$body = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $body); $body = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $body);
foreach (self::getByURIId($uriid, [self::IMAGE, self::AUDIO, self::VIDEO]) as $media) { 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; continue;
} }

View file

@ -33,7 +33,7 @@ use Friendica\Model\Post;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Util\DateTimeFormat;
class UserNotification class UserNotification
{ {
@ -128,8 +128,8 @@ class UserNotification
*/ */
public static function setNotification(int $uri_id, int $uid) public static function setNotification(int $uri_id, int $uid)
{ {
$fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', $fields = ['id', 'uri-id', 'parent-uri-id', 'uid', 'body', 'parent', 'gravity', 'vid', 'gravity',
'private', 'contact-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'author-id', 'verb']; '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]); $item = Post::selectFirst($fields, ['uri-id' => $uri_id, 'uid' => $uid, 'origin' => false]);
if (!DBA::isResult($item)) { if (!DBA::isResult($item)) {
return; return;
@ -177,6 +177,10 @@ class UserNotification
if (self::checkShared($item, $uid)) { if (self::checkShared($item, $uid)) {
$notification_type = $notification_type | self::NOTIF_SHARED; $notification_type = $notification_type | self::NOTIF_SHARED;
self::insertNoticationByItem(self::NOTIF_SHARED, $uid, $item);
$notified = true;
} else {
$notified = false;
} }
$profiles = self::getProfileForUser($uid); $profiles = self::getProfileForUser($uid);
@ -194,38 +198,64 @@ class UserNotification
return; 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)) { if (self::checkExplicitMention($item, $profiles)) {
$notification_type = $notification_type | self::NOTIF_EXPLICIT_TAGGED; $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)) { if (self::checkImplicitMention($item, $profiles)) {
$notification_type = $notification_type | self::NOTIF_THREAD_COMMENT; $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)) { if (self::checkDirectComment($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_DIRECT_COMMENT; $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)) { if (self::checkDirectCommentedThread($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_DIRECT_THREAD_COMMENT; $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)) { if (self::checkCommentedParticipation($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_COMMENT_PARTICIPATION; $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)) { if (self::checkActivityParticipation($item, $contacts)) {
$notification_type = $notification_type | self::NOTIF_ACTIVITY_PARTICIPATION; $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; return;
} }
@ -236,6 +266,61 @@ class UserNotification
self::update($item['uri-id'], $uid, $fields, true); 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 * Fetch all profiles (contact URL) of a given user
* @param int $uid User ID * @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) 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)) { if (!DBA::isResult($user) && empty($profiledata)) {
Logger::log('profile error: ' . DI::args()->getQueryString(), Logger::DEBUG); Logger::log('profile error: ' . DI::args()->getQueryString(), Logger::DEBUG);
@ -263,8 +263,20 @@ class Profile
$o = ''; $o = '';
$location = false; $location = false;
// This function can also use contact information in $profile // This function can also use contact information in $profile, but the 'cid'
$is_contact = !empty($profile['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'])) { if (empty($profile['nickname'])) {
Logger::warning('Received profile with no nickname', ['profile' => $profile, 'callstack' => System::callstack(10)]); Logger::warning('Received profile with no nickname', ['profile' => $profile, 'callstack' => System::callstack(10)]);
@ -292,16 +304,12 @@ class Profile
$subscribe_feed_link = null; $subscribe_feed_link = null;
$wallmessage_link = null; $wallmessage_link = null;
// Who is the logged-in user to this profile?
$visitor_contact = []; $visitor_contact = [];
if (!empty($profile['uid']) && self::getMyURL()) { if (!empty($profile['uid']) && self::getMyURL()) {
$visitor_contact = Contact::selectFirst(['rel'], ['uid' => $profile['uid'], 'nurl' => Strings::normaliseLink(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_dfrn = $profile['network'] == Protocol::DFRN;
$profile_is_native = in_array($profile['network'], Protocol::NATIVE_SUPPORT); $profile_is_native = in_array($profile['network'], Protocol::NATIVE_SUPPORT);
$local_user_is_self = self::getMyURL() && ($profile['url'] == self::getMyURL()); $local_user_is_self = self::getMyURL() && ($profile['url'] == self::getMyURL());
@ -332,17 +340,19 @@ class Profile
$subscribe_feed_link = 'dfrn_poll/' . $profile['nickname']; $subscribe_feed_link = 'dfrn_poll/' . $profile['nickname'];
} }
if (Contact::canReceivePrivateMessages($profile)) { if (Contact::canReceivePrivateMessages($profile_contact)) {
if ($visitor_is_followed || $visitor_is_following) { 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'])) { } elseif ($visitor_is_authenticated && !empty($profile['unkmail'])) {
$wallmessage_link = 'wallmessage/' . $profile['nickname']; $wallmessage_link = 'wallmessage/' . $profile['nickname'];
} }
} }
} }
// show edit profile to yourself // show edit profile to yourself, but only if this is not meant to be
if (!$is_contact && $local_user_is_self) { // 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['edit'] = [DI::baseUrl() . '/settings/profile', DI::l10n()->t('Edit profile'), '', DI::l10n()->t('Edit profile')];
$profile['menu'] = [ $profile['menu'] = [
'chg_photo' => DI::l10n()->t('Change profile photo'), 'chg_photo' => DI::l10n()->t('Change profile photo'),

View file

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

View file

@ -21,8 +21,9 @@
namespace Friendica\Module\Api\Mastodon\Accounts; namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\Logger;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
use Friendica\Util\Network; use Friendica\Util\HTTPInputData;
/** /**
* @see https://docs.joinmastodon.org/methods/accounts/ * @see https://docs.joinmastodon.org/methods/accounts/
@ -34,9 +35,10 @@ class UpdateCredentials extends BaseApi
self::login(self::SCOPE_WRITE); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $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'); self::unsupported('patch');
} }
} }

View file

@ -50,7 +50,7 @@ class Apps extends BaseApi
if (!empty($postdata)) { if (!empty($postdata)) {
$postrequest = json_decode($postdata, true); $postrequest = json_decode($postdata, true);
if (!empty($postrequest) && is_array($postrequest)) { 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']]; $params = ['order' => ['uri-id' => true], 'limit' => $request['limit']];
$condition = ['pinned' => true, 'uid' => $uid]; $condition = ['starred' => true, 'uid' => $uid];
if (!empty($request['max_id'])) { if (!empty($request['max_id'])) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $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 = []) 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(); 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); self::login(self::SCOPE_WRITE);
$uid = self::getCurrentUserID(); $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'])) { if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity(); DI::mstdnError()->UnprocessableEntity();
@ -69,7 +74,7 @@ class Media extends BaseApi
DI::mstdnError()->RecordNotFound(); 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'])); System::jsonExit(DI::mstdnAttachment()->createFromPhoto($parameters['id']));
} }

View file

@ -25,8 +25,10 @@ use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Notification; use Friendica\Model\Post;
use Friendica\Model\Verb;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
use Friendica\Protocol\Activity;
/** /**
* @see https://docs.joinmastodon.org/methods/notifications/ * @see https://docs.joinmastodon.org/methods/notifications/
@ -44,10 +46,10 @@ class Notifications extends BaseApi
if (!empty($parameters['id'])) { if (!empty($parameters['id'])) {
$id = $parameters['id']; $id = $parameters['id'];
if (!DBA::exists('notify', ['id' => $id, 'uid' => $uid])) { if (!DBA::exists('notification', ['id' => $id, 'uid' => $uid])) {
DI::mstdnError()->RecordNotFound(); DI::mstdnError()->RecordNotFound();
} }
System::jsonExit(DI::mstdnNotification()->createFromNotifyId($id)); System::jsonExit(DI::mstdnNotification()->createFromNotificationId($id));
} }
$request = self::getRequest([ $request = self::getRequest([
@ -63,7 +65,7 @@ class Notifications extends BaseApi
$params = ['order' => ['id' => true], 'limit' => $request['limit']]; $params = ['order' => ['id' => true], 'limit' => $request['limit']];
$condition = ['uid' => $uid, 'seen' => false, 'type' => []]; $condition = ['uid' => $uid, 'seen' => false];
if (!empty($request['account_id'])) { if (!empty($request['account_id'])) {
$contact = Contact::getById($request['account_id'], ['url']); $contact = Contact::getById($request['account_id'], ['url']);
@ -72,17 +74,40 @@ class Notifications extends BaseApi
} }
} }
if (!in_array('follow_request', $request['exclude_types'])) { if (in_array('follow_request', $request['exclude_types'])) {
$condition['type'] = array_merge($condition['type'], [Notification\Type::INTRO]); $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'])) { if (in_array('follow', $request['exclude_types'])) {
$condition['type'] = array_merge($condition['type'], $condition = DBA::mergeConditions($condition,
[Notification\Type::WALL, Notification\Type::COMMENT, Notification\Type::MAIL, Notification\Type::TAG_SELF, Notification\Type::POKE]); ["(`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'])) { if (in_array('favourite', $request['exclude_types'])) {
$condition['type'] = array_merge($condition['type'], [Notification\Type::SHARE]); $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'])) { if (!empty($request['max_id'])) {
@ -101,9 +126,12 @@ class Notifications extends BaseApi
$notifications = []; $notifications = [];
$notify = DBA::select('notify', ['id'], $condition, $params); $notify = DBA::select('notification', ['id'], $condition, $params);
while ($notification = DBA::fetch($notify)) { 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'])) { if (!empty($request['min_id'])) {

View file

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

View file

@ -40,7 +40,7 @@ class Dismiss extends BaseApi
DI::mstdnError()->UnprocessableEntity(); 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([]); System::jsonExit([]);
} }

View file

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

View file

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

View file

@ -41,6 +41,8 @@ class PublicTimeline extends BaseApi
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
$uid = self::getCurrentUserID();
$request = self::getRequest([ $request = self::getRequest([
'local' => false, // Show only local statuses? Defaults to false. 'local' => false, // Show only local statuses? Defaults to false.
'remote' => false, // Show only remote 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']]; $params = ['order' => ['uri-id' => true], 'limit' => $request['limit']];
$condition = ['gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => Item::PUBLIC, $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']) { if ($request['local']) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]); $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]); $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 = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {

View file

@ -29,7 +29,7 @@ use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network; use Friendica\Util\HTTPInputData;
require_once __DIR__ . '/../../include/api.php'; require_once __DIR__ . '/../../include/api.php';
@ -129,7 +129,7 @@ class BaseApi extends BaseModule
public static function unsupported(string $method = 'all') public static function unsupported(string $method = 'all')
{ {
$path = DI::args()->getQueryString(); $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 = 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.'); $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); $errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
@ -141,26 +141,30 @@ class BaseApi extends BaseModule
* *
* @return array request data * @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 = []; $request = [];
foreach ($defaults as $parameter => $defaultvalue) { foreach ($defaults as $parameter => $defaultvalue) {
if (is_string($defaultvalue)) { if (is_string($defaultvalue)) {
$request[$parameter] = $_REQUEST[$parameter] ?? $defaultvalue; $request[$parameter] = $input[$parameter] ?? $defaultvalue;
} elseif (is_int($defaultvalue)) { } elseif (is_int($defaultvalue)) {
$request[$parameter] = (int)($_REQUEST[$parameter] ?? $defaultvalue); $request[$parameter] = (int)($input[$parameter] ?? $defaultvalue);
} elseif (is_float($defaultvalue)) { } elseif (is_float($defaultvalue)) {
$request[$parameter] = (float)($_REQUEST[$parameter] ?? $defaultvalue); $request[$parameter] = (float)($input[$parameter] ?? $defaultvalue);
} elseif (is_array($defaultvalue)) { } elseif (is_array($defaultvalue)) {
$request[$parameter] = $_REQUEST[$parameter] ?? []; $request[$parameter] = $input[$parameter] ?? [];
} elseif (is_bool($defaultvalue)) { } elseif (is_bool($defaultvalue)) {
$request[$parameter] = in_array(strtolower($_REQUEST[$parameter] ?? ''), ['true', '1']); $request[$parameter] = in_array(strtolower($input[$parameter] ?? ''), ['true', '1']);
} else { } else {
Logger::notice('Unhandled default value type', ['parameter' => $parameter, 'type' => gettype($defaultvalue)]); Logger::notice('Unhandled default value type', ['parameter' => $parameter, 'type' => gettype($defaultvalue)]);
} }
} }
foreach ($_REQUEST ?? [] as $parameter => $value) { foreach ($input ?? [] as $parameter => $value) {
if ($parameter == 'pagename') { if ($parameter == 'pagename') {
continue; continue;
} }
@ -173,45 +177,6 @@ class BaseApi extends BaseModule
return $request; 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. * 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\Database\DBA;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\Strings;
/** /**
* Performs an activity (like, dislike, announce, attendyes, attendno, attendmaybe) * 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']; $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]]); $item = Post::selectFirst($fields, ['id' => $itemId, 'private' => [Item::PUBLIC, Item::UNLISTED]]);
if (!DBA::isResult($item) || ($item['body'] == '')) { if (!DBA::isResult($item)) {
return; return;
} }

View file

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

View file

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

View file

@ -70,7 +70,7 @@ class HTTPRequest implements IHTTPRequest
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @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); $stamp1 = microtime(true);
@ -222,7 +222,7 @@ class HTTPRequest implements IHTTPRequest
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @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); $stamp1 = microtime(true);
@ -447,7 +447,7 @@ class HTTPRequest implements IHTTPRequest
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @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); $ret = $this->fetchFull($url, $timeout, $accept_content, $cookiejar, $redirects);
@ -461,7 +461,7 @@ class HTTPRequest implements IHTTPRequest
* *
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @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( return $this->get(
$url, $url,

View file

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

View file

@ -52,7 +52,7 @@ class Notification extends BaseDataTransferObject
{ {
$this->id = (string)$id; $this->id = (string)$id;
$this->type = $type; $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(); $this->account = $account->toArray();
if (!empty($status)) { 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) 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->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) { if ($item['gravity'] == GRAVITY_COMMENT) {
$this->in_reply_to_id = (string)$item['thr-parent-id']; $this->in_reply_to_id = (string)$item['thr-parent-id'];
@ -114,7 +114,12 @@ class Status extends BaseDataTransferObject
$this->visibility = $visibility[$item['private']]; $this->visibility = $visibility[$item['private']];
$languages = json_decode($item['language'], true); $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->uri = $item['uri'];
$this->url = $item['plink'] ?? null; $this->url = $item['plink'] ?? null;

View file

@ -53,6 +53,6 @@ class Token extends BaseDataTransferObject
$this->access_token = $access_token; $this->access_token = $access_token;
$this->token_type = $token_type; $this->token_type = $token_type;
$this->scope = $scope; $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 // 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'), $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'))]; 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 { } else {
$remote_comment = ''; $remote_comment = '';
} }

View file

@ -602,6 +602,12 @@ class Processor
continue; 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)) { if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
$skip = !Contact::isSharingByURL($activity['author'], $receiver); $skip = !Contact::isSharingByURL($activity['author'], $receiver);

View file

@ -748,10 +748,6 @@ class Transmitter
$contacts = DBA::select('contact', ['id', 'url', 'network', 'protocol', 'gsid'], $condition); $contacts = DBA::select('contact', ['id', 'url', 'network', 'protocol', 'gsid'], $condition);
while ($contact = DBA::fetch($contacts)) { while ($contact = DBA::fetch($contacts)) {
if (Contact::isLocal($contact['url'])) {
continue;
}
if (!self::isAPContact($contact, $networks)) { if (!self::isAPContact($contact, $networks)) {
continue; continue;
} }
@ -766,7 +762,7 @@ class Transmitter
$profile = APContact::getByURL($contact['url'], false); $profile = APContact::getByURL($contact['url'], false);
if (!empty($profile)) { if (!empty($profile)) {
if (empty($profile['sharedinbox']) || $personal) { if (empty($profile['sharedinbox']) || $personal || Contact::isLocal($contact['url'])) {
$target = $profile['inbox']; $target = $profile['inbox'];
} else { } else {
$target = $profile['sharedinbox']; $target = $profile['sharedinbox'];
@ -829,15 +825,11 @@ class Transmitter
if ($item_profile && ($receiver == $item_profile['followers']) && ($uid == $profile_uid)) { if ($item_profile && ($receiver == $item_profile['followers']) && ($uid == $profile_uid)) {
$inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal, self::isAPPost($last_id))); $inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal, self::isAPPost($last_id)));
} else { } else {
if (Contact::isLocal($receiver)) {
continue;
}
$profile = APContact::getByURL($receiver, false); $profile = APContact::getByURL($receiver, false);
if (!empty($profile)) { if (!empty($profile)) {
$contact = Contact::getByURLForUser($receiver, $uid, false, ['id']); $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']; $target = $profile['inbox'];
} else { } else {
$target = $profile['sharedinbox']; $target = $profile['sharedinbox'];
@ -1525,12 +1517,21 @@ class Transmitter
if ($type == 'Note') { if ($type == 'Note') {
$body = $item['raw-body'] ?? self::removePictures($body); $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')) { if (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) {
$body = self::prependMentions($body, $item['uri-id'], $item['author-link']); $body = self::prependMentions($body, $item['uri-id'], $item['author-link']);
} }

View file

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

View file

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

View file

@ -34,6 +34,7 @@ class DateTimeFormat
const ATOM = 'Y-m-d\TH:i:s\Z'; const ATOM = 'Y-m-d\TH:i:s\Z';
const MYSQL = 'Y-m-d H:i:s'; const MYSQL = 'Y-m-d H:i:s';
const HTTP = 'D, d M Y H:i:s \G\M\T'; 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. * 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); $profile = APContact::getByURL($url);
if (!empty($profile)) { 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']]; return ['url' => $url, 'pubkey' => $profile['pubkey'], 'type' => $profile['type']];
} elseif ($url != $actor) { } elseif ($url != $actor) {
$profile = APContact::getByURL($actor); $profile = APContact::getByURL($actor);
if (!empty($profile)) { 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']]; return ['url' => $actor, 'pubkey' => $profile['pubkey'], 'type' => $profile['type']];
} }
} }
Logger::notice('Key could not be fetched', ['url' => $url, 'actor' => $actor]);
return false; return false;
} }
} }

View file

@ -548,4 +548,15 @@ class Network
exit; 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\Diaspora;
use Friendica\Protocol\Email; use Friendica\Protocol\Email;
use Friendica\Protocol\Activity; use Friendica\Protocol\Activity;
use Friendica\Util\Strings;
use Friendica\Util\Network; use Friendica\Util\Network;
use Friendica\Core\Worker; use Friendica\Core\Worker;
use Friendica\Model\Conversation; use Friendica\Model\Conversation;
use Friendica\Model\FContact; use Friendica\Model\FContact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Protocol\Relay; use Friendica\Protocol\Relay;
class Delivery class Delivery
@ -216,11 +214,6 @@ class Delivery
$contact['network'] = Protocol::DIASPORA; $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']]); Logger::notice('Delivering', ['cmd' => $cmd, 'uri-id' => $post_uriid, 'followup' => $followup, 'network' => $contact['network']]);
switch ($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); 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; $protocol = Model\Post\DeliveryData::DFRN;
// We don't have a relationship with contacts on a public post. // 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'); Logger::notice('Adding missing entries');
$rows = 0; $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)) { while ($fields = DBA::fetch($userposts)) {
$post_fields = DBStructure::getFieldsForTable('post', $fields); $post_fields = DBStructure::getFieldsForTable('post', $fields);
DBA::insert('post', $post_fields, Database::INSERT_IGNORE); DBA::insert('post', $post_fields, Database::INSERT_IGNORE);
@ -133,7 +133,7 @@ class ExpirePosts
} }
$rows = 0; $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)) { while ($fields = DBA::fetch($userposts)) {
$post_fields = DBStructure::getFieldsForTable('post-thread', $fields); $post_fields = DBStructure::getFieldsForTable('post-thread', $fields);
$post_fields['commented'] = $post_fields['changed'] = $post_fields['created']; $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\OStatus;
use Friendica\Protocol\Relay; use Friendica\Protocol\Relay;
use Friendica\Protocol\Salmon; use Friendica\Protocol\Salmon;
use Friendica\Util\Network;
/* /*
* The notifier is typically called with: * The notifier is typically called with:
@ -514,9 +515,12 @@ class Notifier
$delivery_queue_count = 0; $delivery_queue_count = 0;
foreach ($contacts as $contact) { foreach ($contacts as $contact) {
// Ensure that local contacts are delivered via DFRN // Direct delivery of local contacts
if (Contact::isLocal($contact['url'])) { if ($target_uid = User::getIdForURL($contact['url'])) {
$contact['network'] = Protocol::DFRN; 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. // Deletions are always sent via DFRN as well.
@ -775,6 +779,16 @@ class Notifier
foreach ($inboxes as $inbox => $receivers) { foreach ($inboxes as $inbox => $receivers) {
$contacts = array_merge($contacts, $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]); Logger::info('Delivery via ActivityPub', ['cmd' => $cmd, 'id' => $target_item['id'], 'inbox' => $inbox]);
if (Worker::add(['priority' => $priority, 'created' => $created, 'dont_fork' => true], if (Worker::add(['priority' => $priority, 'created' => $created, 'dont_fork' => true],

View file

@ -55,7 +55,7 @@
use Friendica\Database\DBA; use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) { if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1419); define('DB_UPDATE_VERSION', 1421);
} }
return [ return [
@ -882,6 +882,29 @@ return [
"mid" => ["mid"], "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" => [ "notify" => [
"comment" => "notifications", "comment" => "notifications",
"fields" => [ "fields" => [

View file

@ -196,6 +196,8 @@
"parent-author-link" => ["parent-post-author", "url"], "parent-author-link" => ["parent-post-author", "url"],
"parent-author-name" => ["parent-post-author", "name"], "parent-author-name" => ["parent-post-author", "name"],
"parent-author-network" => ["parent-post-author", "network"], "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` "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` 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); StaticDatabase::statConnect($_SERVER);
// Rollbacks every DB usage (in case the test couldn't call tearDown) // Rollbacks every DB usage (in case the test couldn't call tearDown)
StaticDatabase::statRollback(); StaticDatabase::statRollback();
// Rollback the first, outer transaction just 2 be sure
StaticDatabase::getGlobConnection()->rollBack();
// Start the first, outer transaction // Start the first, outer transaction
StaticDatabase::getGlobConnection()->beginTransaction(); StaticDatabase::getGlobConnection()->beginTransaction();
} }

View file

@ -69,7 +69,7 @@ class ExtendedPDO extends PDO
{ {
if($this->_transactionDepth <= 0 || !$this->hasSavepoint()) { if($this->_transactionDepth <= 0 || !$this->hasSavepoint()) {
parent::beginTransaction(); parent::beginTransaction();
$this->_transactionDepth = $this->_transactionDepth < 0 ? 0 : $this->_transactionDepth; $this->_transactionDepth = 0;
} else { } else {
$this->exec("SAVEPOINT LEVEL{$this->_transactionDepth}"); $this->exec("SAVEPOINT LEVEL{$this->_transactionDepth}");
} }
@ -84,15 +84,16 @@ class ExtendedPDO extends PDO
*/ */
public function commit() 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--; $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}"); $this->exec("RELEASE SAVEPOINT LEVEL{$this->_transactionDepth}");
} }
}
/** /**
* Rollback current transaction, * Rollback current transaction,
@ -102,14 +103,15 @@ class ExtendedPDO extends PDO
*/ */
public function rollBack() public function rollBack()
{ {
if ($this->_transactionDepth <= 0) {
throw new PDOException('Rollback error : There is no transaction started');
}
$this->_transactionDepth--; $this->_transactionDepth--;
if($this->_transactionDepth == 0 || !$this->hasSavepoint()) { if($this->_transactionDepth <= 0 || !$this->hasSavepoint()) {
$this->_transactionDepth = 0;
try {
parent::rollBack(); parent::rollBack();
} catch (PDOException $e) {
// this shouldn't happen, but it does ...
}
} else { } else {
$this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->_transactionDepth}"); $this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->_transactionDepth}");
} }

View file

@ -39,6 +39,9 @@ class StaticDatabase extends Database
*/ */
private static $staticConnection; private static $staticConnection;
/** @var bool */
private $_locked = false;
/** /**
* Override the behaviour of connect, due there is just one, static connection at all * Override the behaviour of connect, due there is just one, static connection at all
* *
@ -77,6 +80,31 @@ class StaticDatabase extends Database
return true; 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 * Does a commit
* *
@ -163,18 +191,6 @@ class StaticDatabase extends Database
return self::$staticConnection; 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 * 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 = '') private function assertXml($result = '', $root_element = '')
{ {
self::assertStringStartsWith('<?xml version="1.0"?>', $result); self::assertStringStartsWith('<?xml version="1.0"?>', $result);
self::assertContains('<' . $root_element, $result); self::assertStringContainsString('<' . $root_element, $result);
// We could probably do more checks here. // We could probably do more checks here.
} }
@ -1505,7 +1505,7 @@ class ApiTest extends FixtureTest
$result = api_search('json'); $result = api_search('json');
foreach ($result['status'] as $status) { foreach ($result['status'] as $status) {
self::assertStatus($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'); $result = api_search('json');
foreach ($result['status'] as $status) { foreach ($result['status'] as $status) {
self::assertStatus($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'); $result = api_search('json');
foreach ($result['status'] as $status) { foreach ($result['status'] as $status) {
self::assertStatus($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'); $result = api_search('json');
foreach ($result['status'] as $status) { foreach ($result['status'] as $status) {
self::assertStatus($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', 'network' => 'feed',
'title' => 'item_title', 'title' => 'item_title',
'uri-id' => 1,
// We need a long string to test that it is correctly cut // We need a long string to test that it is correctly cut
'body' => 'perspiciatis impedit voluptatem quis molestiae ea qui ' . 'body' => 'perspiciatis impedit voluptatem quis molestiae ea qui ' .
'reiciendis dolorum aut ducimus sunt consequatur inventore dolor ' . 'reiciendis dolorum aut ducimus sunt consequatur inventore dolor ' .
@ -2308,11 +2309,12 @@ class ApiTest extends FixtureTest
[ [
'network' => 'feed', 'network' => 'feed',
'title' => 'item_title', 'title' => 'item_title',
'uri-id' => -1,
'body' => '', 'body' => '',
'plink' => 'item_plink' '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']); self::assertEquals('<h4>item_title</h4><br>item_plink', $result['html']);
} }
@ -2326,7 +2328,8 @@ class ApiTest extends FixtureTest
$result = api_convert_item( $result = api_convert_item(
[ [
'title' => 'item_title', 'title' => 'item_title',
'body' => 'item_title item_body' 'body' => 'item_title item_body',
'uri-id' => 1,
] ]
); );
self::assertEquals('item_title item_body', $result['text']); self::assertEquals('item_title item_body', $result['text']);
@ -2874,7 +2877,7 @@ class ApiTest extends FixtureTest
$_POST['text'] = 'message_text'; $_POST['text'] = 'message_text';
$_POST['screen_name'] = $this->friendUser['nick']; $_POST['screen_name'] = $this->friendUser['nick'];
$result = api_direct_messages_new('json'); $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('selfcontact', $result['direct_message']['sender_screen_name']);
self::assertEquals(1, $result['direct_message']['friendica_seen']); self::assertEquals(1, $result['direct_message']['friendica_seen']);
} }
@ -2891,8 +2894,8 @@ class ApiTest extends FixtureTest
$_POST['screen_name'] = $this->friendUser['nick']; $_POST['screen_name'] = $this->friendUser['nick'];
$_REQUEST['title'] = 'message_title'; $_REQUEST['title'] = 'message_title';
$result = api_direct_messages_new('json'); $result = api_direct_messages_new('json');
self::assertContains('message_text', $result['direct_message']['text']); self::assertStringContainsString('message_text', $result['direct_message']['text']);
self::assertContains('message_title', $result['direct_message']['text']); self::assertStringContainsString('message_title', $result['direct_message']['text']);
self::assertEquals('selfcontact', $result['direct_message']['sender_screen_name']); self::assertEquals('selfcontact', $result['direct_message']['sender_screen_name']);
self::assertEquals(1, $result['direct_message']['friendica_seen']); self::assertEquals(1, $result['direct_message']['friendica_seen']);
} }

View file

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

View file

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

View file

@ -81,7 +81,7 @@ class FilesystemStorageTest extends StorageTest
public function testMissingDirPermissions() public function testMissingDirPermissions()
{ {
$this->expectException(StorageException::class); $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); $this->root->getChild('storage')->chmod(000);
$instance = $this->getInstance(); $instance = $this->getInstance();
@ -97,7 +97,7 @@ class FilesystemStorageTest extends StorageTest
static::markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure"); static::markTestIncomplete("Cannot catch file_put_content() error due vfsStream failure");
$this->expectException(StorageException::class); $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); vfsStream::create(['storage' => ['f0' => ['c0' => ['k0i0' => '']]]], $this->root);

View file

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

View file

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

View file

@ -78,16 +78,16 @@ class EMailerTest extends MockedTest
self::assertTrue($emailer->send($testEmail)); self::assertTrue($emailer->send($testEmail));
self::assertContains("X-Friendica-Host: friendica.local", EmailerSpy::$MAIL_DATA['headers']); self::assertStringContainsString("X-Friendica-Host: friendica.local", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("X-Friendica-Platform: Friendica", EmailerSpy::$MAIL_DATA['headers']); self::assertStringContainsString("X-Friendica-Platform: Friendica", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("List-ID: <notification.friendica.local>", EmailerSpy::$MAIL_DATA['headers']); self::assertStringContainsString("List-ID: <notification.friendica.local>", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("List-Archive: <http://friendica.local/notifications/system>", EmailerSpy::$MAIL_DATA['headers']); self::assertStringContainsString("List-Archive: <http://friendica.local/notifications/system>", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("Reply-To: Sender <sender@friendica.local>", EmailerSpy::$MAIL_DATA['headers']); self::assertStringContainsString("Reply-To: Sender <sender@friendica.local>", EmailerSpy::$MAIL_DATA['headers']);
self::assertContains("MIME-Version: 1.0", EmailerSpy::$MAIL_DATA['headers']); self::assertStringContainsString("MIME-Version: 1.0", EmailerSpy::$MAIL_DATA['headers']);
// Base64 "Test Text" // 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>" // 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("Test Subject", EmailerSpy::$MAIL_DATA['subject']);
self::assertEquals("recipient@friendica.local", EmailerSpy::$MAIL_DATA['to']); self::assertEquals("recipient@friendica.local", EmailerSpy::$MAIL_DATA['to']);
self::assertEquals("-f sender@friendica.local", EmailerSpy::$MAIL_DATA['parameters']); 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->emergency('A {psr} test', ['psr' => 'working']);
$logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]); $logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]);
$text = $this->getContent(); $text = $this->getContent();
self::assertContains('A working test', $text); self::assertStringContainsString('A working test', $text);
self::assertContains('An ["it","is","working"] test', $text); self::assertStringContainsString('An ["it","is","working"] test', $text);
} }
/** /**
@ -119,9 +119,9 @@ abstract class AbstractLoggerTest extends MockedTest
$logger->emergency('A test'); $logger->emergency('A test');
$text = $this->getContent(); $text = $this->getContent();
self::assertContains('"file":"' . self::FILE . '"', $text); self::assertStringContainsString('"file":"' . self::FILE . '"', $text);
self::assertContains('"line":' . self::LINE, $text); self::assertStringContainsString('"line":' . self::LINE, $text);
self::assertContains('"function":"' . self::FUNC . '"', $text); self::assertStringContainsString('"function":"' . self::FUNC . '"', $text);
} }
/** /**
@ -157,7 +157,7 @@ abstract class AbstractLoggerTest extends MockedTest
self::assertLogline($text); 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::assertLogline($text);
self::assertContains(@json_encode($assertion), $this->getContent()); self::assertStringContainsString(@json_encode($assertion), $this->getContent());
} }
public function testNoObjectHandling() public function testNoObjectHandling()
@ -187,6 +187,6 @@ abstract class AbstractLoggerTest extends MockedTest
self::assertLogline($text); 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() public function testWrongUrl()
{ {
$this->expectException(\UnexpectedValueException::class); $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') $logfile = vfsStream::newFile('friendica.log')
->at($this->root)->chmod(0); ->at($this->root)->chmod(0);
@ -144,7 +144,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongDir() public function testWrongDir()
{ {
$this->expectException(\UnexpectedValueException::class); $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'); static::markTestIncomplete('We need a platform independent way to set directory to readonly');
@ -159,7 +159,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongMinimumLevel() public function testWrongMinimumLevel()
{ {
$this->expectException(\InvalidArgumentException::class); $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'); $logger = new StreamLogger('test', 'file.text', $this->introspection, $this->fileSystem, 'NOPE');
} }
@ -170,7 +170,7 @@ class StreamLoggerTest extends AbstractLoggerTest
public function testWrongLogLevel() public function testWrongLogLevel()
{ {
$this->expectException(\InvalidArgumentException::class); $this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessageRegExp("/The level \".*\" is not valid./"); $this->expectExceptionMessageMatches("/The level \".*\" is not valid./");
$logfile = vfsStream::newFile('friendica.log') $logfile = vfsStream::newFile('friendica.log')
->at($this->root); ->at($this->root);

View file

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