Compare commits

..

306 commits

Author SHA1 Message Date
Michael Vogel
f86b6a1569
Merge pull request #15609 from Raroun/fix-fbrowser-switcher
Fix: File browser mode switch requiring double click
2026-03-24 12:18:32 +01:00
“Raroun”
90b94e955a Fix file browser mode switch requiring double click 2026-03-24 10:39:40 +01:00
Philipp
5bf6a3d68e
Merge pull request #15602 from annando/issue-15598
Issue 15598: Handle missing function "exif_read_data"
2026-03-22 08:20:48 +01:00
c98c4957b4 Fix code standards 2026-03-22 02:52:31 +00:00
Philipp
796c207d36
Merge pull request #15603 from annando/warning
Fixes: E_WARNING: Undefined array key "register_date"
2026-03-21 21:21:06 +01:00
41a3f2edca Fixes: E_WARNING: Undefined array key "register_date" 2026-03-21 09:06:31 +00:00
fc60c12d6f Issue 15598: Handle missing function "exif_read_data" 2026-03-21 08:23:26 +00:00
Michael Vogel
5174702158
Merge pull request #15594 from Art4/fix-release-names
fix: replace non-existing release 2025.07 with 2026.01
2026-03-14 19:25:47 +01:00
Art4
da46df0af2 chore: fix code style 2026-03-14 15:22:10 +01:00
Art4
fd96f4bccd fix: replace non-existing release 2025.07 with 2026.01 2026-03-14 07:52:23 +01:00
b509656d00
Merge pull request #15585 from annando/exif
Store EXIF data for media
2026-03-13 06:50:05 +01:00
Michael Vogel
d724428e22
Merge pull request #15592 from Raroun/fix/queue-overflow
Fix text overflow in admin queue page
2026-03-12 13:51:29 +01:00
“Raroun”
dab7a606f8 fix coding standards 2026-03-12 12:14:03 +01:00
Michael Vogel
a4989795aa
Merge pull request #14960 from marcusxss/profile_menu_always_show_manage_accounts
Always show 'Accounts' in the profile menu
2026-03-12 11:40:13 +01:00
Michael Vogel
3c010cd958
Merge pull request #15582 from loma-one/develop
Refactor image link generation in BBCode
2026-03-12 11:37:59 +01:00
loma-one
1e26082fcc
Fix indentation for is_photo variable assignment 2026-03-12 10:37:52 +01:00
loma-one
2ac2000a11
Fix link attributes assignment for photo previews 2026-03-12 10:30:06 +01:00
loma-one
ecd432c413
Fix link attributes formatting in BBCode.php 2026-03-12 09:45:36 +01:00
Philipp
47fa9484bd
Merge pull request #15576 from Art4/update-codebase
Update codebase to PHP 7.4
2026-03-12 08:22:22 +01:00
17f2c209e5
Merge pull request #15591 from annando/countries
Fix for country.js: united states is added
2026-03-12 07:30:12 +01:00
Philipp
0ccf21e35a
Merge pull request #15549 from marcusxss/move_profile_edit_to_vcard
Move profile edit to vcard
2026-03-11 20:20:47 +01:00
Michael Vogel
0928001386
Merge pull request #15581 from Raroun/js/jot-modernize
Modernize jot.js
2026-03-11 20:20:41 +01:00
Michael Vogel
aad5bf2aaa
Merge pull request #15590 from Raroun/frio/remove-box-shadow-mobile
FRIO: Remove box shadow from action buttons in frio dark theme mobile view
2026-03-11 20:15:02 +01:00
70b4eebc87 Fix for country.js: united states is added 2026-03-11 18:01:49 +00:00
“Raroun”
b0b72d7cad Remove box shadow from action buttons in dark theme mobile view 2026-03-11 18:46:08 +01:00
Michael Vogel
3bf96ff1f0
Merge pull request #15589 from Tealk/theme/frio/gnome/mod
fix moderation/users/pending
2026-03-11 18:15:13 +01:00
Tealk
351a6afe44
fix moderation/users/pending 2026-03-11 13:46:56 +01:00
Philipp
d5a9a79da4
Merge pull request #15586 from annando/fix-counts
Fix PR #15584 with counts
2026-03-09 01:25:03 +01:00
4bf0626d3c Fix PR #15584 with counts 2026-03-08 21:19:30 +00:00
7e7ceb671a Fix code style 2026-03-08 16:00:29 +00:00
ba9943fe21 Store EXIF data for media 2026-03-08 15:42:40 +00:00
Philipp
74c77a8847
Merge pull request #15584 from annando/verb-counts
Improve performance on post counts
2026-03-08 16:34:16 +01:00
4958bf56e3 Fix code style 2026-03-08 13:25:33 +00:00
0d29f03bcc Improve performance on post counts 2026-03-08 13:22:04 +00:00
Philipp
93ac2986e5
Merge pull request #15583 from annando/missing-handle
Handle warnings because of missing handle information
2026-03-07 18:44:00 +01:00
263d452bbc Handle warnings because of missing handle information 2026-03-07 13:20:37 +00:00
loma-one
c78d5adb40
Refactor image link generation in BBCode
Previously, images embedded in continuous text were opened with the attribute `target=’_blank‘`. However, they should be loaded and displayed in the Fancybox.

As a result of the change, images are now loaded in the Fancybox. Links and videos are identified in order to maintain the previous functionality.
2026-03-06 21:20:54 +01:00
Philipp
f652cd26c0
Merge pull request #15568 from annando/fix-infinite-scroll
Infinite scrolling is fixed in general and on contacts
2026-03-06 07:24:03 +01:00
Philipp
d136daf4a0
Merge pull request #15577 from Art4/set-max-php-version
Require PHP 7.4 to 8.5 in composer.json
2026-03-06 07:21:31 +01:00
Philipp
8431b24e90
Merge pull request #15578 from annando/threads
Fix detection for threads.net
2026-03-06 07:19:34 +01:00
Philipp
2b596087ab
Merge pull request #15551 from annando/lostpass
"lostpass" is now a module as well
2026-03-06 07:19:17 +01:00
6e28128f77
Merge pull request #15579 from Alkarex/patch-1
Fix installation documentation link in README
2026-03-06 06:52:39 +01:00
“Raroun”
c4b2ffa66c Modernize jot.js
- Fix implicit global variables (reply, noAttachment)
- Add feature detection for aStr, bin2hex, autosize
- Add error handling for AJAX requests
- Wrap in IIFE with strict mode
- Add JSDoc documentation
2026-03-05 12:03:26 +01:00
Alexandre Alapetite
49f7e17292
Fix installation documentation link in README
Previous link was broken
2026-03-05 10:29:24 +01:00
00979edfee Added phpdoc 2026-03-05 07:28:59 +00:00
07cf5b5d30 Changes because of review 2026-03-05 07:25:49 +00:00
8c869cdcf4 Change after review 2026-03-05 07:24:27 +00:00
f749b099f5 Fix code style 2026-03-05 05:25:18 +00:00
074d352cb9 Fix detection for threads.net 2026-03-05 05:22:52 +00:00
Art4
d498dc8907 require php 7.4 to 8.5 2026-03-04 22:04:41 +00:00
Art4
ea3c576b22 chore: fix code style 2026-03-04 21:53:29 +00:00
Art4
64af3c6b8a refactor: update codebase to PHP 7.4 2026-03-04 21:52:35 +00:00
Art4
df7e15d1df update rector config to PHP 7.4 2026-03-04 21:52:09 +00:00
Art4
47ef274176 Update codebase to PHP 7.0 2026-03-04 21:26:00 +00:00
Art4
dc30924759 update rector for PHP 7.0 2026-03-04 21:23:50 +00:00
Art4
6b782ba617 update downgrade level to php 8.2 2026-03-04 20:20:25 +00:00
Art4
6e71306e9b update rector and phpstan 2026-03-04 20:15:03 +00:00
Marcus F.
494d919c25 Move profile edit to vcard 2026-03-04 20:06:55 +01:00
Philipp
08f505c866
Merge pull request #15575 from annando/no-photo-item
Don't create item containers for photos
2026-03-04 18:29:19 +01:00
299bb864d8 Don't create item containers for photos 2026-03-03 22:35:53 +00:00
Michael Vogel
7660a94e7d
Merge pull request #15559 from Raroun/js/modal-compose-modernization
Modernize compose.js and modal.js with improved architecture and bug fixing
2026-03-03 15:39:07 +01:00
Michael Vogel
f0f9e45076
Merge pull request #15572 from marcusxss/directory_no_results
Directory: No results from sysmsg to page
2026-03-02 22:24:44 +01:00
Marcus F.
1402509351 Directory: No results from sysmsg to page 2026-03-02 22:09:12 +01:00
d9b28f86a7 Changed git branch 2026-03-02 20:59:36 +00:00
49243ddf5b "lostpass" is now a module as well 2026-03-02 17:06:54 +00:00
309f22bf75 Fix code style 2026-03-02 17:06:15 +00:00
d8efe9f891 Infinite scrolling is fixed in general and on contacts 2026-03-02 17:06:14 +00:00
44e1f32ca8
Merge pull request #14019 from annando/edit-summary
The "summary" has now a dedicated field when creating and editing posts
2026-03-02 12:01:11 -05:00
3a5c3522e2 The "summary" has now a dedicated field when creating and editing posts 2026-03-02 16:26:00 +00:00
Michael Vogel
b905ecfa37
Merge pull request #15563 from marcusxss/directory_show_count_users_listed
Directory: Show the number of users listed (results)
2026-03-01 22:45:35 +01:00
Michael Vogel
872f0d7a50
Merge pull request #15565 from marcusxss/format_date_joined_member_since
Update the date format for member since/joined
2026-03-01 08:59:35 +01:00
Philipp
dbde77b1fe
Merge pull request #15561 from annando/leap-year
Allow entries for birthdays on 29 February
2026-02-28 23:14:21 +01:00
Marcus F.
e8c8fa4315 Update the date format for member since/joined 2026-02-28 20:39:17 +01:00
Marcus F.
17781c7193 Directory: Show the number of users listed (results) 2026-02-28 16:29:33 +01:00
24a1792ccc Fix code standards 2026-02-28 14:40:08 +00:00
fa6dfd98c5 Allow to enter birthdays on February 29th 2026-02-28 14:33:12 +00:00
2c5cbbe36e
Merge pull request #15560 from annando/more-localisation
More localisated dates
2026-02-28 15:31:55 +01:00
dae56f198f Fix codestyle 2026-02-28 12:01:39 +00:00
2ca162c69e More localisated dates 2026-02-28 11:53:50 +00:00
“Raroun”
d6067d3466 Modernize compose.js and modal.js with improved architecture and bug fixes
compose.js:
- Add draft autosave to sessionStorage (body, location, scheduled_at)
- Restore draft when returning to compose page
- Clear form after successful post submission
- Fix geolocation operator precedence bug
- Fix button staying disabled after geolocation error
- Add sessionStorage availability check for private browsing
- Wrap in IIFE with 'use strict'
- Add JSDoc documentation
- Use namespaced events (.compose)

modal.js:
- Fix critical double event registration bug in editpost()
- Add isJotResetBound flag to ensure single handler binding
- Add isEditJotClosing guard for race condition protection
- Wrap in IIFE with 'use strict'
- Add feature detection for all external dependencies
- Use namespaced events (.frio, .frio-edit)
- Add JSDoc documentation
2026-02-27 13:23:03 +01:00
27f4c93407
Merge pull request #15556 from annando/issue-15553
Issue 15553: Display dates in a language depending format
2026-02-27 10:32:28 +01:00
397aecb71d
Merge pull request #15557 from annando/any-scope
API: verify_credentials can be called with any scope
2026-02-27 10:23:38 +01:00
Philipp
14c96fe2e2
Merge pull request #15543 from Art4/update-codebase-to-php-5.5
Update codebase to PHP 5.5
2026-02-27 00:56:31 +01:00
Philipp
0350f60d0a
Merge pull request #15558 from annando/no-salmon
No salmon anymore
2026-02-27 00:53:30 +01:00
1bab29c9cc No salmon anymore 2026-02-26 22:40:34 +00:00
Art4
52ee7f782c chore: fix code style 2026-02-26 20:49:05 +00:00
Art4
f5db74a33c refactor: run ClassConstantToSelfClassRector and StringClassNameToClassConstantRector 2026-02-26 20:49:05 +00:00
Art4
260e4875bd chore: update rector to codebase for PHP 5.5 2026-02-26 20:49:05 +00:00
Michael Vogel
3152caf169
Merge pull request #15539 from Art4/update-codebase-to-php-5.4
Update codebase to php 5.4
2026-02-26 14:34:05 +01:00
61e804b277 API: verify_credentials can be called with any scope 2026-02-25 21:47:55 +00:00
8f908f6c72 Fix code style 2026-02-25 07:01:30 +00:00
6461d2766e Issue 15553: Display dates in a language depending format 2026-02-25 06:46:35 +00:00
Philipp
fc76e52220
Merge pull request #15554 from Tealk/theme/frio/gnome/codeblock
fix codeblock colore
2026-02-24 23:12:46 +01:00
Tealk
eb16e5b29f
fix codeblock colore 2026-02-24 20:09:11 +01:00
Philipp
574a8eeb36
Merge pull request #15552 from annando/db-logging
Additional database logging when inserting data
2026-02-24 08:22:04 +01:00
6f14bc22d2 Fix code standards 2026-02-24 06:51:05 +00:00
28d8d7a77d Additional database logging when inserting data 2026-02-24 06:39:09 +00:00
Philipp
fdc512ca49
Merge pull request #15547 from Art4/review-composer-dependencies
Move dev libraries into project composer.json
2026-02-23 16:04:45 +01:00
Art4
bf141d4437 chore: fix code style 2026-02-23 11:08:45 +00:00
Art4
4a2fa925e1 Merge branch 'develop' into review-composer-dependencies 2026-02-23 11:05:52 +00:00
Philipp
615537cd56
Merge pull request #15534 from annando/modules
Move some more modules into classes
2026-02-23 10:39:14 +01:00
4267a04f44 Changes after review 2026-02-23 09:25:50 +00:00
960b5c0861 Move some more modules into classes 2026-02-23 09:25:49 +00:00
Michael Vogel
e6712edc4d
Merge pull request #15550 from marcusxss/vcard_member_since
Show "member since" in vcard, if enabled
2026-02-23 04:41:35 +01:00
Marcus F.
bf2bc053fe Show member since in vcard, if enabled 2026-02-22 22:38:35 +01:00
Philipp
3af81486ef
Merge pull request #15542 from randompenguin1/develop
Updated instruction text for sortable lists
2026-02-22 16:45:44 +01:00
Art4
6c9602fd3f fix: fix phpstan errors 2026-02-22 15:03:14 +00:00
Art4
b9605576a6 chore: update phpstan and php-mock with all dependencies 2026-02-22 14:51:51 +00:00
Art4
1534a4f970 chore: update PHPStan to 2.1.39 2026-02-22 14:41:35 +00:00
Art4
7ad038b6e5 chore: fix composer scripts, change all php-cs-fixer and rector paths 2026-02-22 14:33:06 +00:00
Marcus F.
1219306f09 Update messages, code style 2026-02-22 15:12:59 +01:00
Art4
4675921b58 chore: remove rector and php-cs-fixer from bin/dev folder 2026-02-22 13:53:41 +00:00
Art4
f11874f5d1 chore: install rector in project comoser.json 2026-02-22 13:47:22 +00:00
Art4
56e28e1ef0 chore: install php-cs-fixer in project comoser.json 2026-02-22 13:39:18 +00:00
Art4
83c94df5d9 chore: set required php version to ^7.4 || ^8.0 2026-02-22 13:26:41 +00:00
Random Penguin
0d14145f04 removed extra single quote 2026-02-22 12:49:10 +01:00
Random Penguin
6183ff7e7e Updated instruction text for sortable list 2026-02-22 12:49:10 +01:00
Random Penguin
1aac361383 Updated instruction text for sortable list 2026-02-22 12:49:10 +01:00
Philipp
0275227dd4
Merge pull request #15533 from annando/no-photo-post
Item post functionality is removed from photos
2026-02-22 01:47:31 +01:00
Michael Vogel
3ca8483442
Update mod/photos.php
Co-authored-by: Philipp <admin+Github@philipp.info>
2026-02-21 23:40:41 +01:00
Philipp
b93978dc87
Merge pull request #15541 from randompenguin1/patch-1
fixes for sortable lists
2026-02-21 20:58:17 +01:00
Philipp
0f39e099aa
Merge pull request #15544 from Tealk/theme/frio/gnome/form-control
fix disabled & readonly form-control
2026-02-21 20:56:55 +01:00
Art4
ac506f11b0 Merge branch 'develop' into update-codebase-to-php-5.4 2026-02-21 12:57:09 +00:00
Philipp
7e832c5792
Merge pull request #15538 from Art4/review-code-style-rules
Review code style rules, change spaces_inside_parentheses rule
2026-02-21 13:13:27 +01:00
Tealk
d6ba958f76
fix disabled & readonly form-control 2026-02-21 10:45:58 +01:00
Random Penguin
03a9defaac
fixes for sortable lists
Removed mobile drag-n-drop script that did not work
Added button interface for moving list items
Fixed duplication error for keyboard control
Added animation to make it clear which way item moved
2026-02-20 14:29:56 -06:00
Art4
d940868cba fix: revert changes in .rector.php 2026-02-20 13:36:34 +00:00
Art4
54e3d89b96 chore: fix code style 2026-02-20 12:21:06 +00:00
Art4
e6240dbbf9 refactor: switch to PHP 7.4 compatible syntax without named arguments 2026-02-20 12:17:09 +00:00
Art4
26c5207e18 refactor: upgrade rector php level to 9 2026-02-20 12:07:40 +00:00
Art4
114139718e refactor: run LongArrayToShortArrayRector 2026-02-20 12:02:26 +00:00
Art4
3742414c94 refactor: activate TernaryToElvisRector 2026-02-20 11:59:54 +00:00
Art4
95929d08c7 chore: fix code style for PHP 7.4 2026-02-20 11:12:21 +00:00
Art4
d137b625b5 chore: add trailing_comma_in_multiline rule with PHP 7.4 compatible rule 2026-02-20 11:09:27 +00:00
Art4
ff1b44ffde chore: add the php-cs-fixer.dist.php itself and let it fix the code style 2026-02-20 10:56:34 +00:00
Art4
d1a235e8df chore: fix code style 2026-02-20 08:52:49 +00:00
Art4
45ce04ea2d chore: fix code style with spaces_inside_parentheses rule 2026-02-20 08:28:32 +00:00
Art4
5636b052ae chore: remove spaces_inside_parentheses rule 2026-02-20 08:27:41 +00:00
Art4
52257b2a6a chore: remove php-cs-fixer rules already defined in PER-CS3.0 ruleset 2026-02-20 08:14:03 +00:00
Art4
2f06247e58 chore: update PSR12 to PER-CS3x0 ruleset 2026-02-20 07:42:29 +00:00
Art4
57f1ca6135 chore: replace deprecated visibility_required rule with modifier_keywords 2026-02-20 07:30:20 +00:00
Art4
c2bc9b4eb2 chore: set rector downgrade sets to PHP 7.4 2026-02-20 07:30:20 +00:00
Art4
f39281ce29 chore: set indent to tabs for rector 2026-02-20 07:30:20 +00:00
Art4
004a443b39 chore: remove @PSR1 and @PSR2 ruleset
they are part of the @PSR12 ruleset
2026-02-20 07:30:20 +00:00
Michael Vogel
70c9cfc3dc
Merge pull request #15536 from Tealk/theme/frio/gnome/colorbox
fix colorbox theme
2026-02-20 05:16:21 +01:00
Michael Vogel
44a1795e72
Merge pull request #14989 from Art4/destruct-list-to-array
Destruct list() to array [Rector]
2026-02-20 04:57:18 +01:00
Michael Vogel
7d7e1463d6
Merge pull request #15537 from Art4/fix-php-7.4-support
fix: downgrade str_ends_with() for PHP 7.4 support
2026-02-20 04:03:01 +01:00
Art4
284ef0c894 fix: downgrade str_ends_with() for PHP 7.4 support 2026-02-19 21:50:50 +00:00
Artur Weigandt
5bbe83e561
Apply suggestions from code review 2026-02-19 22:07:28 +01:00
Tealk
937e61a3ca
fix colorbox theme 2026-02-19 21:57:09 +01:00
Art4
70ae8fc47f chore: fix code style 2026-02-19 20:23:35 +00:00
Art4
a778b6d54d chore: set php version for rector to PHP 7.4 2026-02-19 20:23:20 +00:00
c4ca959e58 Item post functionality is removed from photos 2026-02-18 21:30:56 +00:00
Michael Vogel
786c9545d6
Merge pull request #15532 from Raroun/register-closed-info
Enhancement: Add friendly message when registration is closed
2026-02-18 20:25:19 +01:00
“Raroun”
2d82f98706 Add friendly message when registration is closed 2026-02-18 15:23:03 +01:00
Philipp
7dd86151be
Merge pull request #15530 from annando/database
Fix Database issue / log failing database queries
2026-02-18 10:29:53 +01:00
3d0109cfb8 Fix Database issue / log failing database queries 2026-02-18 09:13:24 +00:00
cd8d322fec
Merge pull request #15527 from marcusxss/messages_sidebar_hide_empty
Hide the messages sidebar when its empty (no messages)
2026-02-18 07:10:14 +01:00
Marcus F.
eefef09cb9 Hide the messages sidebar when it's empty 2026-02-17 20:21:21 +01:00
Philipp
fb7d9c8e12
Merge pull request #15526 from annando/uncaught
Fix some "Uncaught exception in worker method execution"
2026-02-17 18:46:18 +01:00
Philipp
b2ac6f2dc9
Merge pull request #15524 from Raroun/album-upload-fix
Photo upload to album doesn't ask for album anymore when already viewing one / Fix for Redirect to empty Page when album is deleted
2026-02-17 17:23:29 +01:00
0b8f110d75 Fix some "Uncaught exception in worker method execution" 2026-02-17 12:34:45 +00:00
“Raroun”
0ea6eca324 fix for #15520 2026-02-17 11:25:47 +01:00
“Raroun”
a4d7e1ea10 Add translation strings for album upload 2026-02-17 11:09:27 +01:00
“Raroun”
c7d03c3ff7 Pre-fill album when uploading from album view 2026-02-17 11:05:38 +01:00
Philipp
796da0bb24
Merge pull request #15522 from annando/quote
Fetch missing quoted post asynchronously
2026-02-17 01:01:37 +01:00
Michael Vogel
72b29c5fe5
Merge pull request #15436 from Raroun/circle-mark-as-read
Add "Mark all as read" button to circles
2026-02-17 00:35:21 +01:00
7a7654aba4 Fetch missing quoted post asynchronously 2026-02-16 08:54:13 +00:00
Raroun
604dc7538b
Merge branch 'develop' into circle-mark-as-read 2026-02-16 09:02:35 +01:00
“Raroun”
170bcb4df8 Use DBA::update() instead of raw SQL for marking circle posts as read 2026-02-16 08:59:01 +01:00
Philipp
88530a317c
Merge pull request #15521 from marcusxss/photos_updates_2
Small UI/UX updates to some photos and admin pages
2026-02-15 22:51:30 +01:00
Philipp
3e12c54002
Merge pull request #15519 from annando/vanished-quotes
Additional logging for issue 15395
2026-02-15 22:47:44 +01:00
3b7015b3e0 Additional logging for issue 15395 2026-02-15 19:37:15 +00:00
Marcus F.
4bb2f2c89a Photos updates 2026-02-15 19:49:06 +01:00
7f8492b0e4
Merge pull request #15518 from annando/permissions
Don't copy permissions when permissions are already defined
2026-02-15 09:40:04 +01:00
697df94788 Don't copy permissions when permissions are already defined 2026-02-15 08:14:37 +00:00
Raroun
1d752fc839
Merge branch 'develop' into circle-mark-as-read 2026-02-14 16:35:18 +01:00
“Raroun”
7cc7a441e5 updated messages.po 2026-02-14 16:31:15 +01:00
“Raroun”
5ef83b1845 fixed code standards 2026-02-14 16:13:38 +01:00
Raroun
90b389db71 Add mark all as read button to circles 2026-02-14 16:13:38 +01:00
Philipp
9c50d7cb84
Merge pull request #15515 from annando/mentions
Don't set "mention" on view / don't display channels on "mention" timeline
2026-02-14 11:50:14 +01:00
Philipp
a0b9d5de9a
Merge pull request #15514 from annando/database
Fix: Prevent not nullable fields set to null
2026-02-14 11:49:49 +01:00
Philipp
4f2506adb8
Merge pull request #15512 from marcusxss/admin_logs_minor_adjustments
Admin logs rename settings page, minor ui adjustments
2026-02-14 10:58:51 +01:00
Art4
00537d4347 downgrade php-cs-fixer dependencies for PHP 7.4 2026-02-14 09:53:05 +01:00
Art4
1281ae82c6 split up composer scripts for rector:run and rectify 2026-02-14 09:47:10 +01:00
Art4
6977042f36 update php-cs-fixer to run it on PHP 8.4 2026-02-14 09:43:44 +01:00
Art4
024eb04c1a fix caching problem devcontainer 2026-02-14 09:42:01 +01:00
Art4
17829955be let REUSE ignore the composer files in rector install folder 2026-02-14 08:33:11 +01:00
Art4
7bb3ca5371 Add license note in rector config file 2026-02-14 08:33:11 +01:00
Art4
b43946a219 Rename rector config file 2026-02-14 08:33:11 +01:00
Art4
4f84589b55 Fix code style 2026-02-14 08:33:09 +01:00
Art4
fb6c810107 destruct list to array 2026-02-14 08:31:35 +01:00
Art4
ffb72bfdab Add refactor command, fix code style 2026-02-14 08:28:18 +01:00
Art4
af1750938b Add rector config and check command 2026-02-14 08:28:18 +01:00
Art4
3ead048693 install rector as standalone dev tool 2026-02-14 08:28:18 +01:00
0820648545 Don't set "mention" on view / don't display channels on "mention" timeline 2026-02-14 05:14:17 +00:00
6a59c57790 Fix: Prevent not nullable fields set to null 2026-02-14 04:11:11 +00:00
Marcus Funch
da9e699885 Admin logs rename settings page, minor ui adjustments 2026-02-13 23:06:46 +01:00
acfc731e60
Merge pull request #15508 from nupplaphil/ci/releaser-allinone
[CI] add all-in-one package for Release
2026-02-13 06:24:34 +01:00
06d67ed783
Merge pull request #15511 from annando/view2
Don't process "View"
2026-02-13 06:17:30 +01:00
003a54a429 Don't process "View" 2026-02-13 04:40:20 +00:00
Philipp
acd0cd45ab
Merge pull request #15505 from marcusxss/terminology_photos_settings
Adjustments to some strings around photos and events
2026-02-13 00:58:34 +01:00
Philipp
54e198e8a4
Merge pull request #15509 from annando/no-view
Don't transmit "View" activities via Diaspora
2026-02-12 23:34:03 +01:00
Marcus F.
265614db2d Fix CS 2026-02-12 23:21:00 +01:00
Marcus F.
c310fec830 Adjustments to some strings around photos and events 2026-02-12 23:20:58 +01:00
Philipp
1de1b43840
Merge pull request #15510 from marcusxss/fix_bug_photos
Fix bug in #15486 - photo/file browser
2026-02-12 23:16:23 +01:00
Philipp
8f9a5bfa7e
Merge pull request #15506 from marcusxss/frio_fix_login_form_bg
Frio: Make login form background color respect dark theme
2026-02-12 23:15:55 +01:00
Philipp
39a39928bf
Merge pull request #15484 from marcusxss/jot_compose_styling
Compose styling, larger BBCode buttons
2026-02-12 23:14:25 +01:00
27e256c7e3 Don't transmit "View" activities via Diaspora 2026-02-12 22:10:24 +00:00
Marcus F.
fa6d82363a Fix bug in #15486 - photo/file browser 2026-02-12 23:07:05 +01:00
Marcus F.
65bb4b2751 Frio: Make login form background color respect dark theme 2026-02-12 21:55:07 +01:00
44be322a97
[CI] add all-in-one package for Release 2026-02-12 19:33:45 +01:00
Marcus F.
2a919dfca0 Compose styling, larger BBCode buttons 2026-02-12 18:07:12 +01:00
Michael Vogel
9e60b13150
Merge pull request #15402 from Tealk/feature/gnome_scheme
add gnome colored sheme to frio
2026-02-12 15:36:36 +01:00
Michael Vogel
b520b0af27
Merge pull request #15507 from Raroun/fix-channel-pagination
Fix pagination by using array_key_first/last in Channel and Network module
2026-02-12 11:32:04 +01:00
Michael Vogel
fb741fbb07
Merge pull request #15451 from marcusxss/link_global_directory_language
Make link from contacts to global directory pass on user language
2026-02-12 10:41:08 +01:00
“Raroun”
adfc15207a Fix pagination by using array_key_first/last in Channel and Network modules 2026-02-12 10:16:56 +01:00
94590db51a
Merge pull request #15500 from annando/gotosocial
Respect robots.txt when fetching server data
2026-02-12 08:37:59 +01:00
7e0d4f1f70
Merge pull request #15486 from marcusxss/photos_file_browser
Photos - File browser: Larger buttons to switch file types, add text
2026-02-12 08:09:48 +01:00
Michael Vogel
47033abc0a
Merge pull request #15488 from samuel-asleep/develop
Fix event participation responses not delivered to event organizers
2026-02-12 06:51:02 +01:00
Akinniranye Samuel Tomiwa
b84719f1c9
Add explanatory comment for event participation addressing 2026-02-12 06:45:55 +01:00
Michael Vogel
df69b8dbf0
Merge pull request #15504 from marcusxss/html_remove_size_attrs
Remove size attrs from templates, except select/option
2026-02-12 06:34:14 +01:00
Akinniranye Samuel Tomiwa
837ca52679
Refactor event participation addressing to preserve followers notification 2026-02-12 06:33:58 +01:00
Marcus Funch
0871e3c520 Remove size attrs from templates, except select/option 2026-02-11 13:41:27 +01:00
Marcus F.
545a58df58 Photos - File browser: Larger buttons to switch file types, add text 2026-02-11 10:15:17 +01:00
f3caad0b14 Respect robots.txt when fetching server data 2026-02-11 07:50:24 +00:00
1414dcb0a2
Merge pull request #15369 from marcusxss/user_menu_cleanup
Simplify the UI: User menu redundancy
2026-02-11 07:28:17 +01:00
Marcus F.
339a40eda0 Remove media/media posts from the user menu to simplify it 2026-02-11 00:33:37 +01:00
Marcus Funch
04572cd25b User menu cleanup 2026-02-11 00:33:37 +01:00
879f4d133f
Merge pull request #15496 from annando/quote-fix
Add quoted post as attachment when it couldn't be fetched
2026-02-10 07:18:51 +01:00
66b4fc4992
Merge pull request #15495 from annando/issue-15478
Issue 15478: Prevent comments when blocked or is blocked
2026-02-10 07:17:02 +01:00
3392dbd032
Merge pull request #15493 from annando/issue-14583
Issue 14583: Avoid empty entries in list of trending links
2026-02-10 07:14:00 +01:00
09b9dc7903
Merge pull request #15499 from annando/database
Two slow database queries fixed
2026-02-10 07:04:24 +01:00
60823c07fe Two slow database queries fixed 2026-02-09 21:02:35 +00:00
bf3e2c306f Add quoted post as attachment when it couldn't be fetched 2026-02-08 16:48:10 +00:00
a40e2f3bee Issue 15478: Prevent comments when blocked or is blocked 2026-02-08 16:39:37 +00:00
Michael Vogel
7ea03b0e22
Merge pull request #15482 from marcusxss/frio_theme_text_changes
Update the text on Frio's config page
2026-02-08 15:00:25 +01:00
dddca40d75 Issue 14583: Avoid empty entries in list of trending links 2026-02-08 13:49:24 +00:00
14e1bce96f
Merge pull request #15494 from annando/issue-15456
Issue 15456: Fix login to Bridgy Fed
2026-02-08 14:14:55 +01:00
Marcus Funch
05da451545 Update the text on Frio's config page 2026-02-08 14:12:17 +01:00
e647dcf54e Issue 15456: Fix login to Bridgy Fed 2026-02-08 11:54:51 +00:00
db90f543fe
Merge pull request #15489 from annando/view
Store "View" activity as "seen" marker
2026-02-07 20:01:12 +01:00
f901252c2a Store "View" activity as "seen" marker. 2026-02-07 18:42:56 +00:00
86c24d7ff9
Merge pull request #15490 from annando/numeric
Ensure numeric content in numeric fields
2026-02-07 19:17:59 +01:00
b720ff8d08
Merge pull request #15491 from annando/circles
Fix for empty circles
2026-02-07 19:16:50 +01:00
0a3c537be5 Fix for empty circles 2026-02-07 17:47:32 +00:00
a91c94d196
Merge pull request #15378 from marcusxss/frio_highlight_current_section
Add current section highlighting to Frio's navbar, fix related logic
2026-02-07 18:37:59 +01:00
6fec466bcc Ensure numeric content in numeric fields 2026-02-07 17:35:30 +00:00
Akinniranye Samuel Tomiwa
0a58198ac2
Fix event participation responses not delivered to event organizers 2026-02-07 17:12:53 +01:00
Marcus F.
5555478b30 Make contact link to global directory pass on user language 2026-02-06 12:06:26 +01:00
Marcus Funch
8043016e0e Fix missing link to calendar in Vier, and add calendar highlighting 2026-02-06 11:46:00 +01:00
Marcus Funch
b99652d8c5 Add current page highlighting to frio, fix related logic 2026-02-06 11:44:32 +01:00
aa07454d15
Merge pull request #15466 from loma-one/loma-one-patch-1
Optimization of the editor toolbar and enhancement of the preview funktion
2026-02-06 06:36:24 +01:00
b272292b41
Merge pull request #15472 from annando/warning
Fixes:  Undefined array key "filter_channels"
2026-02-05 07:26:18 +01:00
4bc958973a
Merge pull request #15474 from Espionage724/patch-1
Update console.php (minor typo)
2026-02-05 07:25:38 +01:00
Sean Rhone
4218af5a1e
Update console.php
- Even shorter (script itself is named console)
2026-02-04 22:09:55 -05:00
Sean Rhone
c74180b96a
Update console.php (minor typo)
- Shortened a tiny bit too
2026-02-04 22:05:48 -05:00
540e046cd9 Fixes: Undefined array key "filter_channels" 2026-02-04 15:57:30 +00:00
loma-one
489ebb257e
Remove unnecessary placeholder in compose template
Remove unnecessary placeholder in compose template
2026-02-04 16:40:57 +01:00
Michael Vogel
eeb3f266d7
Merge pull request #15469 from tobiasd/20260204-de
fix missing space in DE translation
2026-02-04 15:18:02 +01:00
270a9a7c7a update DE translation
fix missing space that @annando discovered
2026-02-04 11:44:32 +01:00
657271b857
Merge pull request #15468 from annando/loadhtml
Fixes "DOMDocument::loadHTML(): Argument #1 ($source) must not be empty"
2026-02-04 08:21:37 +01:00
c9e65db83b
Merge pull request #15467 from annando/fix-null
Fixes "Argument #3 ($author_id) must be of type int, null given
2026-02-04 08:20:23 +01:00
3c36efd4c5 fix missing space in DE translation
closes #15464
2026-02-04 08:16:55 +01:00
acc195116d Fixes "DOMDocument::loadHTML(): Argument #1 ($source) must not be empty" 2026-02-03 20:57:54 +00:00
cda1925aff Fixes "Argument #3 ($author_id) must be of type int, null given 2026-02-03 20:51:01 +00:00
loma-one
a250969e11
Optimization of the editor toolbar and enhancement of the preview function
This pull request implements structural improvements to the editor (compose) to enhance usability and visual clarity:

Toolbar Reorganization: Buttons have been grouped into logical Bootstrap btn-group units. Media buttons (image, attachment) are now positioned on the left (pull-left), while formatting options and special features (Emoji, CW, Code) are arranged in two distinct groups on the right (pull-right).

Enhanced Preview Logic: Added a toggle functionality to the preview button. When the preview is active, the button text changes to "Close Preview" (or local equivalent) to provide a more intuitive user experience.
2026-02-03 20:37:03 +01:00
d65c7e7d23
Merge pull request #15448 from annando/network-channel
Channel posts can now be embedded into the timeline
2026-02-03 20:13:54 +01:00
d6b6dd1b94 Channel posts can now be embedded into the timeline 2026-02-03 12:08:36 +00:00
635511588d
Merge pull request #15449 from annando/no-quote-check
Quote check is removed
2026-02-02 14:11:57 -05:00
Michael Vogel
766493bedf
Merge pull request #15428 from mediaformat/c2s-inbox-cors
C2S: Access-Control-Allow-Origin header for already authorized users
2026-01-31 16:28:28 +01:00
Michael Vogel
1ccb0e2579
Merge pull request #15368 from marcusxss/ui_admin_mod_summaries
Adjustments to the admin and moderation summaries (mostly Frio)
2026-01-29 02:54:17 +01:00
Michael Vogel
1db196ad71
Merge pull request #15440 from marcusxss/personal_notifications_always_show_all
Always show all personal notifications, remove related link to toggle
2026-01-29 02:33:58 +01:00
8d64ef115f Quote check is removed 2026-01-28 11:19:27 +00:00
Philipp
4c4d9954ab
Merge pull request #15446 from annando/adduserchannel
Check to store the user defined channel cache for all posts
2026-01-28 07:53:08 +01:00
Philipp
d80f6c3f81
Merge pull request #15447 from tobiasd/20260128-bug-smartytemplateerror
Fix: syntax error in SMARTY template
2026-01-28 07:51:05 +01:00
8c27953d48 Fix: syntax error in SMARTY template
When viewing a contact in Friendica 2026.04-dev I encountered a syntax error in the `contact_edit` template. Adding the missing `fetchoutbox` fixes it and seems what was supposed to be there without digging deeper into the calling PHP files.
2026-01-28 06:31:32 +01:00
4603e9c5d5 Check to store the user defined channel cache for all posts 2026-01-27 11:53:05 +00:00
5c0e024f03 bump version 2026.04-dev 2026-01-27 09:01:00 +01:00
eb48c7888c added 2026.04 to CHANGELOG 2026-01-27 09:00:29 +01:00
d842a21388 Merge branch 'stable' into develop 2026-01-27 08:56:40 +01:00
Marcus F.
a01fb9cb66 Switch approach to change BaseNotifications instead 2026-01-25 16:48:50 +01:00
Marcus F.
99b497a0c3 Always show all personal notifications, remove related link to toggle 2026-01-22 21:32:40 +01:00
Django
0cd582fddd
Remove unnecessary line in Inbox.php 2026-01-20 20:26:04 -07:00
Django
864f400c66
Merge branch 'friendica:2025.07-rc' into c2s-inbox-cors 2026-01-20 20:24:58 -07:00
Django
a64e8e20d5
Include getPublicInbox in CORS header for inbox response
getPublicInbox seems to correspond to the sharedInbox endpoint, so this is worth including for c2s clients
2026-01-18 17:07:58 -07:00
Django
1a6ddffe9c
Merge branch 'friendica:2025.07-rc' into c2s-inbox-cors 2026-01-18 17:04:34 -07:00
Tealk
ccb54a3801
fix eventcard colore 2026-01-18 11:37:12 +01:00
Tealk
02c19adfdc
remove space 2026-01-18 11:37:02 +01:00
Django
c71f81549b
Add Access-Control-Allow-Origin header
Added CORS header to allow requests from any origin.
2026-01-18 01:30:49 -07:00
Tealk
c6104eac12
some light link fixes 2026-01-15 17:00:47 +01:00
Tealk
ffe9d06104
remove space 2026-01-12 11:36:45 +01:00
Tealk
fc566bdccf
fix code_standards_check 2026-01-11 23:20:20 +01:00
Tealk
c9c129aa49
add Licence to css file 2026-01-11 23:17:26 +01:00
Tealk
92eedc70bf
add gnome colored sheme to frio 2026-01-11 22:59:38 +01:00
Marcus Funch
90bfc38e0a UI adjustments to the admin and moderation summaries 2026-01-06 15:56:11 +01:00
Philipp
1cfa88bb90
Merge pull request #15231 from Art4/update-devcontainer
Update devcontainer to PHP 8.4
2025-10-29 06:59:45 +01:00
Artur Weigandt
3ad5031e09
Update .devcontainer/Dockerfile 2025-10-28 20:22:04 +01:00
Art4
046c9770fa install pinentry-curses in devcontainer 2025-10-27 21:56:32 +01:00
Art4
7322431178 update devcontainer to php 8.4, update pecl packages 2025-10-27 21:50:10 +01:00
Art4
eea2fd4f70 Update docker-compose.yml 2025-10-27 21:22:38 +01:00
Marcus Funch
f508cbb79d Always show 'Accounts' in the profile menu 2025-10-11 20:23:35 +02:00
Michael Vogel
e3b2f8289b
Merge pull request #15230 from Art4/merge-2025.07-rc-into-develop
Merge 2025.07-rc into develop
2025-10-11 11:45:23 +02:00
Art4
7f95b5f8b1 Switch to image php:8.3-apache, fix code style 2025-10-11 10:58:20 +02:00
Art4
73d4642489 change Dockerfile to use php image 2025-10-11 10:42:26 +02:00
Art4
333c649000 Merge branch '2025.07-rc' into merge-2025.07-rc-into-develop 2025-10-11 09:35:04 +02:00
348 changed files with 12912 additions and 12467 deletions

View file

@ -1,26 +1,29 @@
ARG VARIANT="8.2-apache-bullseye" FROM php:8.4-apache
FROM mcr.microsoft.com/vscode/devcontainers/php:${VARIANT}
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG apcu_version=5.1.23
ARG memcached_version=3.2.0
ARG redis_version=6.0.2
ARG imagick_version=3.7.0
RUN apt-get update -y;
# Install MariaDB client # Install MariaDB client
RUN apt-get install -y mariadb-client RUN apt-get update \
&& apt-get install -y --no-install-recommends mariadb-client \
;
# Base packages # Base packages
RUN apt install -y vim software-properties-common sudo nano gnupg2 RUN apt-get install -y \
vim \
sudo \
nano \
git \
gnupg2 \
pinentry-curses \
;
# entrypoint.sh and cron.sh dependencies # entrypoint.sh and cron.sh dependencies
RUN apt-get install -y --no-install-recommends \ RUN apt-get install -y --no-install-recommends \
rsync \ rsync \
bzip2 \ bzip2 \
msmtp \ msmtp \
tini tini \
;
RUN apt-get install -y --no-install-recommends \ RUN apt-get install -y --no-install-recommends \
bash \ bash \
@ -38,36 +41,40 @@ RUN apt-get install -y --no-install-recommends \
libzip-dev \ libzip-dev \
libldap2-dev \ libldap2-dev \
libgmp-dev \ libgmp-dev \
libmagickcore-6.q16-6-extra \ # libmagickcore-6.q16-6-extra \
; \ ;
\
docker-php-ext-configure gd \ RUN docker-php-ext-configure gd \
--with-freetype \ --with-freetype \
--with-jpeg \ --with-jpeg \
--with-webp \ --with-webp \
; \ ;
docker-php-ext-install -j "$(nproc)" \
pdo_mysql \ RUN docker-php-ext-install -j "$(nproc)" \
gd \ pdo_mysql \
exif \ gd \
zip \ exif \
opcache \ zip \
ctype \ opcache \
pcntl \ ctype \
ldap \ pcntl \
gmp \ ldap \
intl gmp \
intl \
;
# pecl will claim success even if one install fails, so we need to perform each install separately # pecl will claim success even if one install fails, so we need to perform each install separately
RUN pecl install apcu-${apcu_version}; \ RUN pecl install apcu-5.1.27;
pecl install memcached-${memcached_version}; \ RUN pecl install memcached-3.4.0;
pecl install redis-${redis_version}; \ RUN pecl install redis-6.2.0;
pecl install imagick-${imagick_version}; \ RUN pecl install imagick-3.8.0;
docker-php-ext-enable \
apcu \ RUN docker-php-ext-enable \
memcached \ apcu \
redis \ memcached \
imagick redis \
imagick \
;
RUN apt-get clean -y && rm -rf /var/lib/apt/lists/* RUN apt-get clean -y && rm -rf /var/lib/apt/lists/*
@ -89,8 +96,8 @@ RUN { \
echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \
echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \
echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \
} > /usr/local/etc/php/conf.d/friendica.ini; \ } > /usr/local/etc/php/conf.d/friendica.ini;
ln -s /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini; \
\ RUN ln -s /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
mkdir /var/www/data; \ RUN mkdir /var/www/data
chmod -R g=u /var/www RUN chmod -R g=u /var/www

View file

@ -1,6 +1,4 @@
version: '3.8' services:
services:
app: app:
build: build:
context: . context: .
@ -14,16 +12,14 @@ services:
command: sleep infinity command: sleep infinity
ports: ports:
- 80:80 - ${ServerPort:-8080}:80
- 443:443 - 8443:443
- 8080:8080
- 3306:3306 # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.) # (Adding the "ports" property to this file will not forward from a Codespace.)
extra_hosts: extra_hosts:
- "${ServerAlias}:127.0.0.1" - "${ServerAlias}:127.0.0.1"
db: db:
image: mariadb:10.4 image: mariadb:10.4
@ -49,4 +45,3 @@ volumes:
networks: networks:
default: default:

View file

@ -35,14 +35,14 @@ jobs:
tools: none tools: none
- name: Clone addon repository - name: Clone addon repository
run: git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
- name: Install PHP-CS-Fixer - name: Install Composer dependencies
run: composer install --working-dir=bin/dev/php-cs-fixer uses: "ramsey/composer-install@v2"
- name: Run PHP-CS-Fixer - name: Run PHP-CS-Fixer
continue-on-error: true continue-on-error: true
run: bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff --dry-run run: vendor/bin/php-cs-fixer fix --diff --dry-run
phpstan: phpstan:
name: PHPStan (PHP ${{ matrix.php }}) name: PHPStan (PHP ${{ matrix.php }})
@ -68,7 +68,7 @@ jobs:
tools: none tools: none
- name: Clone addon repository - name: Clone addon repository
run: git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
- name: Install Composer dependencies - name: Install Composer dependencies
uses: "ramsey/composer-install@v2" uses: "ramsey/composer-install@v2"
@ -100,7 +100,7 @@ jobs:
tools: none tools: none
- name: Clone addon repository - name: Clone addon repository
run: git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
- name: Install Composer dependencies - name: Install Composer dependencies
uses: "ramsey/composer-install@v2" uses: "ramsey/composer-install@v2"
@ -132,7 +132,7 @@ jobs:
tools: none tools: none
- name: Clone addon repository - name: Clone addon repository
run: git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
- name: Install Composer dependencies - name: Install Composer dependencies
uses: "ramsey/composer-install@v2" uses: "ramsey/composer-install@v2"

View file

@ -34,7 +34,7 @@ jobs:
tools: none tools: none
- name: Clone addon repository - name: Clone addon repository
run: git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
- name: Install Composer dependencies - name: Install Composer dependencies
uses: "ramsey/composer-install@v2" uses: "ramsey/composer-install@v2"
@ -88,7 +88,7 @@ jobs:
ini-values: apc.enabled=1, apc.enable_cli=1 ini-values: apc.enabled=1, apc.enable_cli=1
- name: Clone addon repository - name: Clone addon repository
run: git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon run: git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
# Install composer dependencies and handle caching in one go. # Install composer dependencies and handle caching in one go.
# @link https://github.com/marketplace/actions/install-php-dependencies-with-composer # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer

View file

@ -1,4 +1,5 @@
<?php <?php
/** /**
* SPDX-FileCopyrightText: 2010 - 2024 the Friendica project * SPDX-FileCopyrightText: 2010 - 2024 the Friendica project
* *
@ -7,34 +8,32 @@
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/bin/dev/php-cs-fixer/vendor/autoload.php';
$finder = PhpCsFixer\Finder::create() $finder = PhpCsFixer\Finder::create()
->in(__DIR__) ->in(__DIR__)
->notPath('addon') ->exclude([
->notPath('bin/dev') 'addon',
->notPath('config') 'bin/dev',
->notPath('doc') 'config',
->notPath('images') 'doc',
->notPath('mods') 'images',
->notPath('spec') 'mods',
->notPath('vendor') 'spec',
->notPath('view/asset') 'vendor',
->notPath('lang') 'view/asset',
->notPath('view/smarty3/compiled'); 'lang',
'view/smarty3/compiled',
])
->append([
'.php-cs-fixer.dist.php',
])
;
$config = new PhpCsFixer\Config(); $config = new PhpCsFixer\Config();
return $config return $config
->setRules([ ->setRules([
'@PSR1' => true, '@PER-CS3x0' => true,
'@PSR2' => true,
'@PSR12' => true,
'align_multiline_comment' => true, 'align_multiline_comment' => true,
'array_indentation' => true, 'binary_operator_spaces' => [
'array_syntax' => [
'syntax' => 'short',
],
'binary_operator_spaces' => [
'default' => 'single_space', 'default' => 'single_space',
'operators' => [ 'operators' => [
'=>' => 'align_single_space_minimal', '=>' => 'align_single_space_minimal',
@ -42,40 +41,31 @@ return $config
'??' => 'align_single_space_minimal', '??' => 'align_single_space_minimal',
], ],
], ],
'blank_line_after_namespace' => true, 'braces_position' => [
'braces_position' => [
'anonymous_classes_opening_brace' => 'same_line', 'anonymous_classes_opening_brace' => 'same_line',
'control_structures_opening_brace' => 'same_line', 'control_structures_opening_brace' => 'same_line',
'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end',
], ],
'elseif' => true,
'encoding' => true,
'full_opening_tag' => true,
'function_declaration' => [ 'function_declaration' => [
'closure_function_spacing' => 'one', 'closure_function_spacing' => 'one',
], ],
'indentation_type' => true, 'list_syntax' => [
'line_ending' => true, 'syntax' => 'short',
'list_syntax' => [
'syntax' => 'long',
], ],
'lowercase_keywords' => true, 'new_with_parentheses' => true,
'no_closing_tag' => true, 'no_unused_imports' => true,
'no_spaces_after_function_name' => true, 'single_import_per_statement' => true,
'spaces_inside_parentheses' => false, 'ternary_operator_spaces' => false,
'no_trailing_whitespace' => true, 'trailing_comma_in_multiline' => [
'no_trailing_whitespace_in_comment' => true, 'after_heredoc' => true,
'no_unused_imports' => true, 'elements' => [
'single_blank_line_at_eof' => true, 'arguments',
'single_class_element_per_statement' => true, 'array_destructuring',
'single_import_per_statement' => true, 'arrays',
'single_line_after_imports' => true, // 'match', /* activate `match` after PHP 7.4 support is dropped */
'switch_case_space' => true, // 'parameters', /* activate `arguments` after PHP 7.4 support is dropped */
'ternary_operator_spaces' => false, ],
'visibility_required' => [
'elements' => ['property', 'method']
], ],
'new_with_parentheses' => true,
]) ])
->setFinder($finder) ->setFinder($finder)
->setIndent("\t"); ->setIndent("\t");

31
.rector.php Normal file
View file

@ -0,0 +1,31 @@
<?php
/**
* SPDX-FileCopyrightText: 2010 - 2024 the Friendica project
*
* SPDX-License-Identifier: CC0-1.0
*/
declare(strict_types=1);
return \Rector\Config\RectorConfig::configure()
->withPaths([
__DIR__ . '/config',
__DIR__ . '/mod',
__DIR__ . '/src',
__DIR__ . '/static',
__DIR__ . '/tests',
__DIR__ . '/view',
])
->withIndent("\t", 4)
->withPhpVersion(70400)
// ->withTypeCoverageLevel(0)
// ->withDeadCodeLevel(0)
// ->withCodeQualityLevel(0)
->withSets([
\Rector\Set\ValueObject\LevelSetList::UP_TO_PHP_74,
])
->withSkip([
\Rector\Php56\Rector\FuncCall\PowToExpRector::class,
\Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector::class,
])
;

View file

@ -20,11 +20,14 @@ steps:
- '.composer' - '.composer'
volumes: volumes:
- /tmp/drone-cache:/tmp/cache - /tmp/drone-cache:/tmp/cache
composer_install: composer_install:
image: composer image: friendicaci/php8.3:php8.3.17
commands: commands:
- mkdir addon # create empty addon folder to appease composer
- export COMPOSER_HOME=.composer - export COMPOSER_HOME=.composer
- ./bin/composer.phar run cs:install - ./bin/composer.phar install --prefer-dist
rebuild_cache: rebuild_cache:
image: meltwater/drone-cache:dev image: meltwater/drone-cache:dev
settings: settings:
@ -36,6 +39,7 @@ steps:
- '.composer' - '.composer'
volumes: volumes:
- /tmp/drone-cache:/tmp/cache - /tmp/drone-cache:/tmp/cache
check: check:
image: php:8.3 image: php:8.3
commands: commands:
@ -49,4 +53,4 @@ steps:
CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB ${CI_COMMIT_SHA})"; CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB ${CI_COMMIT_SHA})";
fi fi
- EXTRA_ARGS="--path-mode=intersection -- $${CHANGED_FILES}"; - EXTRA_ARGS="--path-mode=intersection -- $${CHANGED_FILES}";
- ./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --diff --using-cache=no $${EXTRA_ARGS} - ./vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --diff --using-cache=no $${EXTRA_ARGS}

View file

@ -57,7 +57,7 @@ steps:
composer_install: composer_install:
image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION}
commands: commands:
- git clone -b 2025.07-rc --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon - git clone -b develop --single-branch https://git.friendi.ca/friendica/friendica-addons.git addon
- export COMPOSER_HOME=.composer - export COMPOSER_HOME=.composer
- ./bin/composer.phar validate - ./bin/composer.phar validate
- ./bin/composer.phar install --prefer-dist - ./bin/composer.phar install --prefer-dist

View file

@ -0,0 +1,96 @@
# SPDX-FileCopyrightText: 2010 - 2024 the Friendica project
#
# SPDX-License-Identifier: CC0-1.0
# This prevents executing this pipeline at other servers than ci.friendi.ca
labels:
location: friendica
type: releaser
skip_clone: true
when:
repo: friendica/friendica
branch: stable
event: tag
steps:
clone_friendica_base:
image: alpine/git
commands:
- git config --global user.email "no-reply@friendi.ca"
- git config --global user.name "Friendica"
- git config --global --add safe.directory $CI_WORKSPACE
- git clone $CI_REPO_CLONE_URL .
- git checkout $CI_COMMIT_BRANCH
- git fetch origin $CI_COMMIT_REF
- git merge $CI_COMMIT_SHA
clone_friendica_addon:
image: alpine/git
commands:
- git config --global user.email "no-reply@friendi.ca"
- git config --global user.name "Friendica"
- git clone https://github.com/friendica/friendica-addons.git addon
- cd addon/
- git checkout $CI_COMMIT_BRANCH
restore_cache:
image: meltwater/drone-cache:dev
settings:
backend: "filesystem"
restore: true
cache_key: "{{ .Repo.Name }}_php7.4_{{ arch }}_{{ os }}"
archive_format: "gzip"
mount:
- '.composer'
volumes:
- /tmp/drone-cache:/tmp/cache
composer_install:
image: friendicaci/php8.2:php8.2.28
commands:
- export COMPOSER_HOME=.composer
- composer validate
- composer install --no-dev --optimize-autoloader
volumes:
- /etc/hosts:/etc/hosts
create_artifacts:
image: debian
commands:
- apt-get update
- apt-get install bzip2
- mkdir ./build
- export VERSION="$(cat VERSION)"
- export RELEASE="friendica-all-in-one-$VERSION"
- export ARTIFACT="$RELEASE.tar.gz"
- tar
--transform "s,^,$RELEASE/,S"
-X mods/release-list-exclude.txt
-T mods/release-list-include.txt
-cvzf ./build/$ARTIFACT
- cd ./build
- sha256sum "$ARTIFACT" > "$ARTIFACT.sum256"
- chmod 664 ./*
- ls -lh
- cat "$ARTIFACT.sum256"
- sha256sum "$ARTIFACT"
sign_artifacts:
image: plugins/gpgsign
settings:
key:
from_secret: gpg_key
passphrase:
from_secret: gpg_password
files:
- build/*
exclude:
- build/*.sum256
detach_sign: true
when:
repo: friendica/friendica
branch: stable
event: tag
publish_artifacts:
image: alpine
commands:
- cp -fr build/* /tmp/friendica_files/
volumes:
- files:/tmp/friendica_files

View file

@ -1,3 +1,10 @@
Version 2026.04 (unreleased)
Friendica Core
Friendica Addons
Closed Issues
Version 2026.01 (2026-01-27) Version 2026.01 (2026-01-27)
Friendica Core Friendica Core
Updates to the translations Updates to the translations

View file

@ -12,7 +12,7 @@ You can control the privacy scope of your content.
Being part of the Fediverse allows you to be free from data-harvesting corporations. Being part of the Fediverse allows you to be free from data-harvesting corporations.
Enjoy open social communication, independent of any specific provider. Enjoy open social communication, independent of any specific provider.
[Join Friendica](https://dir.friendica.social/servers) today or set up [your own Friendica instance](doc/Install.md). [Join Friendica](https://dir.friendica.social/servers) today or set up [your own Friendica instance](doc/en/admin/install.md).
### Friendica on desktop ### Friendica on desktop

View file

@ -7,7 +7,6 @@ SPDX-PackageDownloadLocation = "https://friendi.ca"
path = [ path = [
"database.sql", "database.sql",
"composer.*", "composer.*",
"bin/dev/php-cs-fixer/composer.*",
"doc/**", "doc/**",
"spec/*", "spec/*",
"tests/**", "tests/**",

View file

@ -1 +1 @@
2026.01 2026.04-dev

View file

@ -13,7 +13,7 @@ if (php_sapi_name() !== 'cli') {
exit(); exit();
} }
// Ensure that te console is executed from the base path of the installation // Ensure console is executed from the base path of the installation
chdir(dirname(__DIR__)); chdir(dirname(__DIR__));
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';

View file

@ -1,12 +1,13 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* Copyright (C) 2010-2024, the Friendica project * Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project * SPDX-FileCopyrightText: 2010-2024 the Friendica project
* *
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
* *
* @deprecated 2025.07 use `bin/console.php daemon` instead * @deprecated 2026.01 use `bin/console.php daemon` instead
*/ */
/** /**
@ -24,7 +25,7 @@ chdir(dirname(__DIR__));
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
fwrite(STDOUT, '`bin/daemon.php` is deprecated since 2025.07 and will be removed in 5 months, please use `bin/console.php daemon` instead.' . \PHP_EOL); fwrite(STDOUT, '`bin/daemon.php` is deprecated since 2026.01 and will be removed in 5 months, please use `bin/console.php daemon` instead.' . \PHP_EOL);
// BC: Add console command as second argument // BC: Add console command as second argument
$argv = $_SERVER['argv'] ?? []; $argv = $_SERVER['argv'] ?? [];

View file

@ -21,4 +21,4 @@ fi
EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}"); EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${CHANGED_FILES}");
./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer ${COMMAND} --config=.php-cs-fixer.dist.php -v --using-cache=no ${EXTRA_ARGS} ./vendor/bin/php-cs-fixer ${COMMAND} --config=.php-cs-fixer.dist.php -v --using-cache=no ${EXTRA_ARGS}

View file

@ -1 +0,0 @@
vendor

View file

@ -1,5 +0,0 @@
{
"require": {
"friendsofphp/php-cs-fixer": "^3.46"
}
}

View file

@ -1,2425 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "df2cc2d63945b734c8edd007837e47ad",
"packages": [
{
"name": "clue/ndjson-react",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/clue/reactphp-ndjson.git",
"reference": "392dc165fce93b5bb5c637b67e59619223c931b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0",
"reference": "392dc165fce93b5bb5c637b67e59619223c931b0",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/stream": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35",
"react/event-loop": "^1.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\React\\NDJson\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.",
"homepage": "https://github.com/clue/reactphp-ndjson",
"keywords": [
"NDJSON",
"json",
"jsonlines",
"newline",
"reactphp",
"streaming"
],
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2022-12-23T10:58:28+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.0",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81",
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.11.10",
"phpstan/phpstan-strict-rules": "^1.1",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-08-19T19:43:53+00:00"
},
{
"name": "composer/semver",
"version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6",
"reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.4",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-07-12T11:35:52+00:00"
},
{
"name": "composer/xdebug-handler",
"version": "3.0.5",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
"reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
"reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
"shasum": ""
},
"require": {
"composer/pcre": "^1 || ^2 || ^3",
"php": "^7.2.5 || ^8.0",
"psr/log": "^1 || ^2 || ^3"
},
"require-dev": {
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-strict-rules": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Composer\\XdebugHandler\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "John Stevenson",
"email": "john-stevenson@blueyonder.co.uk"
}
],
"description": "Restarts a process without Xdebug.",
"keywords": [
"Xdebug",
"performance"
],
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-05-06T16:37:16+00:00"
},
{
"name": "evenement/evenement",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/igorw/evenement.git",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^9 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Evenement\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
}
],
"description": "Événement is a very simple event dispatching library for PHP",
"keywords": [
"event-dispatcher",
"event-emitter"
],
"time": "2023-08-08T05:53:35+00:00"
},
{
"name": "fidry/cpu-core-counter",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/theofidry/cpu-core-counter.git",
"reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42",
"reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"fidry/makefile": "^0.2.0",
"fidry/php-cs-fixer-config": "^1.1.2",
"phpstan/extension-installer": "^1.2.0",
"phpstan/phpstan": "^1.9.2",
"phpstan/phpstan-deprecation-rules": "^1.0.0",
"phpstan/phpstan-phpunit": "^1.2.2",
"phpstan/phpstan-strict-rules": "^1.4.4",
"phpunit/phpunit": "^8.5.31 || ^9.5.26",
"webmozarts/strict-phpunit": "^7.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Fidry\\CpuCoreCounter\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Théo FIDRY",
"email": "theo.fidry@gmail.com"
}
],
"description": "Tiny utility to get the number of CPU cores.",
"keywords": [
"CPU",
"core"
],
"funding": [
{
"url": "https://github.com/theofidry",
"type": "github"
}
],
"time": "2024-02-07T09:43:46+00:00"
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.62.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "627692f794d35c43483f34b01d94740df2a73507"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/627692f794d35c43483f34b01d94740df2a73507",
"reference": "627692f794d35c43483f34b01d94740df2a73507",
"shasum": ""
},
"require": {
"clue/ndjson-react": "^1.0",
"composer/semver": "^3.4",
"composer/xdebug-handler": "^3.0.3",
"ext-filter": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"fidry/cpu-core-counter": "^1.0",
"php": "^7.4 || ^8.0",
"react/child-process": "^0.6.5",
"react/event-loop": "^1.0",
"react/promise": "^2.0 || ^3.0",
"react/socket": "^1.0",
"react/stream": "^1.0",
"sebastian/diff": "^4.0 || ^5.0 || ^6.0",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0",
"symfony/finder": "^5.4 || ^6.0 || ^7.0",
"symfony/options-resolver": "^5.4 || ^6.0 || ^7.0",
"symfony/polyfill-mbstring": "^1.28",
"symfony/polyfill-php80": "^1.28",
"symfony/polyfill-php81": "^1.28",
"symfony/process": "^5.4 || ^6.0 || ^7.0",
"symfony/stopwatch": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3 || ^2.3",
"infection/infection": "^0.29.5",
"justinrainbow/json-schema": "^5.2",
"keradus/cli-executor": "^2.1",
"mikey179/vfsstream": "^1.6.11",
"php-coveralls/php-coveralls": "^2.7",
"php-cs-fixer/accessible-object": "^1.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5",
"phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2",
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
"ext-mbstring": "For handling non-UTF8 characters."
},
"bin": [
"php-cs-fixer"
],
"type": "application",
"autoload": {
"psr-4": {
"PhpCsFixer\\": "src/"
},
"exclude-from-classmap": [
"src/Fixer/Internal/*"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Dariusz Rumiński",
"email": "dariusz.ruminski@gmail.com"
}
],
"description": "A tool to automatically fix PHP code style",
"keywords": [
"Static code analysis",
"fixer",
"standards",
"static analysis"
],
"funding": [
{
"url": "https://github.com/keradus",
"type": "github"
}
],
"time": "2024-08-07T17:03:09+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/log",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2024-08-21T13:31:24+00:00"
},
{
"name": "react/cache",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/cache.git",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/promise": "^3.0 || ^2.0 || ^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, Promise-based cache interface for ReactPHP",
"keywords": [
"cache",
"caching",
"promise",
"reactphp"
],
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2022-11-30T15:59:55+00:00"
},
{
"name": "react/child-process",
"version": "v0.6.5",
"source": {
"type": "git",
"url": "https://github.com/reactphp/child-process.git",
"reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43",
"reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.0",
"react/event-loop": "^1.2",
"react/stream": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
"react/socket": "^1.8",
"sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\ChildProcess\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Event-driven library for executing child processes with ReactPHP.",
"keywords": [
"event-driven",
"process",
"reactphp"
],
"funding": [
{
"url": "https://github.com/WyriHaximus",
"type": "github"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2022-09-16T13:41:56+00:00"
},
{
"name": "react/dns",
"version": "v1.13.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/dns.git",
"reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5",
"reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async DNS resolver for ReactPHP",
"keywords": [
"async",
"dns",
"dns-resolver",
"reactphp"
],
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-13T14:18:03+00:00"
},
{
"name": "react/event-loop",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/event-loop.git",
"reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
"reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"suggest": {
"ext-pcntl": "For signal handling support when using the StreamSelectLoop"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\EventLoop\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
"keywords": [
"asynchronous",
"event-loop"
],
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2023-11-13T13:48:05+00:00"
},
{
"name": "react/promise",
"version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "8a164643313c71354582dc850b42b33fa12a4b63"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63",
"reference": "8a164643313c71354582dc850b42b33fa12a4b63",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
"phpstan/phpstan": "1.10.39 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"keywords": [
"promise",
"promises"
],
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-05-24T10:39:05+00:00"
},
{
"name": "react/socket",
"version": "v1.16.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/socket.git",
"reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1",
"reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.0",
"react/dns": "^1.13",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.6 || ^1.2.1",
"react/stream": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3.3 || ^2",
"react/promise-stream": "^1.4",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Socket\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
"keywords": [
"Connection",
"Socket",
"async",
"reactphp",
"stream"
],
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-07-26T10:38:09+00:00"
},
{
"name": "react/stream",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/stream.git",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.8",
"react/event-loop": "^1.2"
},
"require-dev": {
"clue/stream-filter": "~1.2",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
"keywords": [
"event-driven",
"io",
"non-blocking",
"pipe",
"reactphp",
"readable",
"stream",
"writable"
],
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-11T12:45:25+00:00"
},
{
"name": "sebastian/diff",
"version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
"reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"symfony/process": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "5.1-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
],
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2024-03-02T07:15:17+00:00"
},
{
"name": "symfony/console",
"version": "v6.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc",
"reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/string": "^5.4|^6.0|^7.0"
},
"conflict": {
"symfony/dependency-injection": "<5.4",
"symfony/dotenv": "<5.4",
"symfony/event-dispatcher": "<5.4",
"symfony/lock": "<5.4",
"symfony/process": "<5.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/lock": "^5.4|^6.0|^7.0",
"symfony/messenger": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command-line",
"console",
"terminal"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-07-26T12:30:32+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v6.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "8d7507f02b06e06815e56bb39aa0128e3806208b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b",
"reference": "8d7507f02b06e06815e56bb39aa0128e3806208b",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/event-dispatcher-contracts": "^2.5|^3"
},
"conflict": {
"symfony/dependency-injection": "<5.4",
"symfony/service-contracts": "<2.5"
},
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/error-handler": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\EventDispatcher\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
"reference": "8f93aec25d41b72493c6ddff14e916177c9efc50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50",
"reference": "8f93aec25d41b72493c6ddff14e916177c9efc50",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/event-dispatcher": "^1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\EventDispatcher\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to dispatching event",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
"version": "v6.4.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "b51ef8059159330b74a4d52f68e671033c0fe463"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463",
"reference": "b51ef8059159330b74a4d52f68e671033c0fe463",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^5.4|^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-28T09:49:33+00:00"
},
{
"name": "symfony/finder",
"version": "v6.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "af29198d87112bebdd397bd7735fbd115997824c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c",
"reference": "af29198d87112bebdd397bd7735fbd115997824c",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"symfony/filesystem": "^6.0|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-07-24T07:06:38+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v6.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "22ab9e9101ab18de37839074f8a1197f55590c1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b",
"reference": "22ab9e9101ab18de37839074f8a1197f55590c1b",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
"reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af",
"reference": "3fb075789fb91f9ad9af537c4012d523085bd5af",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/process",
"version": "v6.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5",
"reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/container": "^1.1|^2.0",
"symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/stopwatch",
"version": "v6.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "63e069eb616049632cde9674c46957819454b8aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/63e069eb616049632cde9674c46957819454b8aa",
"reference": "63e069eb616049632cde9674c46957819454b8aa",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/service-contracts": "^2.5|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Stopwatch\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:49:08+00:00"
},
{
"name": "symfony/string",
"version": "v6.4.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ccf9b30251719567bfd46494138327522b9a9446"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446",
"reference": "ccf9b30251719567bfd46494138327522b9a9446",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
"symfony/error-handler": "^5.4|^6.0|^7.0",
"symfony/http-client": "^5.4|^6.0|^7.0",
"symfony/intl": "^6.2|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
"symfony/var-exporter": "^5.4|^6.0|^7.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-07-22T10:21:14+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

View file

@ -1,12 +1,13 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* Copyright (C) 2010-2024, the Friendica project * Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project * SPDX-FileCopyrightText: 2010-2024 the Friendica project
* *
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
* *
* @deprecated 2025.07 use `bin/console.php jetstream` instead * @deprecated 2026.01 use `bin/console.php jetstream` instead
*/ */
if (php_sapi_name() !== 'cli') { if (php_sapi_name() !== 'cli') {
@ -19,7 +20,7 @@ chdir(dirname(__DIR__));
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
fwrite(STDOUT, '`bin/jetstream.php` is deprecated since 2025.07 and will be removed in 5 months, please use `bin/console.php jetstream` instead.' . \PHP_EOL); fwrite(STDOUT, '`bin/jetstream.php` is deprecated since 2026.01 and will be removed in 5 months, please use `bin/console.php jetstream` instead.' . \PHP_EOL);
// BC: Add console command as second argument // BC: Add console command as second argument
$argv = $_SERVER['argv'] ?? []; $argv = $_SERVER['argv'] ?? [];

View file

@ -1,5 +1,6 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* Copyright (C) 2010-2024, the Friendica project * Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project * SPDX-FileCopyrightText: 2010-2024 the Friendica project
@ -8,7 +9,7 @@
* *
* Starts the background processing * Starts the background processing
* *
* @deprecated 2025.07 use `bin/console.php worker` instead * @deprecated 2026.01 use `bin/console.php worker` instead
*/ */
if (php_sapi_name() !== 'cli') { if (php_sapi_name() !== 'cli') {
@ -21,7 +22,7 @@ chdir(dirname(__DIR__));
require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/autoload.php';
fwrite(STDOUT, '`bin/worker.php` is deprecated since 2025.07 and will be removed in 5 months, please use `bin/console.php worker` instead.' . \PHP_EOL); fwrite(STDOUT, '`bin/worker.php` is deprecated since 2026.01 and will be removed in 5 months, please use `bin/console.php worker` instead.' . \PHP_EOL);
// BC: Add console command as second argument // BC: Add console command as second argument
$argv = $_SERVER['argv'] ?? []; $argv = $_SERVER['argv'] ?? [];

View file

@ -13,7 +13,7 @@
"issues": "https://github.com/friendica/friendica/issues" "issues": "https://github.com/friendica/friendica/issues"
}, },
"require": { "require": {
"php": ">=7.4", "php": "7.4.* || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.* || 8.5.*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",
@ -154,14 +154,16 @@
}, },
"require-dev": { "require-dev": {
"dms/phpunit-arraysubset-asserts": "^0.3.1", "dms/phpunit-arraysubset-asserts": "^0.3.1",
"friendsofphp/php-cs-fixer": "^3.94",
"mikey179/vfsstream": "^1.6", "mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.3", "mockery/mockery": "^1.3",
"php-mock/php-mock-mockery": "^1.5", "php-mock/php-mock-mockery": "^1.5",
"php-mock/php-mock-phpunit": "^2.10", "php-mock/php-mock-phpunit": "^2.15",
"phpmd/phpmd": "^2.15", "phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^2.0", "phpstan/phpstan": "^2.1.39",
"phpstan/phpstan-strict-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0.10",
"phpunit/phpunit": "^9" "phpunit/phpunit": "^9",
"rector/rector": "^2.2"
}, },
"scripts": { "scripts": {
"test": "phpunit", "test": "phpunit",
@ -172,16 +174,16 @@
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './view/asset/*' -print0 | xargs -0 -n1 php -l", "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './view/asset/*' -print0 | xargs -0 -n1 php -l",
"docker:translate": "docker run --rm -v $PWD:/data -w /data friendicaci/transifex bin/run_xgettext.sh", "docker:translate": "docker run --rm -v $PWD:/data -w /data friendicaci/transifex bin/run_xgettext.sh",
"lang:recreate": "bin/run_xgettext.sh", "lang:recreate": "bin/run_xgettext.sh",
"cs:install": "@composer install --working-dir=bin/dev/php-cs-fixer", "cs:check": "php-cs-fixer check --diff",
"cs:check": [ "cs:fix": "php-cs-fixer fix",
"@cs:install",
"bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --diff"
],
"cs:fix": [
"@cs:install",
"bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer fix"
],
"cs:fix-develop": "TARGET_BRANCH=develop COMMAND=fix bin/dev/fix-codestyle.sh", "cs:fix-develop": "TARGET_BRANCH=develop COMMAND=fix bin/dev/fix-codestyle.sh",
"rector:check": "@rector:run --dry-run",
"rector:run": "rector --config=\".rector.php\"",
"rectify": [
"@rector:run",
"@lang:recreate",
"@cs:fix-develop"
],
"db:update-structure": "bin/console.php dbstructure dumpsql > database.sql", "db:update-structure": "bin/console.php dbstructure dumpsql > database.sql",
"install:prod": "@composer install -o --no-dev" "install:prod": "@composer install -o --no-dev"
} }

1652
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b1e969f4be6ba6ed11d303a326333125", "content-hash": "2b4d6540f8af879e01ed418f2d5af7ab",
"packages": [ "packages": [
{ {
"name": "alchemy/binary-driver", "name": "alchemy/binary-driver",
@ -5987,6 +5987,70 @@
} }
], ],
"packages-dev": [ "packages-dev": [
{
"name": "clue/ndjson-react",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/clue/reactphp-ndjson.git",
"reference": "392dc165fce93b5bb5c637b67e59619223c931b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0",
"reference": "392dc165fce93b5bb5c637b67e59619223c931b0",
"shasum": ""
},
"require": {
"php": ">=5.3",
"react/stream": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35",
"react/event-loop": "^1.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Clue\\React\\NDJson\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.",
"homepage": "https://github.com/clue/reactphp-ndjson",
"keywords": [
"NDJSON",
"json",
"jsonlines",
"newline",
"reactphp",
"streaming"
],
"support": {
"issues": "https://github.com/clue/reactphp-ndjson/issues",
"source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0"
},
"funding": [
{
"url": "https://clue.engineering/support",
"type": "custom"
},
{
"url": "https://github.com/clue",
"type": "github"
}
],
"time": "2022-12-23T10:58:28+00:00"
},
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "3.3.2", "version": "3.3.2",
@ -6066,6 +6130,83 @@
], ],
"time": "2024-11-12T16:29:46+00:00" "time": "2024-11-12T16:29:46+00:00"
}, },
{
"name": "composer/semver",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-08-20T19:15:30+00:00"
},
{ {
"name": "composer/xdebug-handler", "name": "composer/xdebug-handler",
"version": "3.0.5", "version": "3.0.5",
@ -6247,6 +6388,171 @@
], ],
"time": "2022-12-30T00:15:36+00:00" "time": "2022-12-30T00:15:36+00:00"
}, },
{
"name": "fidry/cpu-core-counter",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/theofidry/cpu-core-counter.git",
"reference": "db9508f7b1474469d9d3c53b86f817e344732678"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
"reference": "db9508f7b1474469d9d3c53b86f817e344732678",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"fidry/makefile": "^0.2.0",
"fidry/php-cs-fixer-config": "^1.1.2",
"phpstan/extension-installer": "^1.2.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^8.5.31 || ^9.5.26",
"webmozarts/strict-phpunit": "^7.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Fidry\\CpuCoreCounter\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Théo FIDRY",
"email": "theo.fidry@gmail.com"
}
],
"description": "Tiny utility to get the number of CPU cores.",
"keywords": [
"CPU",
"core"
],
"support": {
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
"source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
},
"funding": [
{
"url": "https://github.com/theofidry",
"type": "github"
}
],
"time": "2025-08-14T07:29:31+00:00"
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.94.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63",
"reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63",
"shasum": ""
},
"require": {
"clue/ndjson-react": "^1.3",
"composer/semver": "^3.4",
"composer/xdebug-handler": "^3.0.5",
"ext-filter": "*",
"ext-hash": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"fidry/cpu-core-counter": "^1.3",
"php": "^7.4 || ^8.0",
"react/child-process": "^0.6.6",
"react/event-loop": "^1.5",
"react/socket": "^1.16",
"react/stream": "^1.4",
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0",
"symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.33",
"symfony/polyfill-php84": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0",
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7.1",
"infection/infection": "^0.32.3",
"justinrainbow/json-schema": "^6.6.4",
"keradus/cli-executor": "^2.3",
"mikey179/vfsstream": "^1.6.12",
"php-coveralls/php-coveralls": "^2.9.1",
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7",
"phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51",
"symfony/polyfill-php85": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4",
"symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1"
},
"suggest": {
"ext-dom": "For handling output formats in XML",
"ext-mbstring": "For handling non-UTF8 characters."
},
"bin": [
"php-cs-fixer"
],
"type": "application",
"autoload": {
"psr-4": {
"PhpCsFixer\\": "src/"
},
"exclude-from-classmap": [
"src/**/Internal/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Dariusz Rumiński",
"email": "dariusz.ruminski@gmail.com"
}
],
"description": "A tool to automatically fix PHP code style",
"keywords": [
"Static code analysis",
"fixer",
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2"
},
"funding": [
{
"url": "https://github.com/keradus",
"type": "github"
}
],
"time": "2026-02-20T16:13:53+00:00"
},
{ {
"name": "hamcrest/hamcrest-php", "name": "hamcrest/hamcrest-php",
"version": "v2.1.1", "version": "v2.1.1",
@ -6495,16 +6801,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.6.1", "version": "v5.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6547,9 +6853,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
}, },
"time": "2025-08-13T20:13:15+00:00" "time": "2025-12-06T11:56:16+00:00"
}, },
{ {
"name": "pdepend/pdepend", "name": "pdepend/pdepend",
@ -6734,27 +7040,27 @@
}, },
{ {
"name": "php-mock/php-mock", "name": "php-mock/php-mock",
"version": "2.6.2", "version": "2.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-mock/php-mock.git", "url": "https://github.com/php-mock/php-mock.git",
"reference": "e134d210e4707c29724ebc7fe50d220123f0fdd9" "reference": "b59734f19765296bb0311942850d02288a224890"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock/zipball/e134d210e4707c29724ebc7fe50d220123f0fdd9", "url": "https://api.github.com/repos/php-mock/php-mock/zipball/b59734f19765296bb0311942850d02288a224890",
"reference": "e134d210e4707c29724ebc7fe50d220123f0fdd9", "reference": "b59734f19765296bb0311942850d02288a224890",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^5.6 || ^7.0 || ^8.0", "php": "^5.6 || ^7.0 || ^8.0",
"phpunit/php-text-template": "^1 || ^2 || ^3 || ^4 || ^5" "phpunit/php-text-template": "^1 || ^2 || ^3 || ^4 || ^5 || ^6"
}, },
"replace": { "replace": {
"malkusch/php-mock": "*" "malkusch/php-mock": "*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
"squizlabs/php_codesniffer": "^3.8" "squizlabs/php_codesniffer": "^3.8"
}, },
"suggest": { "suggest": {
@ -6798,7 +7104,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/php-mock/php-mock/issues", "issues": "https://github.com/php-mock/php-mock/issues",
"source": "https://github.com/php-mock/php-mock/tree/2.6.2" "source": "https://github.com/php-mock/php-mock/tree/2.7.0"
}, },
"funding": [ "funding": [
{ {
@ -6806,29 +7112,29 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-08-18T19:59:14+00:00" "time": "2026-02-06T07:39:37+00:00"
}, },
{ {
"name": "php-mock/php-mock-integration", "name": "php-mock/php-mock-integration",
"version": "3.0.0", "version": "3.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-mock/php-mock-integration.git", "url": "https://github.com/php-mock/php-mock-integration.git",
"reference": "8ceb860f343a143af604efeb66a7a124381cc52e" "reference": "cbbf39705ec13dece5b04133cef4e2fd3137a345"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/8ceb860f343a143af604efeb66a7a124381cc52e", "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/cbbf39705ec13dece5b04133cef4e2fd3137a345",
"reference": "8ceb860f343a143af604efeb66a7a124381cc52e", "reference": "cbbf39705ec13dece5b04133cef4e2fd3137a345",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.6", "php": ">=5.6",
"php-mock/php-mock": "^2.5", "php-mock/php-mock": "^2.5",
"phpunit/php-text-template": "^1 || ^2 || ^3 || ^4 || ^5" "phpunit/php-text-template": "^1 || ^2 || ^3 || ^4 || ^5 || ^6"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12" "phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || ^13"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -6861,7 +7167,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/php-mock/php-mock-integration/issues", "issues": "https://github.com/php-mock/php-mock-integration/issues",
"source": "https://github.com/php-mock/php-mock-integration/tree/3.0.0" "source": "https://github.com/php-mock/php-mock-integration/tree/3.1.0"
}, },
"funding": [ "funding": [
{ {
@ -6869,7 +7175,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-03-08T19:22:38+00:00" "time": "2026-02-06T07:44:43+00:00"
}, },
{ {
"name": "php-mock/php-mock-mockery", "name": "php-mock/php-mock-mockery",
@ -6938,22 +7244,22 @@
}, },
{ {
"name": "php-mock/php-mock-phpunit", "name": "php-mock/php-mock-phpunit",
"version": "2.13.1", "version": "2.15.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-mock/php-mock-phpunit.git", "url": "https://github.com/php-mock/php-mock-phpunit.git",
"reference": "29f90fe44a04105959d6ae835b10c9e0da2fcaa7" "reference": "701df15b183f25af663af134eb71353cd838b955"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/29f90fe44a04105959d6ae835b10c9e0da2fcaa7", "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/701df15b183f25af663af134eb71353cd838b955",
"reference": "29f90fe44a04105959d6ae835b10c9e0da2fcaa7", "reference": "701df15b183f25af663af134eb71353cd838b955",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7", "php": ">=7",
"php-mock/php-mock-integration": "^3.0", "php-mock/php-mock-integration": "^3.0",
"phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10.0.17 || ^11 || ^12.0.9" "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10.0.17 || ^11 || ^12.0.9 || ^13"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.3.6" "mockery/mockery": "^1.3.6"
@ -6994,7 +7300,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/php-mock/php-mock-phpunit/issues", "issues": "https://github.com/php-mock/php-mock-phpunit/issues",
"source": "https://github.com/php-mock/php-mock-phpunit/tree/2.13.1" "source": "https://github.com/php-mock/php-mock-phpunit/tree/2.15.0"
}, },
"funding": [ "funding": [
{ {
@ -7002,7 +7308,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-09-23T06:00:08+00:00" "time": "2026-02-06T09:12:10+00:00"
}, },
{ {
"name": "phpmd/phpmd", "name": "phpmd/phpmd",
@ -7089,16 +7395,11 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "2.1.29", "version": "2.1.40",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phar-composer-source.git",
"reference": "git"
},
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e", "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7143,25 +7444,25 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-09-25T06:58:18+00:00" "time": "2026-02-23T15:04:35+00:00"
}, },
{ {
"name": "phpstan/phpstan-strict-rules", "name": "phpstan/phpstan-strict-rules",
"version": "2.0.6", "version": "2.0.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git", "url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094" "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094", "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f",
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094", "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.0.4" "phpstan/phpstan": "^2.1.39"
}, },
"require-dev": { "require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2", "php-parallel-lint/php-parallel-lint": "^1.2",
@ -7187,11 +7488,14 @@
"MIT" "MIT"
], ],
"description": "Extra strict and opinionated rules for PHPStan", "description": "Extra strict and opinionated rules for PHPStan",
"keywords": [
"static analysis"
],
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues", "issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6" "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.10"
}, },
"time": "2025-07-21T12:19:29+00:00" "time": "2026-02-11T14:17:32+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -7514,16 +7818,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "9.6.29", "version": "9.6.34",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" "reference": "b36f02317466907a230d3aa1d34467041271ef4a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a",
"reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "reference": "b36f02317466907a230d3aa1d34467041271ef4a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7545,7 +7849,7 @@
"phpunit/php-timer": "^5.0.3", "phpunit/php-timer": "^5.0.3",
"sebastian/cli-parser": "^1.0.2", "sebastian/cli-parser": "^1.0.2",
"sebastian/code-unit": "^1.0.8", "sebastian/code-unit": "^1.0.8",
"sebastian/comparator": "^4.0.9", "sebastian/comparator": "^4.0.10",
"sebastian/diff": "^4.0.6", "sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5", "sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.8", "sebastian/exporter": "^4.0.8",
@ -7597,7 +7901,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34"
}, },
"funding": [ "funding": [
{ {
@ -7621,7 +7925,593 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-09-24T06:29:11+00:00" "time": "2026-01-27T05:45:00+00:00"
},
{
"name": "react/cache",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/cache.git",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
"reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/promise": "^3.0 || ^2.0 || ^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, Promise-based cache interface for ReactPHP",
"keywords": [
"cache",
"caching",
"promise",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/cache/issues",
"source": "https://github.com/reactphp/cache/tree/v1.2.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2022-11-30T15:59:55+00:00"
},
{
"name": "react/child-process",
"version": "v0.6.7",
"source": {
"type": "git",
"url": "https://github.com/reactphp/child-process.git",
"reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3",
"reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.0",
"react/event-loop": "^1.2",
"react/stream": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/socket": "^1.16",
"sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\ChildProcess\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Event-driven library for executing child processes with ReactPHP.",
"keywords": [
"event-driven",
"process",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/child-process/issues",
"source": "https://github.com/reactphp/child-process/tree/v0.6.7"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-12-23T15:25:20+00:00"
},
{
"name": "react/dns",
"version": "v1.14.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/dns.git",
"reference": "7562c05391f42701c1fccf189c8225fece1cd7c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3",
"reference": "7562c05391f42701c1fccf189c8225fece1cd7c3",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async DNS resolver for ReactPHP",
"keywords": [
"async",
"dns",
"dns-resolver",
"reactphp"
],
"support": {
"issues": "https://github.com/reactphp/dns/issues",
"source": "https://github.com/reactphp/dns/tree/v1.14.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-18T19:34:28+00:00"
},
{
"name": "react/event-loop",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/event-loop.git",
"reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
"reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"suggest": {
"ext-pcntl": "For signal handling support when using the StreamSelectLoop"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\EventLoop\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
"keywords": [
"asynchronous",
"event-loop"
],
"support": {
"issues": "https://github.com/reactphp/event-loop/issues",
"source": "https://github.com/reactphp/event-loop/tree/v1.6.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-17T20:46:25+00:00"
},
{
"name": "react/promise",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
"reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
"shasum": ""
},
"require": {
"php": ">=7.1.0"
},
"require-dev": {
"phpstan/phpstan": "1.12.28 || 1.4.10",
"phpunit/phpunit": "^9.6 || ^7.5"
},
"type": "library",
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"React\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"keywords": [
"promise",
"promises"
],
"support": {
"issues": "https://github.com/reactphp/promise/issues",
"source": "https://github.com/reactphp/promise/tree/v3.3.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-08-19T18:57:03+00:00"
},
{
"name": "react/socket",
"version": "v1.17.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/socket.git",
"reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08",
"reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.0",
"react/dns": "^1.13",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.6 || ^1.2.1",
"react/stream": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3.3 || ^2",
"react/promise-stream": "^1.4",
"react/promise-timer": "^1.11"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Socket\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
"keywords": [
"Connection",
"Socket",
"async",
"reactphp",
"stream"
],
"support": {
"issues": "https://github.com/reactphp/socket/issues",
"source": "https://github.com/reactphp/socket/tree/v1.17.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2025-11-19T20:47:34+00:00"
},
{
"name": "react/stream",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/stream.git",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
"shasum": ""
},
"require": {
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"php": ">=5.3.8",
"react/event-loop": "^1.2"
},
"require-dev": {
"clue/stream-filter": "~1.2",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"type": "library",
"autoload": {
"psr-4": {
"React\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering",
"homepage": "https://clue.engineering/"
},
{
"name": "Cees-Jan Kiewiet",
"email": "reactphp@ceesjankiewiet.nl",
"homepage": "https://wyrihaximus.net/"
},
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "https://sorgalla.com/"
},
{
"name": "Chris Boden",
"email": "cboden@gmail.com",
"homepage": "https://cboden.dev/"
}
],
"description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
"keywords": [
"event-driven",
"io",
"non-blocking",
"pipe",
"reactphp",
"readable",
"stream",
"writable"
],
"support": {
"issues": "https://github.com/reactphp/stream/issues",
"source": "https://github.com/reactphp/stream/tree/v1.4.0"
},
"funding": [
{
"url": "https://opencollective.com/reactphp",
"type": "open_collective"
}
],
"time": "2024-06-11T12:45:25+00:00"
},
{
"name": "rector/rector",
"version": "2.3.8",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
"reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/bbd37aedd8df749916cffa2a947cfc4714d1ba2c",
"reference": "bbd37aedd8df749916cffa2a947cfc4714d1ba2c",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0",
"phpstan/phpstan": "^2.1.38"
},
"conflict": {
"rector/rector-doctrine": "*",
"rector/rector-downgrade-php": "*",
"rector/rector-phpunit": "*",
"rector/rector-symfony": "*"
},
"suggest": {
"ext-dom": "To manipulate phpunit.xml via the custom-rule command"
},
"bin": [
"bin/rector"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Instant Upgrade and Automated Refactoring of any PHP code",
"homepage": "https://getrector.com/",
"keywords": [
"automation",
"dev",
"migration",
"refactoring"
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/2.3.8"
},
"funding": [
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2026-02-22T09:45:50+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@ -7792,16 +8682,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "4.0.9", "version": "4.0.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
"reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7854,7 +8744,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
}, },
"funding": [ "funding": [
{ {
@ -7874,7 +8764,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-10T06:51:50+00:00" "time": "2026-01-24T09:22:56+00:00"
}, },
{ {
"name": "sebastian/complexity", "name": "sebastian/complexity",
@ -8713,6 +9603,105 @@
], ],
"time": "2024-10-30T07:58:02+00:00" "time": "2024-10-30T07:58:02+00:00"
}, },
{
"name": "symfony/console",
"version": "v5.4.47",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed",
"reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.9",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2|^3",
"symfony/string": "^5.1|^6.0"
},
"conflict": {
"psr/log": ">=3",
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
"symfony/lock": "<4.4",
"symfony/process": "<4.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0"
},
"require-dev": {
"psr/log": "^1|^2",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/event-dispatcher": "^4.4|^5.0|^6.0",
"symfony/lock": "^4.4|^5.0|^6.0",
"symfony/process": "^4.4|^5.0|^6.0",
"symfony/var-dumper": "^4.4|^5.0|^6.0"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command-line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.4.47"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-11-06T11:30:55+00:00"
},
{ {
"name": "symfony/dependency-injection", "name": "symfony/dependency-injection",
"version": "v5.4.48", "version": "v5.4.48",
@ -8802,6 +9791,305 @@
], ],
"time": "2024-11-20T10:51:57+00:00" "time": "2024-11-20T10:51:57+00:00"
}, },
{
"name": "symfony/finder",
"version": "v5.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "63741784cd7b9967975eec610b256eed3ede022b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b",
"reference": "63741784cd7b9967975eec610b256eed3ede022b",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v5.4.45"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-28T13:32:08+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v5.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6",
"reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-php73": "~1.0",
"symfony/polyfill-php80": "^1.16"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v5.4.45"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's grapheme_* functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"grapheme",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's Normalizer class and related functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"intl",
"normalizer",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{ {
"name": "symfony/polyfill-php81", "name": "symfony/polyfill-php81",
"version": "v1.33.0", "version": "v1.33.0",
@ -8883,17 +10171,245 @@
"time": "2024-09-09T11:45:10+00:00" "time": "2024-09-09T11:45:10+00:00"
}, },
{ {
"name": "theseer/tokenizer", "name": "symfony/polyfill-php84",
"version": "1.2.3", "version": "v1.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/theseer/tokenizer.git", "url": "https://github.com/symfony/polyfill-php84.git",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
"reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php84\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-24T13:30:11+00:00"
},
{
"name": "symfony/stopwatch",
"version": "v5.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/fb2c199cf302eb207f8c23e7ee174c1c31a5c004",
"reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/service-contracts": "^1|^2|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Stopwatch\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/stopwatch/tree/v5.4.45"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/string",
"version": "v5.4.47",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "136ca7d72f72b599f2631aca474a4f8e26719799"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799",
"reference": "136ca7d72f72b599f2631aca474a4f8e26719799",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "~1.15"
},
"conflict": {
"symfony/translation-contracts": ">=3.0"
},
"require-dev": {
"symfony/error-handler": "^4.4|^5.0|^6.0",
"symfony/http-client": "^4.4|^5.0|^6.0",
"symfony/translation-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0|^6.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
"i18n",
"string",
"unicode",
"utf-8",
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.4.47"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-11-10T20:33:58+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
"reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8922,7 +10438,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": { "support": {
"issues": "https://github.com/theseer/tokenizer/issues", "issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.3" "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
}, },
"funding": [ "funding": [
{ {
@ -8930,7 +10446,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-03-03T12:36:25+00:00" "time": "2025-11-17T20:03:58+00:00"
} }
], ],
"aliases": [], "aliases": [],
@ -8941,7 +10457,7 @@
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=7.4", "php": "7.4.* || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.* || 8.5.*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2026.01 (Blutwurz) -- Friendica 2026.04-dev (Blutwurz)
-- DB_UPDATE_VERSION 1586 -- DB_UPDATE_VERSION 1590
-- ------------------------------------------ -- ------------------------------------------
@ -236,6 +236,7 @@ CREATE TABLE IF NOT EXISTS `contact` (
INDEX `next-update` (`next-update`), INDEX `next-update` (`next-update`),
INDEX `local-data-next-update` (`local-data`,`next-update`), INDEX `local-data-next-update` (`local-data`,`next-update`),
INDEX `uid_lastitem` (`uid`,`last-item`), INDEX `uid_lastitem` (`uid`,`last-item`),
INDEX `uid_created` (`uid`,`created`),
INDEX `baseurl` (`baseurl`(64)), INDEX `baseurl` (`baseurl`(64)),
INDEX `uid_contact-type` (`uid`,`contact-type`), INDEX `uid_contact-type` (`uid`,`contact-type`),
INDEX `uid_self_contact-type` (`uid`,`self`,`contact-type`), INDEX `uid_self_contact-type` (`uid`,`self`,`contact-type`),
@ -414,7 +415,7 @@ CREATE TABLE IF NOT EXISTS `application` (
`name` varchar(255) NOT NULL COMMENT '', `name` varchar(255) NOT NULL COMMENT '',
`redirect_uri` varbinary(383) NOT NULL COMMENT '', `redirect_uri` varbinary(383) NOT NULL COMMENT '',
`website` varbinary(383) COMMENT '', `website` varbinary(383) COMMENT '',
`scopes` varchar(255) COMMENT '', `scopes` varchar(511) COMMENT '',
`read` boolean COMMENT 'Read scope', `read` boolean COMMENT 'Read scope',
`write` boolean COMMENT 'Write scope', `write` boolean COMMENT 'Write scope',
`follow` boolean COMMENT 'Follow scope', `follow` boolean COMMENT 'Follow scope',
@ -1353,6 +1354,7 @@ CREATE TABLE IF NOT EXISTS `post-engagement` (
`restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network', `restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network',
`comments` mediumint unsigned COMMENT 'Number of comments', `comments` mediumint unsigned COMMENT 'Number of comments',
`activities` mediumint unsigned COMMENT 'Number of activities (like, dislike, ...)', `activities` mediumint unsigned COMMENT 'Number of activities (like, dislike, ...)',
`views` mediumint unsigned COMMENT 'Number of views',
PRIMARY KEY(`uri-id`), PRIMARY KEY(`uri-id`),
INDEX `owner-id` (`owner-id`), INDEX `owner-id` (`owner-id`),
INDEX `created` (`created`), INDEX `created` (`created`),
@ -1368,6 +1370,7 @@ CREATE TABLE IF NOT EXISTS `channel-post` (
`channel` int unsigned NOT NULL COMMENT 'Channel id', `channel` int unsigned NOT NULL COMMENT 'Channel id',
`uri-id` int unsigned NOT NULL COMMENT 'Post engagement entry', `uri-id` int unsigned NOT NULL COMMENT 'Post engagement entry',
`uid` mediumint unsigned NOT NULL COMMENT 'User id', `uid` mediumint unsigned NOT NULL COMMENT 'User id',
`in-timeline` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is in the user\'s main timeline',
`created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
`received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
`commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
@ -1389,13 +1392,14 @@ CREATE TABLE IF NOT EXISTS `system-channel-post` (
`channel` varchar(20) NOT NULL COMMENT 'System channel id', `channel` varchar(20) NOT NULL COMMENT 'System channel id',
`uid` mediumint unsigned NOT NULL COMMENT 'User id', `uid` mediumint unsigned NOT NULL COMMENT 'User id',
`uri-id` int unsigned NOT NULL COMMENT 'Post engagement entry', `uri-id` int unsigned NOT NULL COMMENT 'Post engagement entry',
`in-timeline` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is in the user\'s main timeline',
`created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
`received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `received` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
`commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '', `commented` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '',
PRIMARY KEY(`channel`,`uid`,`uri-id`), PRIMARY KEY(`channel`,`uid`,`uri-id`),
INDEX `uri-id` (`uri-id`), INDEX `uri-id` (`uri-id`),
INDEX `uid` (`uid`), INDEX `uid` (`uid`),
INDEX `channel_created` (`channel`,`uid`,`created`), INDEX `channel_uid_created` (`channel`,`uid`,`created`),
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`uri-id`) REFERENCES `post-engagement` (`uri-id`) ON UPDATE RESTRICT ON DELETE CASCADE FOREIGN KEY (`uri-id`) REFERENCES `post-engagement` (`uri-id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Posts in a system channel'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Posts in a system channel';
@ -1494,6 +1498,47 @@ CREATE TABLE IF NOT EXISTS `post-media` (
FOREIGN KEY (`attach-id`) REFERENCES `attach` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE FOREIGN KEY (`attach-id`) REFERENCES `attach` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Attached media'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Attached media';
--
-- TABLE post-media-exif
--
CREATE TABLE IF NOT EXISTS `post-media-exif` (
`media-id` int unsigned NOT NULL COMMENT 'If of the post-media entry with EXIF data',
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`raw-data` text COMMENT 'JSON array with the raw exif data',
`coord` varchar(255) COMMENT 'GPS coordinates (latitude and longitude) representing the location where the picture was taken.',
`FocalLength` varchar(16) COMMENT 'The focal length of the lens in mm.',
`ExposureTime` varchar(16) COMMENT 'The exposure time in fractions of 1/x or full seconds.',
`ApertureFNumber` varchar(16) COMMENT 'The lens aperture calculated as f number',
`ISOSpeedRatings` smallint unsigned COMMENT 'The ISO speed used to expose the image.',
`LensSpecification` varchar(32) COMMENT 'Lens specifications, for example 35mm f/2.8 or 70-200mm f/2.8-6.3',
`FocusDistance` varchar(16) COMMENT 'The distance to the subject, given in meters.',
`CCDWidth` varchar(16) COMMENT '',
`BodySerialNumber` varchar(255) COMMENT 'The serial number of the body of the camera.',
`Artist` varchar(255) COMMENT 'The name of the camera owner, photographer or image creator.',
`Copyright` varchar(255) COMMENT 'Copyright information. In this standard the tag is used to indicate both the photographer and editor copyrights.',
`DateTime` datetime COMMENT 'The date and time of image creation. In Exif standard, it is the time the file was changed.',
`DateTimeOriginal` datetime COMMENT 'The date and time when the original image data was generated.',
`DateTimeDigitized` datetime COMMENT 'The date and time when the image was stored as digital data.',
`ExpandFilm` varchar(255) COMMENT 'The type or brand of film used for the image, such as analog film types (e.g., Kodak E100SW).',
`ExpandLens` varchar(255) COMMENT 'The lens model or description used for the image (e.g., Nikkor 20-35mm f/2.8 zoom).',
`HostComputer` varchar(255) COMMENT 'Information about the host computer used to generate the image.',
`ImageDescription` text COMMENT 'A character string giving the title of the image.',
`ImageUniqueID` varchar(255) COMMENT 'A unique identifier for each image, typically in the form of a UUID or other unique string.',
`LensMake` varchar(255) COMMENT 'The name of the lens manufacturer.',
`LensModel` varchar(255) COMMENT 'The model name or model number of the lens used.',
`Make` varchar(255) COMMENT 'The manufacturer of the recording equipment.',
`MakerNote` varchar(255) COMMENT 'A tag for manufacturers of Exif writers to record any desired information. The contents are up to the manufacturer.',
`Model` varchar(255) COMMENT 'The model name or model number of the equipment.',
`OwnerName` varchar(255) COMMENT 'The owner of the camera.',
`Orientation` tinyint unsigned COMMENT 'The image orientation in terms of rows and columns.',
`Software` varchar(255) COMMENT 'The name and version of the software or firmware of the camera or image input device used to generate the image.',
`UserComment` text COMMENT 'A comment provided by the user about the image.',
PRIMARY KEY(`media-id`),
INDEX `uri-id` (`uri-id`),
FOREIGN KEY (`media-id`) REFERENCES `post-media` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Exif data for attached media, see https://exiv2.org/tags.html';
-- --
-- TABLE post-origin -- TABLE post-origin
-- --
@ -2103,6 +2148,8 @@ CREATE VIEW `channel-post-view` AS SELECT
`channel-post`.`channel` AS `channel`, `channel-post`.`channel` AS `channel`,
`channel-post`.`uid` AS `uid`, `channel-post`.`uid` AS `uid`,
`channel-post`.`uri-id` AS `uri-id`, `channel-post`.`uri-id` AS `uri-id`,
`channel-post`.`in-timeline` AS `in-timeline`,
`post-engagement`.`owner-id` AS `contact-id`,
`post-engagement`.`owner-id` AS `owner-id`, `post-engagement`.`owner-id` AS `owner-id`,
`post-engagement`.`media-type` AS `media-type`, `post-engagement`.`media-type` AS `media-type`,
`post-engagement`.`language` AS `language`, `post-engagement`.`language` AS `language`,
@ -2114,8 +2161,9 @@ CREATE VIEW `channel-post-view` AS SELECT
`post-engagement`.`network` AS `network`, `post-engagement`.`network` AS `network`,
`ownercontact`.`contact-type` AS `contact-type`, `ownercontact`.`contact-type` AS `contact-type`,
`post-engagement`.`restricted` AS `restricted`, `post-engagement`.`restricted` AS `restricted`,
0 AS `comments`, `post-engagement`.`comments` AS `comments`,
0 AS `activities` `post-engagement`.`activities` AS `activities`,
`post-engagement`.`views` AS `views`
FROM `channel-post` FROM `channel-post`
INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `channel-post`.`uri-id` INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `channel-post`.`uri-id`
INNER JOIN `post-thread` ON `post-thread`.`uri-id` = `channel-post`.`uri-id` INNER JOIN `post-thread` ON `post-thread`.`uri-id` = `channel-post`.`uri-id`
@ -2131,6 +2179,8 @@ CREATE VIEW `system-channel-post-view` AS SELECT
`system-channel-post`.`channel` AS `channel`, `system-channel-post`.`channel` AS `channel`,
`system-channel-post`.`uid` AS `uid`, `system-channel-post`.`uid` AS `uid`,
`system-channel-post`.`uri-id` AS `uri-id`, `system-channel-post`.`uri-id` AS `uri-id`,
`system-channel-post`.`in-timeline` AS `in-timeline`,
`post-engagement`.`owner-id` AS `contact-id`,
`post-engagement`.`owner-id` AS `owner-id`, `post-engagement`.`owner-id` AS `owner-id`,
`post-engagement`.`media-type` AS `media-type`, `post-engagement`.`media-type` AS `media-type`,
`post-engagement`.`language` AS `language`, `post-engagement`.`language` AS `language`,
@ -2142,8 +2192,9 @@ CREATE VIEW `system-channel-post-view` AS SELECT
`post-engagement`.`network` AS `network`, `post-engagement`.`network` AS `network`,
`ownercontact`.`contact-type` AS `contact-type`, `ownercontact`.`contact-type` AS `contact-type`,
`post-engagement`.`restricted` AS `restricted`, `post-engagement`.`restricted` AS `restricted`,
0 AS `comments`, `post-engagement`.`comments` AS `comments`,
0 AS `activities` `post-engagement`.`activities` AS `activities`,
`post-engagement`.`views` AS `views`
FROM `system-channel-post` FROM `system-channel-post`
INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `system-channel-post`.`uri-id` INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `system-channel-post`.`uri-id`
INNER JOIN `post-thread` ON `post-thread`.`uri-id` = `system-channel-post`.`uri-id` INNER JOIN `post-thread` ON `post-thread`.`uri-id` = `system-channel-post`.`uri-id`
@ -2214,8 +2265,9 @@ CREATE VIEW `post-engagement-user-view` AS SELECT
`post-thread-user`.`network` AS `network`, `post-thread-user`.`network` AS `network`,
`post-user`.`protocol` AS `protocol`, `post-user`.`protocol` AS `protocol`,
`post-engagement`.`restricted` AS `restricted`, `post-engagement`.`restricted` AS `restricted`,
0 AS `comments`, `post-engagement`.`comments` AS `comments`,
0 AS `activities` `post-engagement`.`activities` AS `activities`,
`post-engagement`.`views` AS `views`
FROM `post-thread-user` FROM `post-thread-user`
INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `post-thread-user`.`uri-id` INNER JOIN `post-engagement` ON `post-engagement`.`uri-id` = `post-thread-user`.`uri-id`
INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id`
@ -2239,9 +2291,9 @@ CREATE VIEW `post-timeline-view` AS SELECT
`post-user`.`gravity` AS `gravity`, `post-user`.`gravity` AS `gravity`,
`post-user`.`created` AS `created`, `post-user`.`created` AS `created`,
`post-user`.`edited` AS `edited`, `post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`, `post-thread`.`commented` AS `commented`,
`post-user`.`received` AS `received`, `post-user`.`received` AS `received`,
`post-thread-user`.`changed` AS `changed`, `post-thread`.`changed` AS `changed`,
`post-user`.`private` AS `private`, `post-user`.`private` AS `private`,
`post-user`.`visible` AS `visible`, `post-user`.`visible` AS `visible`,
`post-user`.`deleted` AS `deleted`, `post-user`.`deleted` AS `deleted`,
@ -2267,11 +2319,11 @@ CREATE VIEW `post-timeline-view` AS SELECT
`post-user`.`causer-id` AS `causer-id`, `post-user`.`causer-id` AS `causer-id`,
`causer`.`blocked` AS `causer-blocked`, `causer`.`blocked` AS `causer-blocked`,
`causer`.`gsid` AS `causer-gsid`, `causer`.`gsid` AS `causer-gsid`,
`post-thread-user`.`network` AS `parent-network`, `post-thread`.`network` AS `parent-network`,
`post-thread-user`.`owner-id` AS `parent-owner-id`, `post-thread`.`owner-id` AS `parent-owner-id`,
`post-thread-user`.`author-id` AS `parent-author-id` `post-thread`.`author-id` AS `parent-author-id`
FROM `post-user` FROM `post-user`
LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` LEFT JOIN `post-thread` ON `post-thread`.`uri-id` = `post-user`.`parent-uri-id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id`
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
@ -2287,9 +2339,9 @@ CREATE VIEW `post-timeline-origin-view` AS SELECT
`post-origin`.`gravity` AS `gravity`, `post-origin`.`gravity` AS `gravity`,
`post-origin`.`created` AS `created`, `post-origin`.`created` AS `created`,
`post-user`.`edited` AS `edited`, `post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`, `post-thread`.`commented` AS `commented`,
`post-origin`.`received` AS `received`, `post-origin`.`received` AS `received`,
`post-thread-user`.`changed` AS `changed`, `post-thread`.`changed` AS `changed`,
`post-origin`.`private` AS `private`, `post-origin`.`private` AS `private`,
`post-user`.`visible` AS `visible`, `post-user`.`visible` AS `visible`,
`post-user`.`deleted` AS `deleted`, `post-user`.`deleted` AS `deleted`,
@ -2317,7 +2369,7 @@ CREATE VIEW `post-timeline-origin-view` AS SELECT
`causer`.`gsid` AS `causer-gsid` `causer`.`gsid` AS `causer-gsid`
FROM `post-origin` FROM `post-origin`
INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id` INNER JOIN `post-user` ON `post-user`.`id` = `post-origin`.`id`
LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-origin`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-origin`.`uid` LEFT JOIN `post-thread` ON `post-thread`.`uri-id` = `post-origin`.`parent-uri-id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id`
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
@ -2342,7 +2394,8 @@ CREATE VIEW `post-searchindex-user-view` AS SELECT
`post-user`.`protocol` AS `protocol`, `post-user`.`protocol` AS `protocol`,
`post-searchindex`.`restricted` AS `restricted`, `post-searchindex`.`restricted` AS `restricted`,
0 AS `comments`, 0 AS `comments`,
0 AS `activities` 0 AS `activities`,
0 AS `views`
FROM `post-thread-user` FROM `post-thread-user`
INNER JOIN `post-searchindex` ON `post-searchindex`.`uri-id` = `post-thread-user`.`uri-id` INNER JOIN `post-searchindex` ON `post-searchindex`.`uri-id` = `post-thread-user`.`uri-id`
INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id` INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id`
@ -2761,9 +2814,9 @@ CREATE VIEW `post-user-view` AS SELECT
`thr-parent-item-uri`.`uri` AS `thr-parent`, `thr-parent-item-uri`.`uri` AS `thr-parent`,
`post-user`.`thr-parent-id` AS `thr-parent-id`, `post-user`.`thr-parent-id` AS `thr-parent-id`,
`conversation-item-uri`.`uri` AS `conversation`, `conversation-item-uri`.`uri` AS `conversation`,
`post-thread-user`.`conversation-id` AS `conversation-id`, `post-thread`.`conversation-id` AS `conversation-id`,
`context-item-uri`.`uri` AS `context`, `context-item-uri`.`uri` AS `context`,
`post-thread-user`.`context-id` AS `context-id`, `post-thread`.`context-id` AS `context-id`,
`quote-item-uri`.`uri` AS `quote-uri`, `quote-item-uri`.`uri` AS `quote-uri`,
`post-content`.`quote-uri-id` AS `quote-uri-id`, `post-content`.`quote-uri-id` AS `quote-uri-id`,
`item-uri`.`guid` AS `guid`, `item-uri`.`guid` AS `guid`,
@ -2775,9 +2828,9 @@ CREATE VIEW `post-user-view` AS SELECT
`post-user`.`replies-id` AS `replies-id`, `post-user`.`replies-id` AS `replies-id`,
`post-user`.`created` AS `created`, `post-user`.`created` AS `created`,
`post-user`.`edited` AS `edited`, `post-user`.`edited` AS `edited`,
`post-thread-user`.`commented` AS `commented`, `post-thread`.`commented` AS `commented`,
`post-user`.`received` AS `received`, `post-user`.`received` AS `received`,
`post-thread-user`.`changed` AS `changed`, `post-thread`.`changed` AS `changed`,
`post-user`.`post-type` AS `post-type`, `post-user`.`post-type` AS `post-type`,
`post-user`.`post-reason` AS `post-reason`, `post-user`.`post-reason` AS `post-reason`,
`post-user`.`private` AS `private`, `post-user`.`private` AS `private`,
@ -2909,15 +2962,16 @@ CREATE VIEW `post-user-view` AS SELECT
EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-user`.`uri-id`) AS `has-media`, EXISTS(SELECT `id` FROM `post-media` WHERE `post-media`.`uri-id` = `post-user`.`uri-id`) AS `has-media`,
`diaspora-interaction`.`interaction` AS `signed_text`, `diaspora-interaction`.`interaction` AS `signed_text`,
`parent-item-uri`.`guid` AS `parent-guid`, `parent-item-uri`.`guid` AS `parent-guid`,
`post-thread-user`.`network` AS `parent-network`, `post-thread`.`network` AS `parent-network`,
`post-thread-user`.`owner-id` AS `parent-owner-id`, `post-thread`.`owner-id` AS `parent-owner-id`,
`post-thread-user`.`author-id` AS `parent-author-id`, `post-thread`.`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`.`nick` AS `parent-author-nick`, `parent-post-author`.`nick` AS `parent-author-nick`,
`parent-post-author`.`network` AS `parent-author-network` `parent-post-author`.`network` AS `parent-author-network`
FROM `post-user` FROM `post-user`
INNER JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid` LEFT JOIN `post-thread-user` ON `post-thread-user`.`uri-id` = `post-user`.`parent-uri-id` AND `post-thread-user`.`uid` = `post-user`.`uid`
INNER JOIN `post-thread` ON `post-thread`.`uri-id` = `post-user`.`parent-uri-id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id` STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id` STRAIGHT_JOIN `contact` AS `author` ON `author`.`id` = `post-user`.`author-id`
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id` STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
@ -2925,8 +2979,8 @@ CREATE VIEW `post-user-view` AS SELECT
LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-user`.`uri-id` LEFT JOIN `item-uri` ON `item-uri`.`id` = `post-user`.`uri-id`
LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id` LEFT JOIN `item-uri` AS `thr-parent-item-uri` ON `thr-parent-item-uri`.`id` = `post-user`.`thr-parent-id`
LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id` LEFT JOIN `item-uri` AS `parent-item-uri` ON `parent-item-uri`.`id` = `post-user`.`parent-uri-id`
LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread-user`.`conversation-id` LEFT JOIN `item-uri` AS `conversation-item-uri` ON `conversation-item-uri`.`id` = `post-thread`.`conversation-id`
LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread-user`.`context-id` LEFT JOIN `item-uri` AS `context-item-uri` ON `context-item-uri`.`id` = `post-thread`.`context-id`
LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id` LEFT JOIN `item-uri` AS `external-item-uri` ON `external-item-uri`.`id` = `post-user`.`external-id`
LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id` LEFT JOIN `item-uri` AS `replies-item-uri` ON `replies-item-uri`.`id` = `post-user`.`replies-id`
LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid` LEFT JOIN `verb` ON `verb`.`id` = `post-user`.`vid`
@ -2937,7 +2991,7 @@ CREATE VIEW `post-user-view` AS SELECT
LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-user`.`uri-id` AND `post-user`.`origin` LEFT JOIN `post-delivery-data` ON `post-delivery-data`.`uri-id` = `post-user`.`uri-id` AND `post-user`.`origin`
LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-user`.`uri-id` LEFT JOIN `post-question` ON `post-question`.`uri-id` = `post-user`.`uri-id`
LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid` LEFT JOIN `permissionset` ON `permissionset`.`id` = `post-user`.`psid`
LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread-user`.`author-id`; LEFT JOIN `contact` AS `parent-post-author` ON `parent-post-author`.`id` = `post-thread`.`author-id`;
-- --
-- VIEW post-thread-user-view -- VIEW post-thread-user-view
@ -3517,6 +3571,7 @@ CREATE VIEW `tag-view` AS SELECT
-- --
DROP VIEW IF EXISTS `network-thread-view`; DROP VIEW IF EXISTS `network-thread-view`;
CREATE VIEW `network-thread-view` AS SELECT CREATE VIEW `network-thread-view` AS SELECT
'' AS `channel`,
`post-thread-user`.`uri-id` AS `uri-id`, `post-thread-user`.`uri-id` AS `uri-id`,
`post-thread-user`.`post-user-id` AS `parent`, `post-thread-user`.`post-user-id` AS `parent`,
`post-thread-user`.`received` AS `received`, `post-thread-user`.`received` AS `received`,
@ -3546,6 +3601,7 @@ CREATE VIEW `network-thread-view` AS SELECT
-- --
DROP VIEW IF EXISTS `network-thread-circle-view`; DROP VIEW IF EXISTS `network-thread-circle-view`;
CREATE VIEW `network-thread-circle-view` AS SELECT CREATE VIEW `network-thread-circle-view` AS SELECT
'' AS `channel`,
`post-thread-user`.`uri-id` AS `uri-id`, `post-thread-user`.`uri-id` AS `uri-id`,
`post-thread-user`.`post-user-id` AS `parent`, `post-thread-user`.`post-user-id` AS `parent`,
`post-thread-user`.`received` AS `received`, `post-thread-user`.`received` AS `received`,

View file

@ -23,7 +23,7 @@ Wir planen, diese Einschränkung in einer zukünftigen Version zu beheben.
* Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale `.htaccess`-Datei nutzen kannst * Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale `.htaccess`-Datei nutzen kannst
* PHP 7.4+ * PHP 7.4+
* PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei * PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei
* Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, IntlChar, IDN und OpenSSL-Erweiterung * Curl, GD, GMP, PDO, mbstrings, MySQLi, hash, xml, zip, Intl, IDN und OpenSSL-Erweiterung
* Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert) * Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert)
* Einen E-Mail Server, so dass PHP `mail()` funktioniert. * Einen E-Mail Server, so dass PHP `mail()` funktioniert.
Wenn kein eigener E-Mail Server zur Verfügung steht, kann alternativ das [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) Addon mit einem externen SMTP Account verwendet werden. Wenn kein eigener E-Mail Server zur Verfügung steht, kann alternativ das [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) Addon mit einem externen SMTP Account verwendet werden.

View file

@ -30,7 +30,7 @@ Due to the large variety of operating systems and PHP platforms in existence we
* Apache with `mod_rewrite` enabled and "[AllowOverride All](https://httpd.apache.org/docs/2.4/mod/core.html#allowoverride)" so you can use a local `.htaccess` file * Apache with `mod_rewrite` enabled and "[AllowOverride All](https://httpd.apache.org/docs/2.4/mod/core.html#allowoverride)" so you can use a local `.htaccess` file
* PHP 7.4+ * PHP 7.4+
* PHP *command line* access with register_argc_argv set to true in the php.ini file * PHP *command line* access with register_argc_argv set to true in the php.ini file
* Curl, GD, GMP, PDO, mbstring, MySQLi, xml, zip, IntlChar, IDN and OpenSSL extensions * Curl, GD, GMP, PDO, mbstring, MySQLi, xml, zip, Intl, IDN and OpenSSL extensions
* The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it) * The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it)
* Some form of email server or email gateway such that PHP mail() works. * Some form of email server or email gateway such that PHP mail() works.
If you cannot set up your own email server, you can use the [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) addon and use a remote SMTP server. If you cannot set up your own email server, you can use the [phpmailer](https://github.com/friendica/friendica-addons/tree/develop/phpmailer) addon and use a remote SMTP server.

View file

@ -80,7 +80,7 @@ return [
## Addons ## Addons
> ⚠️ Since Friendica 2025.07 the strategy hooks for addons are deprecated, please use PHP hooks instead. > ⚠️ Since Friendica 2026.01 the strategy hooks for addons are deprecated, please use PHP hooks instead.
The hook logic is useful for decoupling the Friendica core logic, but its primary goal is to modularize Friendica in creating addons. The hook logic is useful for decoupling the Friendica core logic, but its primary goal is to modularize Friendica in creating addons.

View file

@ -12,7 +12,7 @@ OAuth application
| name | | varchar(255) | NO | | NULL | | | name | | varchar(255) | NO | | NULL | |
| redirect_uri | | varbinary(383) | NO | | NULL | | | redirect_uri | | varbinary(383) | NO | | NULL | |
| website | | varbinary(383) | YES | | NULL | | | website | | varbinary(383) | YES | | NULL | |
| scopes | | varchar(255) | YES | | NULL | | | scopes | | varchar(511) | YES | | NULL | |
| read | Read scope | boolean | YES | | NULL | | | read | Read scope | boolean | YES | | NULL | |
| write | Write scope | boolean | YES | | NULL | | | write | Write scope | boolean | YES | | NULL | |
| follow | Follow scope | boolean | YES | | NULL | | | follow | Follow scope | boolean | YES | | NULL | |

View file

@ -4,14 +4,15 @@ Posts in a user defined channel
## Fields ## Fields
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| --------- | --------------------- | ------------------ | ---- | --- | ------------------- | ----- | | ----------- | ------------------------------------------------- | ------------------ | ---- | --- | ------------------- | ----- |
| channel | Channel id | int unsigned | NO | PRI | NULL | | | channel | Channel id | int unsigned | NO | PRI | NULL | |
| uri-id | Post engagement entry | int unsigned | NO | PRI | NULL | | | uri-id | Post engagement entry | int unsigned | NO | PRI | NULL | |
| uid | User id | mediumint unsigned | NO | | NULL | | | uid | User id | mediumint unsigned | NO | | NULL | |
| created | | datetime | NO | | 0001-01-01 00:00:00 | | | in-timeline | If true, this post is in the user's main timeline | boolean | NO | | 0 | |
| received | | datetime | NO | | 0001-01-01 00:00:00 | | | created | | datetime | NO | | 0001-01-01 00:00:00 | |
| commented | | datetime | NO | | 0001-01-01 00:00:00 | | | received | | datetime | NO | | 0001-01-01 00:00:00 | |
| commented | | datetime | NO | | 0001-01-01 00:00:00 | |
## Indexes ## Indexes

View file

@ -114,6 +114,7 @@ contact table
| next-update | next-update | | next-update | next-update |
| local-data-next-update | local-data, next-update | | local-data-next-update | local-data, next-update |
| uid_lastitem | uid, last-item | | uid_lastitem | uid, last-item |
| uid_created | uid, created |
| baseurl | baseurl(64) | | baseurl | baseurl(64) |
| uid_contact-type | uid, contact-type | | uid_contact-type | uid, contact-type |
| uid_self_contact-type | uid, self, contact-type | | uid_self_contact-type | uid, self, contact-type |

View file

@ -18,6 +18,7 @@ Engagement data per post
| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | | | restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
| comments | Number of comments | mediumint unsigned | YES | | NULL | | | comments | Number of comments | mediumint unsigned | YES | | NULL | |
| activities | Number of activities (like, dislike, ...) | mediumint unsigned | YES | | NULL | | | activities | Number of activities (like, dislike, ...) | mediumint unsigned | YES | | NULL | |
| views | Number of views | mediumint unsigned | YES | | NULL | |
## Indexes ## Indexes

View file

@ -0,0 +1,55 @@
# Table post-media-exif
Exif data for attached media, see https://exiv2.org/tags.html
## Fields
| Field | Description | Type | Null | Key | Default | Extra |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------- | ---- | --- | ------- | ----- |
| media-id | If of the post-media entry with EXIF data | int unsigned | NO | PRI | NULL | |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | | NULL | |
| raw-data | JSON array with the raw exif data | text | YES | | NULL | |
| coord | GPS coordinates (latitude and longitude) representing the location where the picture was taken. | varchar(255) | YES | | NULL | |
| FocalLength | The focal length of the lens in mm. | varchar(16) | YES | | NULL | |
| ExposureTime | The exposure time in fractions of 1/x or full seconds. | varchar(16) | YES | | NULL | |
| ApertureFNumber | The lens aperture calculated as f number | varchar(16) | YES | | NULL | |
| ISOSpeedRatings | The ISO speed used to expose the image. | smallint unsigned | YES | | NULL | |
| LensSpecification | Lens specifications, for example 35mm f/2.8 or 70-200mm f/2.8-6.3 | varchar(32) | YES | | NULL | |
| FocusDistance | The distance to the subject, given in meters. | varchar(16) | YES | | NULL | |
| CCDWidth | | varchar(16) | YES | | NULL | |
| BodySerialNumber | The serial number of the body of the camera. | varchar(255) | YES | | NULL | |
| Artist | The name of the camera owner, photographer or image creator. | varchar(255) | YES | | NULL | |
| Copyright | Copyright information. In this standard the tag is used to indicate both the photographer and editor copyrights. | varchar(255) | YES | | NULL | |
| DateTime | The date and time of image creation. In Exif standard, it is the time the file was changed. | datetime | YES | | NULL | |
| DateTimeOriginal | The date and time when the original image data was generated. | datetime | YES | | NULL | |
| DateTimeDigitized | The date and time when the image was stored as digital data. | datetime | YES | | NULL | |
| ExpandFilm | The type or brand of film used for the image, such as analog film types (e.g., Kodak E100SW). | varchar(255) | YES | | NULL | |
| ExpandLens | The lens model or description used for the image (e.g., Nikkor 20-35mm f/2.8 zoom). | varchar(255) | YES | | NULL | |
| HostComputer | Information about the host computer used to generate the image. | varchar(255) | YES | | NULL | |
| ImageDescription | A character string giving the title of the image. | text | YES | | NULL | |
| ImageUniqueID | A unique identifier for each image, typically in the form of a UUID or other unique string. | varchar(255) | YES | | NULL | |
| LensMake | The name of the lens manufacturer. | varchar(255) | YES | | NULL | |
| LensModel | The model name or model number of the lens used. | varchar(255) | YES | | NULL | |
| Make | The manufacturer of the recording equipment. | varchar(255) | YES | | NULL | |
| MakerNote | A tag for manufacturers of Exif writers to record any desired information. The contents are up to the manufacturer. | varchar(255) | YES | | NULL | |
| Model | The model name or model number of the equipment. | varchar(255) | YES | | NULL | |
| OwnerName | The owner of the camera. | varchar(255) | YES | | NULL | |
| Orientation | The image orientation in terms of rows and columns. | tinyint unsigned | YES | | NULL | |
| Software | The name and version of the software or firmware of the camera or image input device used to generate the image. | varchar(255) | YES | | NULL | |
| UserComment | A comment provided by the user about the image. | text | YES | | NULL | |
## Indexes
| Name | Fields |
| ------- | -------- |
| PRIMARY | media-id |
| uri-id | uri-id |
## Foreign keys
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| media-id | [post-media](help/spec/database/db-post-media) | id |
| uri-id | [item-uri](help/spec/database/db-item-uri) | id |
Return to [database documentation](help/spec/database/index)

View file

@ -4,23 +4,24 @@ Posts in a system channel
## Fields ## Fields
| Field | Description | Type | Null | Key | Default | Extra | | Field | Description | Type | Null | Key | Default | Extra |
| --------- | --------------------- | ------------------ | ---- | --- | ------------------- | ----- | | ----------- | ------------------------------------------------- | ------------------ | ---- | --- | ------------------- | ----- |
| channel | System channel id | varchar(20) | NO | PRI | NULL | | | channel | System channel id | varchar(20) | NO | PRI | NULL | |
| uid | User id | mediumint unsigned | NO | PRI | NULL | | | uid | User id | mediumint unsigned | NO | PRI | NULL | |
| uri-id | Post engagement entry | int unsigned | NO | PRI | NULL | | | uri-id | Post engagement entry | int unsigned | NO | PRI | NULL | |
| created | | datetime | NO | | 0001-01-01 00:00:00 | | | in-timeline | If true, this post is in the user's main timeline | boolean | NO | | 0 | |
| received | | datetime | NO | | 0001-01-01 00:00:00 | | | created | | datetime | NO | | 0001-01-01 00:00:00 | |
| commented | | datetime | NO | | 0001-01-01 00:00:00 | | | received | | datetime | NO | | 0001-01-01 00:00:00 | |
| commented | | datetime | NO | | 0001-01-01 00:00:00 | |
## Indexes ## Indexes
| Name | Fields | | Name | Fields |
| --------------- | --------------------- | | ------------------- | --------------------- |
| PRIMARY | channel, uid, uri-id | | PRIMARY | channel, uid, uri-id |
| uri-id | uri-id | | uri-id | uri-id |
| uid | uid | | uid | uid |
| channel_created | channel, uid, created | | channel_uid_created | channel, uid, created |
## Foreign keys ## Foreign keys

View file

@ -65,6 +65,7 @@
| [post-history](help/spec/database/db-post-history) | Post history | | [post-history](help/spec/database/db-post-history) | Post history |
| [post-link](help/spec/database/db-post-link) | Post related external links | | [post-link](help/spec/database/db-post-link) | Post related external links |
| [post-media](help/spec/database/db-post-media) | Attached media | | [post-media](help/spec/database/db-post-media) | Attached media |
| [post-media-exif](help/spec/database/db-post-media-exif) | Exif data for attached media, see https://exiv2.org/tags.html |
| [post-origin](help/spec/database/db-post-origin) | Posts from local users | | [post-origin](help/spec/database/db-post-origin) | Posts from local users |
| [post-question](help/spec/database/db-post-question) | Question | | [post-question](help/spec/database/db-post-question) | Question |
| [post-question-option](help/spec/database/db-post-question-option) | Question option | | [post-question-option](help/spec/database/db-post-question-option) | Question option |

View file

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Copyright (C) 2010-2024, the Friendica project * Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project * SPDX-FileCopyrightText: 2010-2024 the Friendica project
@ -46,7 +47,7 @@ function item_post()
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
$_REQUEST = $eventDispatcher->dispatch( $_REQUEST = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL_START, $_REQUEST) new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL_START, $_REQUEST),
)->getArray(); )->getArray();
$return_path = $_REQUEST['return'] ?? ''; $return_path = $_REQUEST['return'] ?? '';
@ -101,14 +102,16 @@ function item_edit(int $uid, array $request, bool $preview, string $return_path)
$post = item_process($post, $request, $preview, $return_path); $post = item_process($post, $request, $preview, $return_path);
$fields = [ $fields = [
'title' => $post['title'], 'title' => $post['title'],
'body' => $post['body'], 'content-warning' => $post['content-warning'],
'attach' => $post['attach'], 'sensitive' => $post['sensitive'],
'file' => $post['file'], 'body' => $post['body'],
'location' => $post['location'], 'attach' => $post['attach'],
'coord' => $post['coord'], 'file' => $post['file'],
'edited' => DateTimeFormat::utcNow(), 'location' => $post['location'],
'changed' => DateTimeFormat::utcNow() 'coord' => $post['coord'],
'edited' => DateTimeFormat::utcNow(),
'changed' => DateTimeFormat::utcNow(),
]; ];
$fields['body'] = Item::setHashtags($fields['body']); $fields['body'] = Item::setHashtags($fields['body']);
@ -288,7 +291,7 @@ function item_process(array $post, array $request, bool $preview, string $return
]; ];
$hook_data = $eventDispatcher->dispatch( $hook_data = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL, $hook_data) new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL, $hook_data),
)->getArray(); )->getArray();
$post = $hook_data['item'] ?? $post; $post = $hook_data['item'] ?? $post;

View file

@ -1,172 +0,0 @@
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
*/
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\User;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
function lostpass_post()
{
$loginame = trim($_POST['login-name']);
if (!$loginame) {
DI::baseUrl()->redirect();
}
$condition = ['(`email` = ? OR `nickname` = ?) AND `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`', $loginame, $loginame];
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'email', 'language'], $condition);
if (!DBA::isResult($user)) {
DI::sysmsg()->addNotice(DI::l10n()->t('No valid account found.'));
DI::baseUrl()->redirect();
}
$pwdreset_token = Strings::getRandomHex(32);
$fields = [
'pwdreset' => hash('sha256', $pwdreset_token),
'pwdreset_time' => DateTimeFormat::utcNow()
];
$result = DBA::update('user', $fields, ['uid' => $user['uid']]);
if ($result) {
DI::sysmsg()->addInfo(DI::l10n()->t('Password reset request issued. Check your email.'));
}
$sitename = DI::config()->get('config', 'sitename');
$resetlink = DI::baseUrl() . '/lostpass/' . $pwdreset_token;
$preamble = Strings::deindent(DI::l10n()->t('
Dear %1$s,
A request was recently received at "%2$s" to reset your account
password. In order to confirm this request, please select the verification link
below or paste it into your web browser address bar.
If you did NOT request this change, please DO NOT follow the link
provided and ignore and/or delete this email, the request will expire shortly.
Your password will not be changed unless we can verify that you
issued this request.', $user['username'], $sitename));
$body = Strings::deindent(DI::l10n()->t('
Follow this link soon to verify your identity:
%1$s
You will then receive a follow-up message containing the new password.
You may change that password from your account settings page after logging in.
The login details are as follows:
Site Location: %2$s
Login Name: %3$s', $resetlink, DI::baseUrl(), $user['nickname']));
$email = DI::emailer()
->newSystemMail()
->withMessage(DI::l10n()->t('Password reset requested at %s', $sitename), $preamble, $body)
->forUser($user)
->withRecipient($user['email'])
->build();
DI::emailer()->send($email);
DI::baseUrl()->redirect();
}
function lostpass_content()
{
if (DI::args()->getArgc() > 1) {
$pwdreset_token = DI::args()->getArgv()[1];
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'email', 'pwdreset_time', 'language'], ['pwdreset' => hash('sha256', $pwdreset_token)]);
if (!DBA::isResult($user)) {
DI::sysmsg()->addNotice(DI::l10n()->t("Request could not be verified. \x28You may have previously submitted it.\x29 Password reset failed."));
return lostpass_form();
}
// Password reset requests expire in 60 minutes
if ($user['pwdreset_time'] < DateTimeFormat::utc('now - 1 hour')) {
$fields = [
'pwdreset' => null,
'pwdreset_time' => null
];
DBA::update('user', $fields, ['uid' => $user['uid']]);
DI::sysmsg()->addNotice(DI::l10n()->t('Request has expired, please make a new one.'));
return lostpass_form();
}
return lostpass_generate_password($user);
} else {
return lostpass_form();
}
}
function lostpass_form()
{
$tpl = Renderer::getMarkupTemplate('lostpass.tpl');
$o = Renderer::replaceMacros($tpl, [
'$title' => DI::l10n()->t('Forgot your Password?'),
'$desc' => DI::l10n()->t('Enter your email address and submit to have your password reset. Then check your email for further instructions.'),
'$name' => DI::l10n()->t('Nickname or email'),
'$submit' => DI::l10n()->t('Reset my password')
]);
return $o;
}
function lostpass_generate_password($user)
{
$o = '';
$new_password = User::generateNewPassword();
$result = User::updatePassword($user['uid'], $new_password);
if (DBA::isResult($result)) {
$tpl = Renderer::getMarkupTemplate('pwdreset.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$lbl1' => DI::l10n()->t('Password Reset'),
'$lbl2' => DI::l10n()->t('Your password has been reset as requested.'),
'$lbl3' => DI::l10n()->t('Your new password is'),
'$lbl4' => DI::l10n()->t('Save or copy your new password - and then'),
'$lbl5' => '<a href="' . DI::baseUrl() . '">' . DI::l10n()->t('click here to login') . '</a>.',
'$lbl6' => DI::l10n()->t('Your password may be changed from the <em>Settings</em> page after successful login.'),
'$newpass' => $new_password,
]);
DI::sysmsg()->addInfo(DI::l10n()->t("Your password has been reset."));
$sitename = DI::config()->get('config', 'sitename');
$preamble = Strings::deindent(DI::l10n()->t('
Dear %1$s,
Your password has been changed as requested. Please retain this
information for your records ' . "\x28" . 'or change your password immediately to
something that you will remember' . "\x29" . '.
', $user['username']));
$body = Strings::deindent(DI::l10n()->t('
Your login details are as follows:
Site Location: %1$s
Login Name: %2$s
Password: %3$s
You may change that password from your account settings page after logging in.
', DI::baseUrl(), $user['nickname'], $new_password));
$email = DI::emailer()
->newSystemMail()
->withMessage(DI::l10n()->t('Your password has been changed at %s', $sitename), $preamble, $body)
->forUser($user)
->withRecipient($user['email'])
->build();
DI::emailer()->send($email);
}
return $o;
}

View file

@ -46,7 +46,7 @@ function message_init()
$head_tpl = Renderer::getMarkupTemplate('message-head.tpl'); $head_tpl = Renderer::getMarkupTemplate('message-head.tpl');
DI::page()['htmlhead'] .= Renderer::replaceMacros($head_tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($head_tpl, [
'$base' => (string)DI::baseUrl() '$base' => (string) DI::baseUrl(),
]); ]);
} }
@ -147,7 +147,7 @@ function message_content()
DI::baseUrl()->redirect('message'); DI::baseUrl()->redirect('message');
} }
DI::baseUrl()->redirect('message/' . $conversation['id'] ); DI::baseUrl()->redirect('message/' . $conversation['id']);
} else { } else {
$parentmail = DBA::selectFirst('mail', ['parent-uri'], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]); $parentmail = DBA::selectFirst('mail', ['parent-uri'], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]);
if (DBA::isResult($parentmail)) { if (DBA::isResult($parentmail)) {
@ -167,7 +167,7 @@ function message_content()
$tpl = Renderer::getMarkupTemplate('msg-header.tpl'); $tpl = Renderer::getMarkupTemplate('msg-header.tpl');
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$nickname' => DI::userSession()->getLocalUserNickname(), '$nickname' => DI::userSession()->getLocalUserNickname(),
'$linkurl' => DI::l10n()->t('Please enter a link URL:') '$linkurl' => DI::l10n()->t('Please enter a link URL:'),
]); ]);
$recipientId = DI::args()->getArgv()[2] ?? null; $recipientId = DI::args()->getArgv()[2] ?? null;
@ -188,7 +188,7 @@ function message_content()
'$upload' => DI::l10n()->t('Upload photo'), '$upload' => DI::l10n()->t('Upload photo'),
'$insert' => DI::l10n()->t('Insert web link'), '$insert' => DI::l10n()->t('Insert web link'),
'$wait' => DI::l10n()->t('Please wait'), '$wait' => DI::l10n()->t('Please wait'),
'$submit' => DI::l10n()->t('Send Message') '$submit' => DI::l10n()->t('Send Message'),
]); ]);
return $o; return $o;
} }
@ -232,14 +232,14 @@ function message_content()
WHERE `mail`.`uid` = ? AND `mail`.`id` = ? WHERE `mail`.`uid` = ? AND `mail`.`id` = ?
LIMIT 1", LIMIT 1",
DI::userSession()->getLocalUserId(), DI::userSession()->getLocalUserId(),
DI::args()->getArgv()[1] DI::args()->getArgv()[1],
); );
if (DBA::isResult($message)) { if (DBA::isResult($message)) {
$contact_id = $message['contact-id']; $contact_id = $message['contact-id'];
$params = [ $params = [
DI::userSession()->getLocalUserId(), DI::userSession()->getLocalUserId(),
$message['parent-uri'] $message['parent-uri'],
]; ];
if ($message['convid']) { if ($message['convid']) {
@ -256,7 +256,7 @@ function message_content()
WHERE `mail`.`uid` = ? WHERE `mail`.`uid` = ?
$sql_extra $sql_extra
ORDER BY `mail`.`created` ASC", ORDER BY `mail`.`created` ASC",
...$params ...$params,
); );
$messages = DBA::toArray($messages_stmt); $messages = DBA::toArray($messages_stmt);
@ -274,7 +274,7 @@ function message_content()
$tpl = Renderer::getMarkupTemplate('msg-header.tpl'); $tpl = Renderer::getMarkupTemplate('msg-header.tpl');
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$nickname' => DI::userSession()->getLocalUserNickname(), '$nickname' => DI::userSession()->getLocalUserNickname(),
'$linkurl' => DI::l10n()->t('Please enter a link URL:') '$linkurl' => DI::l10n()->t('Please enter a link URL:'),
]); ]);
$mails = []; $mails = [];
@ -345,7 +345,7 @@ function message_content()
'$upload' => DI::l10n()->t('Upload photo'), '$upload' => DI::l10n()->t('Upload photo'),
'$insert' => DI::l10n()->t('Insert web link'), '$insert' => DI::l10n()->t('Insert web link'),
'$submit' => DI::l10n()->t('Send Message'), '$submit' => DI::l10n()->t('Send Message'),
'$wait' => DI::l10n()->t('Please wait') '$wait' => DI::l10n()->t('Please wait'),
]); ]);
return $o; return $o;

View file

@ -1,83 +0,0 @@
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
*/
use Friendica\Content\Conversation;
use Friendica\Content\Nav;
use Friendica\Content\Pager;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\BaseProfile;
function notes_init()
{
if (! DI::userSession()->getLocalUserId()) {
return;
}
Nav::setSelected('home');
}
function notes_content(bool $update = false)
{
$contactId = DI::appHelper()->getContactId();
if (!DI::userSession()->getLocalUserId()) {
DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.'));
return;
}
$o = BaseProfile::getTabsHTML('notes', true, DI::userSession()->getLocalUserNickname(), false);
if (!$update) {
$o .= '<h3>' . DI::l10n()->t('Personal Notes') . '</h3>';
$x = [
'lockstate' => 'lock',
'acl' => \Friendica\Core\ACL::getSelfOnlyHTML(DI::userSession()->getLocalUserId(), DI::l10n()->t('Personal notes are visible only by yourself.')),
'button' => DI::l10n()->t('Save'),
'acl_data' => '',
];
$o .= DI::conversation()->statusEditor($x, $contactId);
}
$condition = ['uid' => DI::userSession()->getLocalUserId(), 'post-type' => Item::PT_PERSONAL_NOTE, 'gravity' => Item::GRAVITY_PARENT,
'contact-id'=> $contactId];
if (DI::mode()->isMobile()) {
$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network',
DI::config()->get('system', 'itemspage_network_mobile'));
} else {
$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_network',
DI::config()->get('system', 'itemspage_network'));
}
$pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage);
$params = ['order' => ['created' => true],
'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
$r = Post::selectThreadForUser(DI::userSession()->getLocalUserId(), ['uri-id'], $condition, $params);
$count = 0;
if (DBA::isResult($r)) {
$notes = Post::toArray($r);
$count = count($notes);
$o .= DI::conversation()->render($notes, Conversation::MODE_NOTES, $update);
}
$o .= $pager->renderMinimal($count);
return $o;
}

View file

@ -10,7 +10,6 @@
use Friendica\Content\Nav; use Friendica\Content\Nav;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Content\Text\BBCode;
use Friendica\Core\ACL; use Friendica\Core\ACL;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\System; use Friendica\Core\System;
@ -19,23 +18,15 @@ use Friendica\Database\DBStructure;
use Friendica\DI; use Friendica\DI;
use Friendica\Event\ArrayFilterEvent; use Friendica\Event\ArrayFilterEvent;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\Photo; use Friendica\Model\Photo;
use Friendica\Model\Post;
use Friendica\Model\Profile; use Friendica\Model\Profile;
use Friendica\Model\Tag;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Module\BaseProfile; use Friendica\Module\BaseProfile;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
use Friendica\Security\Security; use Friendica\Security\Security;
use Friendica\Util\Crypto;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images; use Friendica\Util\Images;
use Friendica\Util\Map;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\Temporal;
use Friendica\Util\XML;
function photos_init() function photos_init()
{ {
@ -70,26 +61,20 @@ function photos_init()
'total' => $album['total'], 'total' => $album['total'],
'url' => 'photos/' . $owner['nickname'] . '/album/' . bin2hex($album['album']), 'url' => 'photos/' . $owner['nickname'] . '/album/' . bin2hex($album['album']),
'urlencode' => urlencode($album['album']), 'urlencode' => urlencode($album['album']),
'bin2hex' => bin2hex($album['album']) 'bin2hex' => bin2hex($album['album']),
]; ];
$ret['albums'][] = $entry; $ret['albums'][] = $entry;
} }
} }
if (DI::userSession()->getLocalUserId() && $owner['uid'] == DI::userSession()->getLocalUserId()) {
$can_post = true;
} else {
$can_post = false;
}
if ($ret['success']) { if ($ret['success']) {
$photo_albums_widget = Renderer::replaceMacros(Renderer::getMarkupTemplate('photo_albums.tpl'), [ $photo_albums_widget = Renderer::replaceMacros(Renderer::getMarkupTemplate('photo_albums.tpl'), [
'$nick' => $owner['nickname'], '$nick' => $owner['nickname'],
'$title' => DI::l10n()->t('Photo Albums'), '$title' => DI::l10n()->t('Photo Albums'),
'$recent' => DI::l10n()->t('Recent Photos'), '$recent' => DI::l10n()->t('Recent Photos'),
'$albums' => $ret['albums'], '$albums' => $ret['albums'],
'$upload' => [DI::l10n()->t('Upload Photos'), 'photos/' . $owner['nickname'] . '/upload'], '$upload' => [DI::l10n()->t('Upload photo'), 'photos/' . $owner['nickname'] . '/upload'],
'$can_post' => $can_post '$can_post' => (DI::userSession()->getLocalUserId() && $owner['uid'] === DI::userSession()->getLocalUserId()),
]); ]);
} }
@ -100,7 +85,7 @@ function photos_init()
$tpl = Renderer::getMarkupTemplate("photos_head.tpl"); $tpl = Renderer::getMarkupTemplate("photos_head.tpl");
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$ispublic' => DI::l10n()->t('everybody') '$ispublic' => DI::l10n()->t('everybody'),
]); ]);
} }
@ -160,13 +145,13 @@ function photos_post()
if (DI::args()->getArgc() > 3 && DI::args()->getArgv()[2] === 'album') { if (DI::args()->getArgc() > 3 && DI::args()->getArgv()[2] === 'album') {
if (!Strings::isHex(DI::args()->getArgv()[3] ?? '')) { if (!Strings::isHex(DI::args()->getArgv()[3] ?? '')) {
DI::baseUrl()->redirect('photos/' . $user['nickname'] . '/album'); DI::baseUrl()->redirect('profile/' . $user['nickname'] . '/photos');
} }
$album = hex2bin(DI::args()->getArgv()[3]); $album = hex2bin(DI::args()->getArgv()[3]);
if (!DBA::exists('photo', ['album' => $album, 'uid' => $page_owner_uid, 'photo-type' => Photo::DEFAULT])) { if (!DBA::exists('photo', ['album' => $album, 'uid' => $page_owner_uid, 'photo-type' => Photo::DEFAULT])) {
DI::sysmsg()->addNotice(DI::l10n()->t('Album not found.')); DI::sysmsg()->addNotice(DI::l10n()->t('Album not found.'));
DI::baseUrl()->redirect('photos/' . $user['nickname'] . '/album'); DI::baseUrl()->redirect('profile/' . $user['nickname'] . '/photos');
return; // NOTREACHED return; // NOTREACHED
} }
@ -198,13 +183,13 @@ function photos_post()
"SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `contact-id` = ? AND `uid` = ? AND `album` = ?", "SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `contact-id` = ? AND `uid` = ? AND `album` = ?",
$visitor, $visitor,
$page_owner_uid, $page_owner_uid,
$album $album,
)); ));
} else { } else {
$r = DBA::toArray(DBA::p( $r = DBA::toArray(DBA::p(
"SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `uid` = ? AND `album` = ?", "SELECT distinct(`resource-id`) AS `rid` FROM `photo` WHERE `uid` = ? AND `album` = ?",
DI::userSession()->getLocalUserId(), DI::userSession()->getLocalUserId(),
$album $album,
)); ));
} }
@ -216,9 +201,6 @@ function photos_post()
// remove the associated photos // remove the associated photos
Photo::delete(['resource-id' => $res, 'uid' => $page_owner_uid]); Photo::delete(['resource-id' => $res, 'uid' => $page_owner_uid]);
// find and delete the corresponding item with all the comments and likes/dislikes
Item::deleteForUser(['resource-id' => $res, 'uid' => $page_owner_uid], $page_owner_uid);
// Update the photo albums cache // Update the photo albums cache
Photo::clearAlbumCache($page_owner_uid); Photo::clearAlbumCache($page_owner_uid);
DI::sysmsg()->addNotice(DI::l10n()->t('Album successfully deleted')); DI::sysmsg()->addNotice(DI::l10n()->t('Album successfully deleted'));
@ -227,7 +209,7 @@ function photos_post()
} }
} }
DI::baseUrl()->redirect('photos/' . $user['nickname'] . '/album'); DI::baseUrl()->redirect('profile/' . $user['nickname'] . '/photos');
} }
if (DI::args()->getArgc() > 3 && DI::args()->getArgv()[2] === 'image') { if (DI::args()->getArgc() > 3 && DI::args()->getArgv()[2] === 'image') {
@ -249,8 +231,6 @@ function photos_post()
if (DBA::isResult($photo)) { if (DBA::isResult($photo)) {
Photo::delete(['uid' => $page_owner_uid, 'resource-id' => $photo['resource-id']]); Photo::delete(['uid' => $page_owner_uid, 'resource-id' => $photo['resource-id']]);
Item::deleteForUser(['resource-id' => $photo['resource-id'], 'uid' => $page_owner_uid], $page_owner_uid);
// Update the photo albums cache // Update the photo albums cache
Photo::clearAlbumCache($page_owner_uid); Photo::clearAlbumCache($page_owner_uid);
} else { } else {
@ -264,8 +244,6 @@ function photos_post()
if (DI::args()->getArgc() > 2 && (!empty($_POST['desc']) || !empty($_POST['newtag']) || isset($_POST['albname']))) { if (DI::args()->getArgc() > 2 && (!empty($_POST['desc']) || !empty($_POST['newtag']) || isset($_POST['albname']))) {
$desc = !empty($_POST['desc']) ? trim($_POST['desc']) : ''; $desc = !empty($_POST['desc']) ? trim($_POST['desc']) : '';
$rawtags = !empty($_POST['newtag']) ? trim($_POST['newtag']) : '';
$item_id = !empty($_POST['item_id']) ? intval($_POST['item_id']) : 0;
$albname = !empty($_POST['albname']) ? trim($_POST['albname']) : ''; $albname = !empty($_POST['albname']) ? trim($_POST['albname']) : '';
$origaname = !empty($_POST['origaname']) ? trim($_POST['origaname']) : ''; $origaname = !empty($_POST['origaname']) ? trim($_POST['origaname']) : '';
@ -317,10 +295,9 @@ function photos_post()
if (DBA::isResult($photos)) { if (DBA::isResult($photos)) {
$photo = $photos[0]; $photo = $photos[0];
$ext = Images::getExtensionByMimeType($photo['type']);
Photo::update( Photo::update(
['desc' => $desc, 'album' => $albname, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny], ['desc' => $desc, 'album' => $albname, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny],
['resource-id' => $resource_id, 'uid' => $page_owner_uid] ['resource-id' => $resource_id, 'uid' => $page_owner_uid],
); );
// Update the photo albums cache if album name was changed // Update the photo albums cache if album name was changed
@ -329,189 +306,6 @@ function photos_post()
} }
} }
if (DBA::isResult($photos) && !$item_id) {
// Create item container
$title = '';
$uri = Item::newURI();
$arr = [];
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['post-type'] = Item::PT_IMAGE;
$arr['wall'] = 1;
$arr['resource-id'] = $photo['resource-id'];
$arr['contact-id'] = $owner_record['id'];
$arr['owner-name'] = $owner_record['name'];
$arr['owner-link'] = $owner_record['url'];
$arr['owner-avatar'] = $owner_record['thumb'];
$arr['author-name'] = $owner_record['name'];
$arr['author-link'] = $owner_record['url'];
$arr['author-avatar'] = $owner_record['thumb'];
$arr['title'] = $title;
$arr['allow_cid'] = $photo['allow_cid'];
$arr['allow_gid'] = $photo['allow_gid'];
$arr['deny_cid'] = $photo['deny_cid'];
$arr['deny_gid'] = $photo['deny_gid'];
$arr['visible'] = 0;
$arr['origin'] = 1;
$arr['body'] = Images::getBBCodeByResource($photo['resource-id'], $user['nickname'], $photo['scale'], $ext);
$item_id = Item::insert($arr);
}
if ($item_id) {
$item = Post::selectFirst(['inform', 'uri-id'], ['id' => $item_id, 'uid' => $page_owner_uid]);
if (DBA::isResult($item)) {
$old_inform = $item['inform'];
}
}
if (strlen($rawtags)) {
$inform = '';
// if the new tag doesn't have a namespace specifier (@foo or #foo) give it a hashtag
$x = substr($rawtags, 0, 1);
if ($x !== '@' && $x !== '#') {
$rawtags = '#' . $rawtags;
}
$taginfo = [];
$tags = BBCode::getTags($rawtags);
if (count($tags)) {
foreach ($tags as $tag) {
if (strpos($tag, '@') === 0) {
$profile = '';
$name = substr($tag, 1);
$contact = Contact::getByURL($name);
if (empty($contact)) {
$newname = $name;
if (strrpos($newname, '+')) {
$tagcid = intval(substr($newname, strrpos($newname, '+') + 1));
} else {
$tagcid = 0;
}
if ($tagcid) {
$contact = DBA::selectFirst('contact', [], ['id' => $tagcid, 'uid' => $page_owner_uid]);
} else {
$newname = str_replace('_', ' ', $name);
//select someone from this user's contacts by name
$contact = DBA::selectFirst('contact', [], ['name' => $newname, 'uid' => $page_owner_uid]);
if (!DBA::isResult($contact)) {
//select someone by attag or nick and the name passed in
$contact = DBA::selectFirst(
'contact',
[],
['(`attag` = ? OR `nick` = ?) AND `uid` = ?', $name, $name, $page_owner_uid],
['order' => ['attag' => true]]
);
}
}
}
if (DBA::isResult($contact)) {
$newname = $contact['name'];
$profile = $contact['url'];
$notify = 'cid:' . $contact['id'];
if (strlen($inform)) {
$inform .= ',';
}
$inform .= $notify;
}
if ($profile) {
if (!empty($contact)) {
$taginfo[] = [$newname, $profile, $notify, $contact];
} else {
$taginfo[] = [$newname, $profile, $notify, null];
}
$profile = str_replace(',', '%2c', $profile);
if (!empty($item['uri-id'])) {
Tag::store($item['uri-id'], Tag::MENTION, $newname, $profile);
}
}
} elseif (strpos($tag, '#') === 0) {
$tagname = substr($tag, 1);
if (!empty($item['uri-id'])) {
Tag::store($item['uri-id'], Tag::HASHTAG, $tagname);
}
}
}
}
$newinform = $old_inform ?? '';
if (strlen($newinform) && strlen($inform)) {
$newinform .= ',';
}
$newinform .= $inform;
$fields = ['inform' => $newinform, 'edited' => DateTimeFormat::utcNow(), 'changed' => DateTimeFormat::utcNow()];
$condition = ['id' => $item_id];
Item::update($fields, $condition);
$best = 0;
foreach ($photos as $scales) {
if (intval($scales['scale']) == 2) {
$best = 2;
break;
}
if (intval($scales['scale']) == 4) {
$best = 4;
break;
}
}
if (count($taginfo)) {
foreach ($taginfo as $tagged) {
$uri = Item::newURI();
$arr = [
'guid' => System::createUUID(),
'uid' => $page_owner_uid,
'uri' => $uri,
'wall' => 1,
'contact-id' => $owner_record['id'],
'owner-name' => $owner_record['name'],
'owner-link' => $owner_record['url'],
'owner-avatar' => $owner_record['thumb'],
'author-name' => $owner_record['name'],
'author-link' => $owner_record['url'],
'author-avatar' => $owner_record['thumb'],
'title' => '',
'allow_cid' => $photo['allow_cid'],
'allow_gid' => $photo['allow_gid'],
'deny_cid' => $photo['deny_cid'],
'deny_gid' => $photo['deny_gid'],
'visible' => 0,
'verb' => Activity::TAG,
'gravity' => Item::GRAVITY_PARENT,
'object-type' => Activity\ObjectType::PERSON,
'target-type' => Activity\ObjectType::IMAGE,
'inform' => $tagged[2],
'origin' => 1,
'body' => DI::l10n()->t('%1$s was tagged in %2$s by %3$s', '[url=' . $tagged[1] . ']' . $tagged[0] . '[/url]', '[url=' . DI::baseUrl() . '/photos/' . $owner_record['nickname'] . '/image/' . $photo['resource-id'] . ']' . DI::l10n()->t('a photo') . '[/url]', '[url=' . $owner_record['url'] . ']' . $owner_record['name'] . '[/url]') . "\n\n" . '[url=' . DI::baseUrl() . '/photos/' . $owner_record['nickname'] . '/image/' . $photo['resource-id'] . ']' . '[img]' . DI::baseUrl() . '/photo/' . $photo['resource-id'] . '-' . $best . '.' . $ext . '[/img][/url]' . "\n",
'object' => '<object><type>' . Activity\ObjectType::PERSON . '</type><title>' . $tagged[0] . '</title><id>' . $tagged[1] . '/' . $tagged[0] . '</id><link>' . XML::escape('<link rel="alternate" type="text/html" href="' . $tagged[1] . '" />' . "\n"),
'target' => '<target><type>' . Activity\ObjectType::IMAGE . '</type><title>' . $photo['desc'] . '</title><id>' . DI::baseUrl() . '/photos/' . $owner_record['nickname'] . '/image/' . $photo['resource-id'] . '</id><link>' . XML::escape('<link rel="alternate" type="text/html" href="' . DI::baseUrl() . '/photos/' . $owner_record['nickname'] . '/image/' . $photo['resource-id'] . '" />' . "\n" . '<link rel="preview" type="' . $photo['type'] . '" href="' . DI::baseUrl() . '/photo/' . $photo['resource-id'] . '-' . $best . '.' . $ext . '" />') . '</link></target>',
];
if ($tagged[3]) {
$arr['object'] .= XML::escape('<link rel="photo" type="' . $photo['type'] . '" href="' . $tagged[3]['photo'] . '" />' . "\n");
}
$arr['object'] .= '</link></object>' . "\n";
Item::insert($arr);
}
}
}
DI::baseUrl()->redirect($_SESSION['photo_return']); DI::baseUrl()->redirect($_SESSION['photo_return']);
return; // NOTREACHED return; // NOTREACHED
} }
@ -645,18 +439,21 @@ function photos_content()
$ret = [ $ret = [
'post_url' => 'profile/' . $user['nickname'] . '/photos', 'post_url' => 'profile/' . $user['nickname'] . '/photos',
'addon_text' => $uploader, 'addon_text' => $uploader,
'default_upload' => true 'default_upload' => true,
]; ];
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
$eventDispatcher->dispatch( $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::PHOTO_UPLOAD_FORM, $ret) new ArrayFilterEvent(ArrayFilterEvent::PHOTO_UPLOAD_FORM, $ret),
); );
// Determine if we're in album context (uploading to a specific album)
$is_album_context = !empty($selname);
$default_upload_box = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_box.tpl'), []); $default_upload_box = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_box.tpl'), []);
$default_upload_submit = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_submit.tpl'), [ $default_upload_submit = Renderer::replaceMacros(Renderer::getMarkupTemplate('photos_default_uploader_submit.tpl'), [
'$submit' => DI::l10n()->t('Upload selected picture'), '$submit' => $is_album_context ? DI::l10n()->t('Upload photo to this album') : DI::l10n()->t('Upload selected photo'),
]); ]);
// Get the relevant size limits for uploads. Abbreviated var names: MaxImageSize -> mis; upload_max_filesize -> umf // Get the relevant size limits for uploads. Abbreviated var names: MaxImageSize -> mis; upload_max_filesize -> umf
@ -679,13 +476,12 @@ function photos_content()
$aclselect_e = ($visitor ? '' : ACL::getFullSelectorHTML(DI::page(), DI::userSession()->getLocalUserId())); $aclselect_e = ($visitor ? '' : ACL::getFullSelectorHTML(DI::page(), DI::userSession()->getLocalUserId()));
$o .= Renderer::replaceMacros($tpl, [ $o .= Renderer::replaceMacros($tpl, [
'$pagename' => DI::l10n()->t('Upload Photos'), '$pagename' => $is_album_context ? DI::l10n()->t('Upload Photos to %s', $selname) : DI::l10n()->t('Upload Photos'),
'$sessid' => session_id(), '$sessid' => session_id(),
'$usage' => $usage_message, '$usage' => $usage_message,
'$nickname' => $user['nickname'], '$nickname' => $user['nickname'],
'$newalbum' => DI::l10n()->t('New album name: '), '$albumtext_label' => DI::l10n()->t('Album name: '),
'$existalbumtext' => DI::l10n()->t('or select existing album:'), '$albumtext_description' => DI::l10n()->t('If you want to add this photo to an album, begin typing its name, and existing albums will be suggested, which you can select. If you choose something new, it will be created.'),
'$nosharetext' => DI::l10n()->t('Do not show a status post for this upload'),
'$albumselect' => $albumselect, '$albumselect' => $albumselect,
'$selname' => $selname, '$selname' => $selname,
'$permissions' => DI::l10n()->t('Permissions'), '$permissions' => DI::l10n()->t('Permissions'),
@ -695,6 +491,8 @@ function photos_content()
'$default_upload_box' => ($ret['default_upload'] ? $default_upload_box : ''), '$default_upload_box' => ($ret['default_upload'] ? $default_upload_box : ''),
'$default_upload_submit' => ($ret['default_upload'] ? $default_upload_submit : ''), '$default_upload_submit' => ($ret['default_upload'] ? $default_upload_submit : ''),
'$uploadurl' => $ret['post_url'], '$uploadurl' => $ret['post_url'],
'$is_album_context' => $is_album_context,
'$preselected_album' => $selname,
// ACL permissions box // ACL permissions box
'$return_path' => DI::args()->getQueryString(), '$return_path' => DI::args()->getQueryString(),
@ -707,7 +505,7 @@ function photos_content()
if ($datatype === 'album') { if ($datatype === 'album') {
// if $datum is not a valid hex, redirect to the default page // if $datum is not a valid hex, redirect to the default page
if (is_null($datum) || !Strings::isHex($datum)) { if (is_null($datum) || !Strings::isHex($datum)) {
DI::baseUrl()->redirect('photos/' . $user['nickname'] . '/album'); DI::baseUrl()->redirect('profile/' . $user['nickname'] . '/photos');
} }
$album = hex2bin($datum); $album = hex2bin($datum);
@ -720,7 +518,7 @@ function photos_content()
"SELECT `resource-id`, MAX(`scale`) AS `scale` FROM `photo` WHERE `uid` = ? AND `album` = ? "SELECT `resource-id`, MAX(`scale`) AS `scale` FROM `photo` WHERE `uid` = ? AND `album` = ?
AND `scale` <= 4 $sql_extra GROUP BY `resource-id`", AND `scale` <= 4 $sql_extra GROUP BY `resource-id`",
$owner_uid, $owner_uid,
$album $album,
)); ));
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$total = count($r); $total = count($r);
@ -745,7 +543,7 @@ function photos_content()
intval($owner_uid), intval($owner_uid),
DBA::escape($album), DBA::escape($album),
$pager->getStart(), $pager->getStart(),
$pager->getItemsPerPage() $pager->getItemsPerPage(),
)); ));
if ($cmd === 'drop') { if ($cmd === 'drop') {
@ -805,7 +603,7 @@ function photos_content()
$photos[] = [ $photos[] = [
'id' => $rr['id'], 'id' => $rr['id'],
'twist' => ' ' . ($twist ? 'rotleft' : 'rotright') . rand(2, 4), 'twist' => ' ' . ($twist ? 'rotleft' : 'rotright') . random_int(2, 4),
'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id'] 'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id']
. ($order_field === 'created' ? '?order=created' : ''), . ($order_field === 'created' ? '?order=created' : ''),
'title' => DI::l10n()->t('View Photo'), 'title' => DI::l10n()->t('View Photo'),
@ -823,7 +621,7 @@ function photos_content()
'$photos' => $photos, '$photos' => $photos,
'$album' => $album, '$album' => $album,
'$can_post' => $can_post, '$can_post' => $can_post,
'$upload' => [DI::l10n()->t('Upload Photos'), 'photos/' . $user['nickname'] . '/upload/' . bin2hex($album)], '$upload' => [DI::l10n()->t('Upload photo'), 'photos/' . $user['nickname'] . '/upload/' . bin2hex($album)],
'$order' => $order, '$order' => $order,
'$edit' => $edit, '$edit' => $edit,
'$drop' => $drop, '$drop' => $drop,
@ -917,7 +715,7 @@ function photos_content()
$tpl = Renderer::getMarkupTemplate('photo_edit_head.tpl'); $tpl = Renderer::getMarkupTemplate('photo_edit_head.tpl');
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [ DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, [
'$prevlink' => $prevlink, '$prevlink' => $prevlink,
'$nextlink' => $nextlink '$nextlink' => $nextlink,
]); ]);
if ($prevlink) { if ($prevlink) {
@ -976,9 +774,7 @@ function photos_content()
'filename' => $hires['filename'], 'filename' => $hires['filename'],
]; ];
$map = null; $total = 0;
$link_item = [];
$total = 0;
// Do we have an item for this photo? // Do we have an item for this photo?
@ -988,46 +784,6 @@ function photos_content()
// The difference is that we won't be displaying the conversation head item // The difference is that we won't be displaying the conversation head item
// as a "post" but displaying instead the photo it is linked to // as a "post" but displaying instead the photo it is linked to
$link_item = Post::selectFirst([], ["`resource-id` = ?" . $sql_extra, $datum]);
if (!empty($link_item['parent']) && !empty($link_item['uid'])) {
$condition = ["`parent` = ? AND `gravity` = ?", $link_item['parent'], Item::GRAVITY_COMMENT];
$total = Post::count($condition);
$pager = new Pager(DI::l10n(), DI::args()->getQueryString());
$params = ['order' => ['id'], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
$items = Post::toArray(Post::selectForUser($link_item['uid'], array_merge(Item::ITEM_FIELDLIST, ['author-alias']), $condition, $params));
if (DI::userSession()->getLocalUserId() == $link_item['uid']) {
Item::update(['unseen' => false], ['parent' => $link_item['parent']]);
}
}
if (!empty($link_item['coord'])) {
$map = Map::byCoordinates($link_item['coord']);
}
$tags = null;
if (!empty($link_item['id'])) {
// parse tags and add links
$tag_arr = [];
foreach (explode(',', Tag::getCSVByURIId($link_item['uri-id'])) as $tag_name) {
if ($tag_name) {
$tag_arr[] = [
'name' => BBCode::toPlaintext($tag_name),
'removeurl' => 'post/' . $link_item['id'] . '/tag/remove/' . bin2hex($tag_name) . '?return=' . urlencode(DI::args()->getCommand()),
];
}
}
$tags = ['title' => DI::l10n()->t('Tags'), 'tags' => $tag_arr];
if ($cmd === 'edit' && !empty($tag_arr)) {
$tags['removeanyurl'] = 'post/' . $link_item['id'] . '/tag/remove?return=' . urlencode(DI::args()->getCommand());
$tags['removetitle'] = DI::l10n()->t('[Select tags to remove]');
}
}
$edit = null; $edit = null;
if ($cmd === 'edit' && $can_post) { if ($cmd === 'edit' && $can_post) {
@ -1041,7 +797,6 @@ function photos_content()
'$id' => $ph[0]['id'], '$id' => $ph[0]['id'],
'$album' => ['albname', DI::l10n()->t('New album name'), $album_e, ''], '$album' => ['albname', DI::l10n()->t('New album name'), $album_e, ''],
'$caption' => ['desc', DI::l10n()->t('Caption'), $caption_e, ''], '$caption' => ['desc', DI::l10n()->t('Caption'), $caption_e, ''],
'$tags' => ['newtag', DI::l10n()->t('Add a Tag'), "", "", "", "", "", DI::l10n()->t('Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping')],
'$rotate_none' => ['rotate', DI::l10n()->t('Do not rotate'), 0, '', true], '$rotate_none' => ['rotate', DI::l10n()->t('Do not rotate'), 0, '', true],
'$rotate_cw' => ['rotate', DI::l10n()->t("Rotate CW \x28right\x29"), 1, ''], '$rotate_cw' => ['rotate', DI::l10n()->t("Rotate CW \x28right\x29"), 1, ''],
'$rotate_ccw' => ['rotate', DI::l10n()->t("Rotate CCW \x28left\x29"), 2, ''], '$rotate_ccw' => ['rotate', DI::l10n()->t("Rotate CCW \x28left\x29"), 2, ''],
@ -1051,219 +806,14 @@ function photos_content()
'$permissions' => DI::l10n()->t('Permissions'), '$permissions' => DI::l10n()->t('Permissions'),
'$aclselect' => $aclselect_e, '$aclselect' => $aclselect_e,
'$item_id' => $link_item['id'] ?? 0, '$submit' => DI::l10n()->t('Save changes'),
'$submit' => DI::l10n()->t('Save changes'), '$delete' => DI::l10n()->t('Delete Photo'),
'$delete' => DI::l10n()->t('Delete Photo'),
// ACL permissions box // ACL permissions box
'$return_path' => DI::args()->getQueryString(), '$return_path' => DI::args()->getQueryString(),
]); ]);
} }
$like = '';
$dislike = '';
$likebuttons = '';
$comments = '';
$paginate = '';
if (!empty($link_item['id']) && !empty($link_item['uri'])) {
$cmnt_tpl = Renderer::getMarkupTemplate('comment_item.tpl');
$tpl = Renderer::getMarkupTemplate('photo_item.tpl');
$return_path = DI::args()->getCommand();
$addonHelper = DI::addonHelper();
if (!DBA::isResult($items)) {
if (($can_post || Security::canWriteToUserWall($owner_uid))) {
/*
* Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook
*/
$qcomment = null;
if ($addonHelper->isAddonEnabled('qcomment')) {
$words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words');
$qcomment = $words ? explode("\n", $words) : [];
}
$comments .= Renderer::replaceMacros($cmnt_tpl, [
'$return_path' => '',
'$jsreload' => $return_path,
'$id' => $link_item['id'],
'$parent' => $link_item['id'],
'$profile_uid' => $owner_uid,
'$mylink' => $contact['url'],
'$mytitle' => DI::l10n()->t('This is you'),
'$myphoto' => $contact['thumb'],
'$comment' => DI::l10n()->t('Comment'),
'$submit' => DI::l10n()->t('Comment'),
'$preview' => DI::l10n()->t('Preview'),
'$loading' => DI::l10n()->t('Loading...'),
'$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12),
]);
}
}
$conv_responses = [
'like' => [],
'dislike' => [],
'attendyes' => [],
'attendno' => [],
'attendmaybe' => []
];
if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'hide_dislike')) {
unset($conv_responses['dislike']);
}
// display comments
if (DBA::isResult($items)) {
foreach ($items as $item) {
DI::conversation()->builtinActivityPuller($item, $conv_responses);
}
if (!empty($conv_responses['like'][$link_item['uri']])) {
$like = DI::conversation()->formatActivity($conv_responses['like'][$link_item['uri']]['links'], 'like', $link_item['id'], '', []);
}
if (!empty($conv_responses['dislike'][$link_item['uri']])) {
$dislike = DI::conversation()->formatActivity($conv_responses['dislike'][$link_item['uri']]['links'], 'dislike', $link_item['id'], '', []);
}
if (($can_post || Security::canWriteToUserWall($owner_uid))) {
/*
* Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook
*/
$qcomment = null;
if ($addonHelper->isAddonEnabled('qcomment')) {
$words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words');
$qcomment = $words ? explode("\n", $words) : [];
}
$comments .= Renderer::replaceMacros($cmnt_tpl, [
'$return_path' => '',
'$jsreload' => $return_path,
'$id' => $link_item['id'],
'$parent' => $link_item['id'],
'$profile_uid' => $owner_uid,
'$mylink' => $contact['url'],
'$mytitle' => DI::l10n()->t('This is you'),
'$myphoto' => $contact['thumb'],
'$comment' => DI::l10n()->t('Comment'),
'$submit' => DI::l10n()->t('Comment'),
'$preview' => DI::l10n()->t('Preview'),
'$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12),
]);
}
foreach ($items as $item) {
$comment = '';
$template = $tpl;
$activity = DI::activity();
if (($activity->match($item['verb'], Activity::LIKE) ||
$activity->match($item['verb'], Activity::DISLIKE)) &&
($item['gravity'] != Item::GRAVITY_PARENT)
) {
continue;
}
$author = [
'uid' => 0,
'id' => $item['author-id'],
'network' => $item['author-network'],
'url' => $item['author-link'],
'alias' => $item['author-alias']
];
$profile_url = Contact::magicLinkByContact($author);
if (strpos($profile_url, 'contact/redir/') === 0) {
$sparkle = ' sparkle';
} else {
$sparkle = '';
}
$dropping = (($item['contact-id'] == $contact_id) || ($item['uid'] == DI::userSession()->getLocalUserId()));
$drop = [
'dropping' => $dropping,
'pagedrop' => false,
'select' => DI::l10n()->t('Select'),
'delete' => DI::l10n()->t('Delete'),
];
$title_e = $item['title'];
$body_e = BBCode::convertForUriId($item['uri-id'], $item['body']);
$comments .= Renderer::replaceMacros($template, [
'$id' => $item['id'],
'$profile_url' => $profile_url,
'$name' => $item['author-name'],
'$thumb' => $item['author-avatar'],
'$sparkle' => $sparkle,
'$title' => $title_e,
'$body' => $body_e,
'$ago' => Temporal::getRelativeDate($item['created']),
'$indent' => (($item['parent'] != $item['id']) ? ' comment' : ''),
'$drop' => $drop,
'$comment' => $comment
]);
if (($can_post || Security::canWriteToUserWall($owner_uid))) {
/*
* Hmmm, code depending on the presence of a particular addon?
* This should be better if done by a hook
*/
$qcomment = null;
if ($addonHelper->isAddonEnabled('qcomment')) {
$words = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'qcomment', 'words');
$qcomment = $words ? explode("\n", $words) : [];
}
$comments .= Renderer::replaceMacros($cmnt_tpl, [
'$return_path' => '',
'$jsreload' => $return_path,
'$id' => $item['id'],
'$parent' => $item['parent'],
'$profile_uid' => $owner_uid,
'$mylink' => $contact['url'],
'$mytitle' => DI::l10n()->t('This is you'),
'$myphoto' => $contact['thumb'],
'$comment' => DI::l10n()->t('Comment'),
'$submit' => DI::l10n()->t('Comment'),
'$preview' => DI::l10n()->t('Preview'),
'$qcomment' => $qcomment,
'$rand_num' => Crypto::randomDigits(12),
]);
}
}
}
$responses = [];
foreach ($conv_responses as $verb => $activity) {
if (isset($activity[$link_item['uri']])) {
$responses[$verb] = $activity[$link_item['uri']];
}
}
if ($cmd === 'view' && ($can_post || Security::canWriteToUserWall($owner_uid))) {
$like_tpl = Renderer::getMarkupTemplate('like_noshare.tpl');
$likebuttons = Renderer::replaceMacros($like_tpl, [
'$id' => $link_item['id'],
'$like' => DI::l10n()->t('Like'),
'$like_title' => DI::l10n()->t('I like this (toggle)'),
'$dislike' => DI::l10n()->t('Dislike'),
'$wait' => DI::l10n()->t('Please wait'),
'$dislike_title' => DI::l10n()->t('I don\'t like this (toggle)'),
'$hide_dislike' => DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'hide_dislike'),
'$responses' => $responses,
'$return_path' => DI::args()->getQueryString(),
]);
}
$paginate = $pager->renderFull($total);
}
$photo_tpl = Renderer::getMarkupTemplate('photo_view.tpl'); $photo_tpl = Renderer::getMarkupTemplate('photo_view.tpl');
$o .= Renderer::replaceMacros($photo_tpl, [ $o .= Renderer::replaceMacros($photo_tpl, [
'$id' => $ph[0]['id'], '$id' => $ph[0]['id'],
@ -1273,19 +823,11 @@ function photos_content()
'$prevlink' => $prevlink, '$prevlink' => $prevlink,
'$nextlink' => $nextlink, '$nextlink' => $nextlink,
'$desc' => $ph[0]['desc'], '$desc' => $ph[0]['desc'],
'$tags' => $tags,
'$edit' => $edit, '$edit' => $edit,
'$edit_text' => DI::l10n()->t('Edit'), '$edit_text' => DI::l10n()->t('Edit'),
'$delete_text' => DI::l10n()->t('Delete'), '$delete_text' => DI::l10n()->t('Delete'),
'$use_as_profile_picture_text' => DI::l10n()->t('Use as profile picture'), '$use_as_profile_picture_text' => DI::l10n()->t('Use as profile picture'),
'$back_to_viewing_text' => DI::l10n()->t('Back to viewing'), '$back_text' => DI::l10n()->t('Back to viewing'),
'$map' => $map,
'$map_text' => DI::l10n()->t('Map'),
'$likebuttons' => $likebuttons,
'$like' => $like,
'$dislike' => $dislike,
'$comments' => $comments,
'$paginate' => $paginate,
]); ]);
DI::page()['htmlhead'] .= "\n" . '<meta name="twitter:card" content="summary_large_image" />' . "\n"; DI::page()['htmlhead'] .= "\n" . '<meta name="twitter:card" content="summary_large_image" />' . "\n";

View file

@ -1,34 +0,0 @@
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* See update_profile.php for documentation
*
*/
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Model\Contact;
function update_contact_content()
{
if (!empty(DI::args()->get(1)) && !empty($_GET['force'])) {
$contact = DBA::selectFirst('account-user-view', ['pid', 'deleted'], ['id' => DI::args()->get(1)]);
if (DBA::isResult($contact) && empty($contact['deleted'])) {
DI::page()['aside'] = '';
if (!empty($_GET['item'])) {
$item = Post::selectFirst(['parent'], ['id' => $_GET['item']]);
}
$text = Contact::getThreadsFromId($contact['pid'], DI::userSession()->getLocalUserId(), true, $item['parent'] ?? 0, $_GET['last_received'] ?? '');
}
}
System::htmlUpdateExit($text ?? '');
}

View file

@ -1,32 +0,0 @@
<?php
/**
* Copyright (C) 2010-2024, the Friendica project
* SPDX-FileCopyrightText: 2010-2024 the Friendica project
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* AJAX synchronisation of notes page
*/
use Friendica\Core\System;
require_once 'mod/notes.php';
function update_notes_content()
{
$profile_uid = intval($_GET['p']);
/**
*
* Grab the page inner contents by calling the content function from the profile module directly,
* but move any image src attributes to another attribute name. This is because
* some browsers will prefetch all the images for the page even if we don't need them.
* The only ones we need to fetch are those for new page additions, which we'll discover
* on the client side and then swap the image back.
*
*/
$text = notes_content($profile_uid);
System::htmlUpdateExit($text);
}

View file

@ -65,9 +65,9 @@ use Psr\Log\LoggerInterface;
*/ */
class App class App
{ {
const PLATFORM = 'Friendica'; public const PLATFORM = 'Friendica';
const CODENAME = 'Blutwurz'; public const CODENAME = 'Blutwurz';
const VERSION = '2026.01'; public const VERSION = '2026.04-dev';
/** /**
* @internal * @internal
@ -195,7 +195,7 @@ class App
$addonHelper, $addonHelper,
$this->container->create(ModuleHTTPException::class), $this->container->create(ModuleHTTPException::class),
$start_time, $start_time,
$request $request,
); );
} }
@ -339,7 +339,7 @@ class App
private function registerTemplateEngine(): void private function registerTemplateEngine(): void
{ {
Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); Renderer::registerTemplateEngine(\Friendica\Render\FriendicaSmartyEngine::class);
} }
/** /**
@ -467,11 +467,11 @@ class App
if (!$this->mode->isInstall()) { if (!$this->mode->isInstall()) {
// Force SSL redirection // Force SSL redirection
if ($this->config->get('system', 'force_ssl') && if ($this->config->get('system', 'force_ssl')
(empty($serverVars['HTTPS']) || $serverVars['HTTPS'] === 'off') && && (empty($serverVars['HTTPS']) || $serverVars['HTTPS'] === 'off')
(empty($serverVars['HTTP_X_FORWARDED_PROTO']) || $serverVars['HTTP_X_FORWARDED_PROTO'] === 'http') && && (empty($serverVars['HTTP_X_FORWARDED_PROTO']) || $serverVars['HTTP_X_FORWARDED_PROTO'] === 'http')
!empty($serverVars['REQUEST_METHOD']) && && !empty($serverVars['REQUEST_METHOD'])
$serverVars['REQUEST_METHOD'] === 'GET') { && $serverVars['REQUEST_METHOD'] === 'GET') {
System::externalRedirect($this->baseURL . '/' . $this->args->getQueryString()); System::externalRedirect($this->baseURL . '/' . $this->args->getQueryString());
} }
@ -484,8 +484,8 @@ class App
if (!empty($queryVars['zrl']) && $this->mode->isNormal() && !$this->mode->isBackend() && !$this->session->getLocalUserId()) { if (!empty($queryVars['zrl']) && $this->mode->isNormal() && !$this->mode->isBackend() && !$this->session->getLocalUserId()) {
// Only continue when the given profile link seems valid. // Only continue when the given profile link seems valid.
// Valid profile links contain a path with "/profile/" and no query parameters // Valid profile links contain a path with "/profile/" and no query parameters
if ((parse_url($queryVars['zrl'], PHP_URL_QUERY) == '') && if ((parse_url($queryVars['zrl'], PHP_URL_QUERY) == '')
strpos(parse_url($queryVars['zrl'], PHP_URL_PATH) ?? '', '/profile/') !== false) { && strpos(parse_url($queryVars['zrl'], PHP_URL_PATH) ?? '', '/profile/') !== false) {
$this->auth->setUnauthenticatedVisitor($queryVars['zrl']); $this->auth->setUnauthenticatedVisitor($queryVars['zrl']);
OpenWebAuth::zrlInit(); OpenWebAuth::zrlInit();
} else { } else {
@ -616,8 +616,8 @@ class App
/** @var Router $router */ /** @var Router $router */
$router = $this->container->create(Router::class); $router = $this->container->create(Router::class);
$moduleClass = $moduleClass ?? $router->getModuleClass(); $moduleClass ??= $router->getModuleClass();
$parameters = $router->getParameters(); $parameters = $router->getParameters();
$dice_profiler_threshold = $this->config->get('system', 'dice_profiler_threshold', 0); $dice_profiler_threshold = $this->config->get('system', 'dice_profiler_threshold', 0);
@ -655,10 +655,10 @@ class App
@file_put_contents( @file_put_contents(
$logfile, $logfile,
DateTimeFormat::utcNow() . "\t" . round($duration, 3) . "\t" . DateTimeFormat::utcNow() . "\t" . round($duration, 3) . "\t"
$this->requestId . "\t" . $code . "\t" . . $this->requestId . "\t" . $code . "\t"
$request . "\t" . $agent . "\n", . $request . "\t" . $agent . "\n",
FILE_APPEND FILE_APPEND,
); );
} }
} }

View file

@ -32,9 +32,9 @@ class BaseCollection extends \ArrayIterator
} }
/** /**
* @inheritDoc * @param mixed $key
* @param mixed $value
*/ */
#[\ReturnTypeWillChange]
public function offsetSet($key, $value): void public function offsetSet($key, $value): void
{ {
if (is_null($key)) { if (is_null($key)) {
@ -45,9 +45,8 @@ class BaseCollection extends \ArrayIterator
} }
/** /**
* @inheritDoc * @param mixed $key
*/ */
#[\ReturnTypeWillChange]
public function offsetUnset($key): void public function offsetUnset($key): void
{ {
if ($this->offsetExists($key)) { if ($this->offsetExists($key)) {
@ -136,7 +135,7 @@ class BaseCollection extends \ArrayIterator
return new $class($array); return new $class($array);
}, },
array_chunk($this->getArrayCopy(), $length) array_chunk($this->getArrayCopy(), $length),
); );
} }

View file

@ -26,7 +26,7 @@ use Psr\Log\LoggerInterface;
*/ */
abstract class BaseRepository abstract class BaseRepository
{ {
const LIMIT = 30; public const LIMIT = 30;
/** /**
* @var string This should be set to the main database table name the depository is using * @var string This should be set to the main database table name the depository is using
@ -148,15 +148,15 @@ abstract class BaseRepository
} }
/** /**
* @deprecated 2025.07 Use `\Friendica\BaseRepository::_selectFirstRowAsArray()` instead * @deprecated 2026.01 Use `\Friendica\BaseRepository::_selectFirstRowAsArray()` instead
* *
* @throws NotFoundException * @throws NotFoundException
*/ */
protected function _selectOne(array $condition, array $params = []): BaseEntity protected function _selectOne(array $condition, array $params = []): BaseEntity
{ {
@trigger_error('`' . __METHOD__ . '()` is deprecated since 2025.07 and will be removed after 5 months, use `\Friendica\BaseRepository::_selectFirstRowAsArray()` instead.', E_USER_DEPRECATED); @trigger_error('`' . __METHOD__ . '()` is deprecated since 2026.01 and will be removed after 5 months, use `\Friendica\BaseRepository::_selectFirstRowAsArray()` instead.', E_USER_DEPRECATED);
$fields = $this->_selectFirstRowAsArray( $condition, $params); $fields = $this->_selectFirstRowAsArray($condition, $params);
return $this->factory->createFromTableRow($fields); return $this->factory->createFromTableRow($fields);
} }

View file

@ -72,7 +72,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -62,14 +62,14 @@ HELP;
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->dba = $dba; $this->dba = $dba;
$this->l10n = $l10n; $this->l10n = $l10n;
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -80,7 +80,7 @@ HELP;
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Executable: ' . $this->executable); $this->out('Executable: ' . $this->executable);
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -85,14 +85,14 @@ HELP;
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->config = $config; $this->config = $config;
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Executable: ' . $this->executable); $this->out('Executable: ' . $this->executable);
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -102,8 +102,8 @@ HELP;
} }
if (count($this->args) == 3) { if (count($this->args) == 3) {
$cat = $this->getArgument(0); $cat = $this->getArgument(0);
$key = $this->getArgument(1); $key = $this->getArgument(1);
$value = $this->getArgument(2); $value = $this->getArgument(2);
if (is_array($this->config->get($cat, $key))) { if (is_array($this->config->get($cat, $key))) {
@ -116,16 +116,16 @@ HELP;
$result = $this->config->set($cat, $key, $value); $result = $this->config->set($cat, $key, $value);
if ($result) { if ($result) {
$this->out("{$cat}.{$key} <= " . $this->out("{$cat}.{$key} <= "
$this->config->get($cat, $key)); . $this->config->get($cat, $key));
} else { } else {
$this->out("Unable to set {$cat}.{$key}"); $this->out("Unable to set {$cat}.{$key}");
} }
} }
if (count($this->args) == 2) { if (count($this->args) == 2) {
$cat = $this->getArgument(0); $cat = $this->getArgument(0);
$key = $this->getArgument(1); $key = $this->getArgument(1);
$value = $this->config->get($this->getArgument(0), $this->getArgument(1)); $value = $this->config->get($this->getArgument(0), $this->getArgument(1));
if (is_array($value)) { if (is_array($value)) {

View file

@ -66,7 +66,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -34,7 +34,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -92,7 +92,7 @@ HELP;
$found = false; $found = false;
} }
if ($found && ( trim($previous) == "*/")) { if ($found && (trim($previous) == "*/")) {
$found = false; $found = false;
} }
@ -117,33 +117,35 @@ HELP;
private function addDocumentation($line) private function addDocumentation($line)
{ {
$trimmed = ltrim($line); $trimmed = ltrim($line);
$length = strlen($line) - strlen($trimmed); $length = strlen($line) - strlen($trimmed);
$space = substr($line, 0, $length); $space = substr($line, 0, $length);
$block = $space . "/**\n" . $block = $space . "/**\n"
$space . " * \n" . . $space . " * \n"
$space . " *\n"; /**/ . $space . " *\n"; /**/
$left = strpos($line, "("); $left = strpos($line, "(");
$line = substr($line, $left + 1); $line = substr($line, $left + 1);
$right = strpos($line, ")"); $right = strpos($line, ")");
$line = trim(substr($line, 0, $right)); $line = trim(substr($line, 0, $right));
if ($line != "") { if ($line != "") {
$parameters = explode(",", $line); $parameters = explode(",", $line);
foreach ($parameters as $parameter) { foreach ($parameters as $parameter) {
$parameter = trim($parameter); $parameter = trim($parameter);
$splitted = explode("=", $parameter); $splitted = explode("=", $parameter);
$block .= $space . " * @param " . trim($splitted[0], "& ") . "\n"; $block .= $space . " * @param " . trim($splitted[0], "& ") . "\n";
} }
if (count($parameters) > 0) $block .= $space . " *\n"; if (count($parameters) > 0) {
$block .= $space . " *\n";
}
} }
$block .= $space . " * @return \n" . $block .= $space . " * @return \n"
$space . " */\n"; . $space . " */\n";
return $block; return $block;
} }

View file

@ -73,17 +73,17 @@ HELP;
{ {
parent::__construct($argv); parent::__construct($argv);
$this->dba = $dba; $this->dba = $dba;
$this->dbaDefinition = $dbaDefinition; $this->dbaDefinition = $dbaDefinition;
$this->viewDefinition = $viewDefinition; $this->viewDefinition = $viewDefinition;
$this->config = $config; $this->config = $config;
$this->basePath = $basePath->getPath(); $this->basePath = $basePath->getPath();
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -112,7 +112,7 @@ HELP;
case "update": case "update":
$force = $this->getOption(['f', 'force'], false); $force = $this->getOption(['f', 'force'], false);
$override = $this->getOption(['o', 'override'], false); $override = $this->getOption(['o', 'override'], false);
$output = Update::run($basePath, $force, $override,true, false); $output = Update::run($basePath, $force, $override, true, false);
break; break;
case "drop": case "drop":
$execute = $this->getOption(['e', 'execute'], false); $execute = $this->getOption(['e', 'execute'], false);

View file

@ -30,7 +30,6 @@ use Friendica\AppHelper;
*/ */
class DocBloxErrorChecker extends \Asika\SimpleConsole\Console class DocBloxErrorChecker extends \Asika\SimpleConsole\Console
{ {
protected $helpOptions = ['h', 'help', '?']; protected $helpOptions = ['h', 'help', '?'];
/** @var string */ /** @var string */
@ -60,7 +59,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -108,7 +107,7 @@ HELP;
//check half of the set and discard if that half is okay //check half of the set and discard if that half is okay
$res = $filelist; $res = $filelist;
$i = count($res); $i = count($res);
do { do {
$this->out($i . '/' . count($filelist) . ' elements remaining.'); $this->out($i . '/' . count($filelist) . ' elements remaining.');
$res = $this->reduce($res, count($res) / 2); $res = $this->reduce($res, count($res) / 2);
@ -135,7 +134,7 @@ HELP;
private function commandExists($command) private function commandExists($command)
{ {
$prefix = strpos(strtolower(PHP_OS),'win') > -1 ? 'where' : 'which'; $prefix = strpos(strtolower(PHP_OS), 'win') > -1 ? 'where' : 'which';
exec("{$prefix} {$command}", $output, $returnVal); exec("{$prefix} {$command}", $output, $returnVal);
return $returnVal === 0; return $returnVal === 0;
} }

View file

@ -7,8 +7,8 @@
namespace Friendica\Console; namespace Friendica\Console;
use \RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use \RecursiveIteratorIterator; use RecursiveIteratorIterator;
/** /**
* Extracts translation strings from the Friendica project's files to be exported * Extracts translation strings from the Friendica project's files to be exported
@ -41,7 +41,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -64,16 +64,16 @@ HELP;
['index.php'], ['index.php'],
glob('mod/*'), glob('mod/*'),
glob('addon/*/*'), glob('addon/*/*'),
$this->globRecursive('src') $this->globRecursive('src'),
); );
foreach ($files as $file) { foreach ($files as $file) {
$str = file_get_contents($file); $str = file_get_contents($file);
$pat = '|->t\(([^\)]*+)[\)]|'; $pat = '|->t\(([^\)]*+)[\)]|';
$patt = '|->tt\(([^\)]*+)[\)]|'; $patt = '|->tt\(([^\)]*+)[\)]|';
$matches = []; $matches = [];
$matchestt = []; $matchestt = [];
preg_match_all($pat, $str, $matches); preg_match_all($pat, $str, $matches);
@ -86,7 +86,7 @@ HELP;
if (!empty($matches[1])) { if (!empty($matches[1])) {
foreach ($matches[1] as $long_match) { foreach ($matches[1] as $long_match) {
$match_arr = preg_split('/(?<=[\'"])\s*,/', $long_match); $match_arr = preg_split('/(?<=[\'"])\s*,/', $long_match);
$match = $match_arr[0]; $match = $match_arr[0];
if (!in_array($match, $arr)) { if (!in_array($match, $arr)) {
if (substr($match, 0, 1) == '$') { if (substr($match, 0, 1) == '$') {
continue; continue;
@ -139,7 +139,7 @@ HELP;
private function globRecursive(string $path): array private function globRecursive(string $path): array
{ {
$dir_iterator = new RecursiveDirectoryIterator($path); $dir_iterator = new RecursiveDirectoryIterator($path);
$iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST); $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
$return = []; $return = [];
foreach ($iterator as $file) { foreach ($iterator as $file) {

View file

@ -70,14 +70,14 @@ HELP;
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->dba = $dba; $this->dba = $dba;
$this->l10n = $l10n; $this->l10n = $l10n;
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -90,9 +90,9 @@ HELP;
throw new RuntimeException('Friendica isn\'t properly installed yet.'); throw new RuntimeException('Friendica isn\'t properly installed yet.');
} }
$this->examined = 0; $this->examined = 0;
$this->processed = 0; $this->processed = 0;
$this->errored = 0; $this->errored = 0;
do { do {
$result = $this->dba->select('workerqueue', ['id', 'parameter'], ["`command` = ? AND `parameter` LIKE ?", "APDelivery", "[\"%\",\"\",%"], ['limit' => [$this->examined, 100]]); $result = $this->dba->select('workerqueue', ['id', 'parameter'], ["`command` = ? AND `parameter` LIKE ?", "APDelivery", "[\"%\",\"\",%"], ['limit' => [$this->examined, 100]]);
@ -134,7 +134,7 @@ HELP;
if (is_array($parameters[2])) { if (is_array($parameters[2])) {
$parameters[4] = $parameters[2]; $parameters[4] = $parameters[2];
$contact = Contact::getById(current($parameters[2]), ['url']); $contact = Contact::getById(current($parameters[2]), ['url']);
$parameters[2] = $contact['url']; $parameters[2] = $contact['url'];
} }

View file

@ -53,13 +53,13 @@ HELP;
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->l10n = $l10n; $this->l10n = $l10n;
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -83,7 +83,7 @@ HELP;
} }
$block_reason = $this->getArgument(1); $block_reason = $this->getArgument(1);
if(Contact::block($contact_id, $block_reason)) { if (Contact::block($contact_id, $block_reason)) {
$this->out($this->l10n->t('The contact has been blocked from the node')); $this->out($this->l10n->t('The contact has been blocked from the node'));
} else { } else {
throw new \RuntimeException('The contact block failed.'); throw new \RuntimeException('The contact block failed.');

View file

@ -58,13 +58,13 @@ HELP;
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->dba =$dba; $this->dba = $dba;
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -74,7 +74,7 @@ HELP;
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Executable: ' . $this->executable); $this->out('Executable: ' . $this->executable);
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -60,13 +60,13 @@ HELP;
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->config = $config; $this->config = $config;
} }
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -15,11 +15,10 @@ use stdClass;
*/ */
class PhpToPo extends \Asika\SimpleConsole\Console class PhpToPo extends \Asika\SimpleConsole\Console
{ {
protected $helpOptions = ['h', 'help', '?']; protected $helpOptions = ['h', 'help', '?'];
private $normBaseMsgIds = []; private $normBaseMsgIds = [];
const NORM_REGEXP = "|[\\\]|"; public const NORM_REGEXP = "|[\\\]|";
/** @var AppHelper */ /** @var AppHelper */
private $appHelper; private $appHelper;
@ -53,7 +52,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -100,9 +99,9 @@ HELP;
$out .= '"Content-Transfer-Encoding: 8bit\n"' . "\n"; $out .= '"Content-Transfer-Encoding: 8bit\n"' . "\n";
// search for plural info // search for plural info
$lang = ""; $lang = "";
$lang_logic = ""; $lang_logic = "";
$lang_pnum = $this->getOption('p', 2); $lang_pnum = $this->getOption('p', 2);
$infile = file($phpfile); $infile = file($phpfile);
foreach ($infile as $l) { foreach ($infile as $l) {
@ -128,27 +127,27 @@ HELP;
// load base messages.po and extract msgids // load base messages.po and extract msgids
$base_msgids = []; $base_msgids = [];
$base_f = file($base_path); $base_f = file($base_path);
if (!$base_f) { if (!$base_f) {
throw new \RuntimeException('The base ' . $base_path . ' file is missing or unavailable to read.'); throw new \RuntimeException('The base ' . $base_path . ' file is missing or unavailable to read.');
} }
$this->out('Loading base file ' . $base_path . '...'); $this->out('Loading base file ' . $base_path . '...');
$_f = 0; $_f = 0;
$_mid = ""; $_mid = "";
$_mids = []; $_mids = [];
foreach ($base_f as $l) { foreach ($base_f as $l) {
$l = trim($l); $l = trim($l);
if ($this->startsWith($l, 'msgstr')) { if ($this->startsWith($l, 'msgstr')) {
if ($_mid != '""') { if ($_mid != '""') {
$base_msgids[$_mid] = $_mids; $base_msgids[$_mid] = $_mids;
$this->normBaseMsgIds[preg_replace(self::NORM_REGEXP, "", $_mid)] = $_mid; $this->normBaseMsgIds[preg_replace(self::NORM_REGEXP, "", $_mid)] = $_mid;
} }
$_f = 0; $_f = 0;
$_mid = ""; $_mid = "";
$_mids = []; $_mids = [];
} }
@ -156,7 +155,7 @@ HELP;
$_mids[count($_mids) - 1] .= "\n" . $l; $_mids[count($_mids) - 1] .= "\n" . $l;
} }
if ($this->startsWith($l, 'msgid_plural ')) { if ($this->startsWith($l, 'msgid_plural ')) {
$_f = 2; $_f = 2;
$_mids[] = str_replace('msgid_plural ', '', $l); $_mids[] = str_replace('msgid_plural ', '', $l);
} }
@ -165,8 +164,8 @@ HELP;
$_mids[count($_mids) - 1] .= "\n" . $l; $_mids[count($_mids) - 1] .= "\n" . $l;
} }
if ($this->startsWith($l, 'msgid ')) { if ($this->startsWith($l, 'msgid ')) {
$_f = 1; $_f = 1;
$_mid = str_replace('msgid ', '', $l); $_mid = str_replace('msgid ', '', $l);
$_mids = [$_mid]; $_mids = [$_mid];
} }
} }
@ -215,7 +214,7 @@ HELP;
private function startsWith($haystack, $needle) private function startsWith($haystack, $needle)
{ {
// search backwards starting from haystack length characters from the end // search backwards starting from haystack length characters from the end
return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE; return $needle === "" || strrpos($haystack, (string) $needle, -strlen($haystack)) !== false;
} }
/** /**

View file

@ -17,7 +17,7 @@ class PoToPhp extends \Asika\SimpleConsole\Console
{ {
protected $helpOptions = ['h', 'help', '?']; protected $helpOptions = ['h', 'help', '?'];
const DQ_ESCAPE = "__DQ__"; public const DQ_ESCAPE = "__DQ__";
protected function getHelp() protected function getHelp()
{ {
@ -39,7 +39,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -116,7 +116,7 @@ HELP;
$out .= self::escapePhpString($entry->getAsString(PoTokens::TRANSLATED)) . ';' . "\n"; $out .= self::escapePhpString($entry->getAsString(PoTokens::TRANSLATED)) . ';' . "\n";
} else { } else {
$out .= '[' . "\n"; $out .= '[' . "\n";
foreach($entry->getAsStringArray(PoTokens::TRANSLATED) as $key => $msgstr) { foreach ($entry->getAsStringArray(PoTokens::TRANSLATED) as $key => $msgstr) {
$out .= "\t" . $key . ' => ' . self::escapePhpString($msgstr) . ',' . "\n"; $out .= "\t" . $key . ' => ' . self::escapePhpString($msgstr) . ',' . "\n";
}; };
@ -130,7 +130,7 @@ HELP;
private function createPluralSelectFunctionString(string $pluralForms, string $lang): string private function createPluralSelectFunctionString(string $pluralForms, string $lang): string
{ {
$return = $this->convertCPluralConditionToPhpReturnStatement( $return = $this->convertCPluralConditionToPhpReturnStatement(
$pluralForms $pluralForms,
); );
$fnname = 'string_plural_select_' . $lang; $fnname = 'string_plural_select_' . $lang;
@ -191,14 +191,14 @@ HELP;
} }
if ($q === false || $s < $q) { if ($q === false || $s < $q) {
list($then, $else) = explode(':', $string, 2); [$then, $else] = explode(':', $string, 2);
$node['then'] = $then; $node['then'] = $then;
$parsedElse = []; $parsedElse = [];
self::parse($else, $parsedElse); self::parse($else, $parsedElse);
$node['else'] = $parsedElse; $node['else'] = $parsedElse;
} else { } else {
list($if, $thenelse) = explode('?', $string, 2); [$if, $thenelse] = explode('?', $string, 2);
$node['if'] = $if; $node['if'] = $if;
self::parse($thenelse, $node); self::parse($thenelse, $node);
} }
} }

View file

@ -67,7 +67,7 @@ HELP;
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Executable: ' . $this->executable); $this->out('Executable: ' . $this->executable);
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -87,7 +87,7 @@ HELP;
} }
if (count($this->args) == 2) { if (count($this->args) == 2) {
$mode = $this->getArgument(0); $mode = $this->getArgument(0);
$actor = $this->getArgument(1); $actor = $this->getArgument(1);
$apcontact = APContact::getByURL($actor); $apcontact = APContact::getByURL($actor);

View file

@ -65,7 +65,7 @@ HELP;
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Executable: ' . $this->executable); $this->out('Executable: ' . $this->executable);
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -31,7 +31,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }

View file

@ -49,7 +49,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -128,7 +128,7 @@ HELP;
} }
$output = []; $output = [];
$ret = 0; $ret = 0;
exec("$php_path -l $file", $output, $ret); exec("$php_path -l $file", $output, $ret);
if ($ret !== 0) { if ($ret !== 0) {
throw new \RuntimeException('Parse error found in ' . $file . ', scan stopped.'); throw new \RuntimeException('Parse error found in ' . $file . ', scan stopped.');

View file

@ -86,7 +86,7 @@ HELP;
protected function doExecute(): int protected function doExecute(): int
{ {
if ($this->getOption('v')) { if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__); $this->out('Class: ' . self::class);
$this->out('Arguments: ' . var_export($this->args, true)); $this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true)); $this->out('Options: ' . var_export($this->options, true));
} }
@ -490,9 +490,9 @@ HELP;
} }
} }
if (array_key_exists($category, $values) and if (array_key_exists($category, $values)
array_key_exists($key, $values[$category]) and and array_key_exists($key, $values[$category])
$values[$category][$key] == $value) { and $values[$category][$key] == $value) {
throw new RuntimeException('Value not changed'); throw new RuntimeException('Value not changed');
} }

View file

@ -20,7 +20,7 @@ use Friendica\Util\Proxy;
*/ */
class Avatar class Avatar
{ {
const BASE_PATH = '/avatar/'; public const BASE_PATH = '/avatar/';
/** /**
* Returns a field array with locally cached avatar pictures * Returns a field array with locally cached avatar pictures
@ -215,7 +215,7 @@ class Avatar
} }
$avatarpath = parse_url(self::baseUrl(), PHP_URL_PATH); $avatarpath = parse_url(self::baseUrl(), PHP_URL_PATH);
$pos = strpos($parts['path'], $avatarpath); $pos = strpos($parts['path'], (string) $avatarpath);
if ($pos !== 0) { if ($pos !== 0) {
return ''; return '';
} }

View file

@ -35,7 +35,7 @@ class Introduction extends BaseRepository
*/ */
private function selectOne(array $condition, array $params = []): IntroductionEntity private function selectOne(array $condition, array $params = []): IntroductionEntity
{ {
$fields = $this->_selectFirstRowAsArray( $condition, $params); $fields = $this->_selectFirstRowAsArray($condition, $params);
return $this->factory->createFromTableRow($fields); return $this->factory->createFromTableRow($fields);
} }
@ -85,7 +85,7 @@ class Introduction extends BaseRepository
['order' => ['id' => 'DESC']], ['order' => ['id' => 'DESC']],
$min_id, $min_id,
$max_id, $max_id,
$limit $limit,
); );
} catch (\Exception $e) { } catch (\Exception $e) {
throw new IntroductionPersistenceException(sprintf('Cannot select Introductions for used %d', $uid), $e); throw new IntroductionPersistenceException(sprintf('Cannot select Introductions for used %d', $uid), $e);

View file

@ -12,6 +12,9 @@ use Friendica\App\BaseURL;
use Friendica\App\Mode; use Friendica\App\Mode;
use Friendica\App\Page; use Friendica\App\Page;
use Friendica\BaseModule; use Friendica\BaseModule;
use Friendica\Content\Conversation\Factory\Channel;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Core\ACL; use Friendica\Core\ACL;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n; use Friendica\Core\L10n;
@ -47,16 +50,16 @@ use Psr\Log\LoggerInterface;
class Conversation class Conversation
{ {
const MODE_CHANNEL = 'channel'; public const MODE_CHANNEL = 'channel';
const MODE_COMMUNITY = 'community'; public const MODE_COMMUNITY = 'community';
const MODE_CONTACTS = 'contacts'; public const MODE_CONTACTS = 'contacts';
const MODE_CONTACT_POSTS = 'contact-posts'; public const MODE_CONTACT_POSTS = 'contact-posts';
const MODE_DISPLAY = 'display'; public const MODE_DISPLAY = 'display';
const MODE_FILED = 'filed'; public const MODE_FILED = 'filed';
const MODE_NETWORK = 'network'; public const MODE_NETWORK = 'network';
const MODE_NOTES = 'notes'; public const MODE_NOTES = 'notes';
const MODE_SEARCH = 'search'; public const MODE_SEARCH = 'search';
const MODE_PROFILE = 'profile'; public const MODE_PROFILE = 'profile';
/** @var Activity */ /** @var Activity */
private $activity; private $activity;
@ -85,23 +88,27 @@ class Conversation
/** @var UserGServerRepository */ /** @var UserGServerRepository */
private $userGServer; private $userGServer;
private EventDispatcherInterface $eventDispatcher; private EventDispatcherInterface $eventDispatcher;
private Channel $channel;
private UserDefinedChannel $userDefinedChannel;
public function __construct(UserGServerRepository $userGServer, LoggerInterface $logger, Profiler $profiler, Activity $activity, L10n $l10n, Item $item, Arguments $args, BaseURL $baseURL, IManageConfigValues $config, IManagePersonalConfigValues $pConfig, Page $page, Mode $mode, EventDispatcherInterface $eventDispatcher, IHandleUserSessions $session) public function __construct(UserGServerRepository $userGServer, Channel $channel, UserDefinedChannel $userDefinedChannel, LoggerInterface $logger, Profiler $profiler, Activity $activity, L10n $l10n, Item $item, Arguments $args, BaseURL $baseURL, IManageConfigValues $config, IManagePersonalConfigValues $pConfig, Page $page, Mode $mode, EventDispatcherInterface $eventDispatcher, IHandleUserSessions $session)
{ {
$this->activity = $activity; $this->activity = $activity;
$this->item = $item; $this->item = $item;
$this->config = $config; $this->config = $config;
$this->mode = $mode; $this->mode = $mode;
$this->baseURL = $baseURL; $this->baseURL = $baseURL;
$this->profiler = $profiler; $this->profiler = $profiler;
$this->logger = $logger; $this->logger = $logger;
$this->l10n = $l10n; $this->l10n = $l10n;
$this->args = $args; $this->args = $args;
$this->pConfig = $pConfig; $this->pConfig = $pConfig;
$this->page = $page; $this->page = $page;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->session = $session; $this->session = $session;
$this->userGServer = $userGServer; $this->userGServer = $userGServer;
$this->channel = $channel;
$this->userDefinedChannel = $userDefinedChannel;
} }
/** /**
@ -266,7 +273,7 @@ class Conversation
break; break;
case 'dislike': case 'dislike':
$dislike_translation_plural = '<button type="button" %2$s>%1$d people</button> don\'t like this'; $dislike_translation_plural = '<button type="button" %2$s>%1$d people</button> don\'t like this';
// @deprecated 2025.07 this translation is scheduled for removal as a new translation has been added without the typo // @deprecated 2026.01 this translation is scheduled for removal as a new translation has been added without the typo
$dislike_translation_plural = '<button type="button" %2$s>%1$d peiple</button> don\'t like this'; $dislike_translation_plural = '<button type="button" %2$s>%1$d peiple</button> don\'t like this';
$phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> doesn\'t like this', $dislike_translation_plural, $total, $spanatts); $phrase = $this->l10n->tt('<button type="button" %2$s>%1$d person</button> doesn\'t like this', $dislike_translation_plural, $total, $spanatts);
break; break;
@ -290,7 +297,7 @@ class Conversation
$output = Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [ $output = Renderer::replaceMacros(Renderer::getMarkupTemplate('voting_fakelink.tpl'), [
'$phrase' => $phrase, '$phrase' => $phrase,
'$type' => $verb, '$type' => $verb,
'$id' => $id '$id' => $id,
]); ]);
$output .= $expanded; $output .= $expanded;
@ -308,15 +315,15 @@ class Conversation
$this->profiler->startRecording('rendering'); $this->profiler->startRecording('rendering');
$o = ''; $o = '';
$x['allow_location'] = $x['allow_location'] ?? $user['allow_location']; $x['allow_location'] ??= $user['allow_location'];
$x['default_location'] = $x['default_location'] ?? $user['default-location']; $x['default_location'] ??= $user['default-location'];
$x['nickname'] = $x['nickname'] ?? $user['nickname']; $x['nickname'] ??= $user['nickname'];
$x['lockstate'] = $x['lockstate'] ?? ACL::getLockstateForUserId($user['uid']) ? 'lock' : 'unlock'; $x['lockstate'] = $x['lockstate'] ?? ACL::getLockstateForUserId($user['uid']) ? 'lock' : 'unlock';
$x['acl'] = $x['acl'] ?? ACL::getFullSelectorHTML($this->page, $user['uid'], true); $x['acl'] ??= ACL::getFullSelectorHTML($this->page, $user['uid'], true);
$x['bang'] = $x['bang'] ?? ''; $x['bang'] ??= '';
$x['visitor'] = $x['visitor'] ?? 'block'; $x['visitor'] ??= 'block';
$x['is_owner'] = $x['is_owner'] ?? true; $x['is_owner'] ??= true;
$x['profile_uid'] = $x['profile_uid'] ?? $this->session->getLocalUserId(); $x['profile_uid'] ??= $this->session->getLocalUserId();
$geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : ''; $geotag = !empty($x['allow_location']) ? Renderer::replaceMacros(Renderer::getMarkupTemplate('jot_geotag.tpl'), []) : '';
@ -347,7 +354,7 @@ class Conversation
new \DateTime('now'), new \DateTime('now'),
null, null,
$this->l10n->t('Created at'), $this->l10n->t('Created at'),
'created_at' 'created_at',
); );
} else { } else {
$created_at = ''; $created_at = '';
@ -382,14 +389,17 @@ class Conversation
'$shortnoloc' => $this->l10n->t('clear location'), '$shortnoloc' => $this->l10n->t('clear location'),
'$title' => $x['title'] ?? '', '$title' => $x['title'] ?? '',
'$placeholdertitle' => $this->l10n->t('Set title'), '$placeholdertitle' => $this->l10n->t('Set title'),
'$summary' => $x['summary'] ?? '',
'$placeholdersummary' => Feature::isEnabled($this->session->getLocalUserId(), Feature::SUMMARY) ? $this->l10n->t('Set summary, abstract or spoiler text') : '',
'$category' => $x['category'] ?? '', '$category' => $x['category'] ?? '',
'$placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->l10n->t("Categories \x28comma-separated list\x29") : '', '$placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->l10n->t("Categories \x28comma-separated list\x29") : '',
'$sensitive' => ['sensitive', $this->l10n->t('Sensitive post'), $x['sensitive'] ?? false],
'$scheduled_at' => Temporal::getDateTimeField( '$scheduled_at' => Temporal::getDateTimeField(
new \DateTime(), new \DateTime(),
new \DateTime('now + 6 months'), new \DateTime('now + 6 months'),
null, null,
$this->l10n->t('Scheduled at'), $this->l10n->t('Scheduled at'),
'scheduled_at' 'scheduled_at',
), ),
'$created_at' => $created_at, '$created_at' => $created_at,
'$wait' => $this->l10n->t('Please wait'), '$wait' => $this->l10n->t('Please wait'),
@ -480,17 +490,17 @@ class Conversation
. "; var netargs = '" . substr($this->args->getCommand(), 8) . "; var netargs = '" . substr($this->args->getCommand(), 8)
. '?f=' . '?f='
. (!empty($_GET['contactid']) ? '&contactid=' . rawurlencode($_GET['contactid']) : '') . (!empty($_GET['contactid']) ? '&contactid=' . rawurlencode($_GET['contactid']) : '')
. (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '') . (!empty($_GET['search']) ? '&search=' . rawurlencode($_GET['search']) : '')
. (!empty($_GET['star']) ? '&star=' . rawurlencode($_GET['star']) : '') . (!empty($_GET['star']) ? '&star=' . rawurlencode($_GET['star']) : '')
. (!empty($_GET['order']) ? '&order=' . rawurlencode($_GET['order']) : '') . (!empty($_GET['order']) ? '&order=' . rawurlencode($_GET['order']) : '')
. (!empty($_GET['bmark']) ? '&bmark=' . rawurlencode($_GET['bmark']) : '') . (!empty($_GET['bmark']) ? '&bmark=' . rawurlencode($_GET['bmark']) : '')
. (!empty($_GET['liked']) ? '&liked=' . rawurlencode($_GET['liked']) : '') . (!empty($_GET['liked']) ? '&liked=' . rawurlencode($_GET['liked']) : '')
. (!empty($_GET['conv']) ? '&conv=' . rawurlencode($_GET['conv']) : '') . (!empty($_GET['conv']) ? '&conv=' . rawurlencode($_GET['conv']) : '')
. (!empty($_GET['nets']) ? '&nets=' . rawurlencode($_GET['nets']) : '') . (!empty($_GET['nets']) ? '&nets=' . rawurlencode($_GET['nets']) : '')
. (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '') . (!empty($_GET['cmin']) ? '&cmin=' . rawurlencode($_GET['cmin']) : '')
. (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '') . (!empty($_GET['cmax']) ? '&cmax=' . rawurlencode($_GET['cmax']) : '')
. (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '') . (!empty($_GET['file']) ? '&file=' . rawurlencode($_GET['file']) : '')
. (!empty($_GET['channel']) ? '&channel=' . rawurlencode($_GET['channel']) : '') . (!empty($_GET['channel']) ? '&channel=' . rawurlencode($_GET['channel']) : '')
. (!empty($_GET['no_sharer']) ? '&no_sharer=' . rawurlencode($_GET['no_sharer']) : '') . (!empty($_GET['no_sharer']) ? '&no_sharer=' . rawurlencode($_GET['no_sharer']) : '')
. (!empty($_GET['accounttype']) ? '&accounttype=' . rawurlencode($_GET['accounttype']) : '') . (!empty($_GET['accounttype']) ? '&accounttype=' . rawurlencode($_GET['accounttype']) : '')
. "'; </script>\r\n"; . "'; </script>\r\n";
@ -687,10 +697,13 @@ class Conversation
* @param array $row Post row * @param array $row Post row
* @param array $activity Contact data of the resharer * @param array $activity Contact data of the resharer
* @param array $thr_parent Thread parent row * @param array $thr_parent Thread parent row
* @param string $channel Channel information
* @param int $uid User ID
* @param array $channels Available channels for the user
* *
* @return array items with parents and comments * @return array items with parents and comments
*/ */
private function addRowInformation(array $row, array $activity, array $thr_parent): array private function addRowInformation(array $row, array $activity, array $thr_parent, string $channel, int $uid, array $channels): array
{ {
$this->profiler->startRecording('rendering'); $this->profiler->startRecording('rendering');
@ -708,13 +721,18 @@ class Conversation
$row['causer-link'] = $contact['url']; $row['causer-link'] = $contact['url'];
$row['causer-avatar'] = $contact['thumb']; $row['causer-avatar'] = $contact['thumb'];
$row['causer-name'] = $contact['name']; $row['causer-name'] = $contact['name'];
} elseif (($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) && ($row['verb'] == Activity::ANNOUNCE) && } elseif (($row['gravity'] == ItemModel::GRAVITY_ACTIVITY) && ($row['verb'] == Activity::ANNOUNCE)
($row['author-id'] == $activity['causer-id']) && ($row['author-id'] == $activity['causer-id'])
) { ) {
return $row; return $row;
} }
} }
if ($channel) {
$row['channel'] = $channel;
$row['post-reason'] = ItemModel::PR_CHANNEL;
}
switch ($row['post-reason']) { switch ($row['post-reason']) {
case ItemModel::PR_TO: case ItemModel::PR_TO:
$row['direction'] = ['direction' => 7, 'title' => $this->l10n->t('You had been addressed (%s).', 'to')]; $row['direction'] = ['direction' => 7, 'title' => $this->l10n->t('You had been addressed (%s).', 'to')];
@ -793,6 +811,16 @@ class Conversation
case ItemModel::PR_PUSHED: case ItemModel::PR_PUSHED:
$row['direction'] = ['direction' => 1, 'title' => $this->l10n->t('Pushed to us')]; $row['direction'] = ['direction' => 1, 'title' => $this->l10n->t('Pushed to us')];
break; break;
case ItemModel::PR_CHANNEL:
$title = $channels[$channel]->label ?? $channel;
$description = $channels[$channel]->description ?? '';
if ($description) {
$row['direction'] = ['direction' => 11, 'title' => $this->l10n->t('Channel "%s": %s', $title, $description)];
} else {
$row['direction'] = ['direction' => 11, 'title' => $this->l10n->t('Channel "%s"', $title)];
}
break;
} }
$row['thr-parent-row'] = $thr_parent; $row['thr-parent-row'] = $thr_parent;
@ -829,6 +857,7 @@ class Conversation
$uriids = []; $uriids = [];
$commentcounter = []; $commentcounter = [];
$activitycounter = []; $activitycounter = [];
$postchannels = [];
foreach ($parents as $parent) { foreach ($parents as $parent) {
if (!empty($parent['thr-parent-id']) && !empty($parent['gravity']) && ($parent['gravity'] == ItemModel::GRAVITY_ACTIVITY)) { if (!empty($parent['thr-parent-id']) && !empty($parent['gravity']) && ($parent['gravity'] == ItemModel::GRAVITY_ACTIVITY)) {
@ -848,6 +877,7 @@ class Conversation
$commentcounter[$uriid] = 0; $commentcounter[$uriid] = 0;
$activitycounter[$uriid] = 0; $activitycounter[$uriid] = 0;
$postchannels[$uriid] = $parent['channel'] ?? '';
} }
$condition = ['parent-uri-id' => $uriids]; $condition = ['parent-uri-id' => $uriids];
@ -865,7 +895,7 @@ class Conversation
$condition = DBA::mergeConditions( $condition = DBA::mergeConditions(
$condition, $condition,
["`uid` IN (0, ?) AND (NOT `verb` IN (?, ?, ?) OR `verb` IS NULL)", $uid, Activity::FOLLOW, Activity::VIEW, Activity::READ] ["`uid` IN (0, ?) AND (NOT `verb` IN (?, ?, ?) OR `verb` IS NULL)", $uid, Activity::FOLLOW, Activity::VIEW, Activity::READ],
); );
$condition = DBA::mergeConditions($condition, ["(`uid` != ? OR `private` != ?)", 0, ItemModel::PRIVATE]); $condition = DBA::mergeConditions($condition, ["(`uid` != ? OR `private` != ?)", 0, ItemModel::PRIVATE]);
@ -875,8 +905,8 @@ class Conversation
[ [
"`visible` AND NOT `deleted` AND NOT `author-blocked` AND NOT `owner-blocked` "`visible` AND NOT `deleted` AND NOT `author-blocked` AND NOT `owner-blocked`
AND ((NOT `contact-pending` AND (`contact-rel` IN (?, ?))) OR `self` OR `contact-uid` = ?)", AND ((NOT `contact-pending` AND (`contact-rel` IN (?, ?))) OR `self` OR `contact-uid` = ?)",
Contact::SHARING, Contact::FRIEND, 0 Contact::SHARING, Contact::FRIEND, 0,
] ],
); );
$thread_parents = Post::select(['uri-id', 'causer-id'], $condition, ['order' => ['uri-id' => false, 'uid']]); $thread_parents = Post::select(['uri-id', 'causer-id'], $condition, ['order' => ['uri-id' => false, 'uid']]);
@ -892,6 +922,16 @@ class Conversation
$thread_items = Post::select(array_merge(ItemModel::DISPLAY_FIELDLIST, ['featured', 'contact-uid', 'gravity', 'post-type', 'post-reason']), $condition, $params); $thread_items = Post::select(array_merge(ItemModel::DISPLAY_FIELDLIST, ['featured', 'contact-uid', 'gravity', 'post-type', 'post-reason']), $condition, $params);
$channels = [];
foreach ($this->userDefinedChannel->selectByUid($uid) as $userchannel) {
$channels[$userchannel->code] = $userchannel;
}
/** @var ChannelEntity $systemchannel */
foreach ($this->channel->getTimelines($uid) as $systemchannel) {
$channels[$systemchannel->code] = $systemchannel;
}
$items = []; $items = [];
$quote_uri_ids = []; $quote_uri_ids = [];
$authors = []; $authors = [];
@ -934,7 +974,7 @@ class Conversation
]; ];
} }
$items[$row['uri-id']] = $this->addRowInformation($row, $activities[$row['uri-id']] ?? [], $thr_parent[$row['thr-parent-id']] ?? []); $items[$row['uri-id']] = $this->addRowInformation($row, $activities[$row['uri-id']] ?? [], $thr_parent[$row['thr-parent-id']] ?? [], $postchannels[$row['thr-parent-id']] ?? '', $uid, $channels);
} }
DBA::close($thread_items); DBA::close($thread_items);
@ -955,7 +995,7 @@ class Conversation
$authors[] = $row['author-id']; $authors[] = $row['author-id'];
$authors[] = $row['owner-id']; $authors[] = $row['owner-id'];
$items[$row['uri-id']] = $this->addRowInformation($row, [], []); $items[$row['uri-id']] = $this->addRowInformation($row, [], [], $postchannels[$row['thr-parent-id']] ?? '', $uid, $channels);
} }
DBA::close($quotes); DBA::close($quotes);
@ -995,8 +1035,8 @@ class Conversation
$items[$key]['user-collapsed-owner'] = !$always_display && in_array($row['owner-id'], $collapses); $items[$key]['user-collapsed-owner'] = !$always_display && in_array($row['owner-id'], $collapses);
if ( if (
in_array($mode, [self::MODE_CHANNEL, self::MODE_COMMUNITY, self::MODE_NETWORK]) && in_array($mode, [self::MODE_CHANNEL, self::MODE_COMMUNITY, self::MODE_NETWORK])
(in_array($row['author-id'], $blocks) || in_array($row['owner-id'], $blocks) || in_array($row['author-id'], $ignores) || in_array($row['owner-id'], $ignores)) && (in_array($row['author-id'], $blocks) || in_array($row['owner-id'], $blocks) || in_array($row['author-id'], $ignores) || in_array($row['owner-id'], $ignores))
) { ) {
unset($items[$key]); unset($items[$key]);
} }
@ -1070,7 +1110,7 @@ class Conversation
{ {
$counts = []; $counts = [];
foreach (Post\Counts::get(['parent-uri-id' => $uriids, 'verb' => Activity::POST]) as $count) { foreach (Post\Counts::get(['parent-uri-id' => $uriids, 'vid' => Verb::getID(Activity::POST)]) as $count) {
$counts[$count['parent-uri-id']] = ($counts[$count['parent-uri-id']] ?? 0) + $count['count']; $counts[$count['parent-uri-id']] = ($counts[$count['parent-uri-id']] ?? 0) + $count['count'];
} }
@ -1292,7 +1332,7 @@ class Conversation
foreach ($parents as $i => $parent) { foreach ($parents as $i => $parent) {
$parents[$i]['children'] = array_merge( $parents[$i]['children'] = array_merge(
$this->getItemChildren($item_array, $parent, true), $this->getItemChildren($item_array, $parent, true),
$this->getItemChildren($item_array, $parent, false) $this->getItemChildren($item_array, $parent, false),
); );
} }
@ -1510,7 +1550,7 @@ class Conversation
$body_html = ItemModel::prepareBody($item, true, $preview); $body_html = ItemModel::prepareBody($item, true, $preview);
list($categories, $folders) = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId()); [$categories, $folders] = $this->item->determineCategoriesTerms($item, $this->session->getLocalUserId());
if (!empty($item['featured'])) { if (!empty($item['featured'])) {
$pinned = $this->l10n->t('Pinned item'); $pinned = $this->l10n->t('Pinned item');
@ -1551,9 +1591,9 @@ class Conversation
'categories' => $categories, 'categories' => $categories,
'folders' => $folders, 'folders' => $folders,
'text' => strip_tags($body_html), 'text' => strip_tags($body_html),
'localtime' => DateTimeFormat::local($item['created'], 'r'), 'localtime' => $this->l10n->fullDateTime($item['created']),
'utc' => DateTimeFormat::utc($item['created'], 'c'), 'utc' => DateTimeFormat::utc($item['created'], 'c'),
'ago' => (($item['app']) ? $this->l10n->t('%s from %s', Temporal::getRelativeDate($item['created']), $item['app']) : Temporal::getRelativeDate($item['created'])), 'ago' => (($item['app']) ? $this->l10n->t('%s from %s', $this->l10n->relativeDateTime($item['created']), $item['app']) : $this->l10n->relativeDateTime($item['created'])),
'location_html' => $location_html, 'location_html' => $location_html,
'indent' => '', 'indent' => '',
'owner_name' => '', 'owner_name' => '',

View file

@ -9,4 +9,10 @@ namespace Friendica\Content\Conversation\Entity;
class UserDefinedChannel extends Channel class UserDefinedChannel extends Channel
{ {
const CIRCLE_GLOBAL = 0;
const CIRCLE_ACTIVITY = -5;
const CIRCLE_POSTS = -4;
const CIRCLE_CREATION = -3;
const CIRCLE_FOLLOWING = -1;
const CIRCLE_FOLLOWERS = -2;
} }

View file

@ -9,13 +9,16 @@ declare(strict_types=1);
namespace Friendica\Content\Conversation\Factory; namespace Friendica\Content\Conversation\Factory;
use Friendica\Content\Conversation\Entity\UserDefinedChannel as UserDefinedChannelEntity;
use Friendica\Content\Conversation\Repository\UserDefinedChannel; use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Protocol\Activity;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
@ -56,11 +59,12 @@ final class ChannelPost
* system's channel caching is enabled and matching channels are found. * system's channel caching is enabled and matching channels are found.
* *
* @param array $engagement post-engagement record * @param array $engagement post-engagement record
* @param int $gravity Gravity of the post
* @param int $uid User id context. * @param int $uid User id context.
* @param int $reshare_id Optional reshare id. * @param int $reshare_id Optional reshare id.
* @return void * @return void
*/ */
public function add(array $engagement, int $uid, int $reshare_id = 0): void public function add(array $engagement, int $gravity, int $uid, int $reshare_id = 0): void
{ {
if (!$this->config->get('system', 'channel_cache')) { if (!$this->config->get('system', 'channel_cache')) {
return; return;
@ -80,35 +84,43 @@ final class ChannelPost
$language = $engagement['language'] !== L10n::UNDETERMINED_LANGUAGE ? $engagement['language'] : ''; $language = $engagement['language'] !== L10n::UNDETERMINED_LANGUAGE ? $engagement['language'] : '';
$tags = array_column(Tag::getByURIId($engagement['uri-id'], [Tag::HASHTAG]), 'name'); $tags = array_column(Tag::getByURIId($engagement['uri-id'], [Tag::HASHTAG]), 'name');
$circles = ($gravity != Item::GRAVITY_PARENT) ? [UserDefinedChannelEntity::CIRCLE_CREATION, UserDefinedChannelEntity::CIRCLE_POSTS, UserDefinedChannelEntity::CIRCLE_ACTIVITY] : [];
$channels = $this->channelRepository->getMatchingChannels($engagement['searchtext'], $language, $tags, $engagement['media-type'], $engagement['owner-id'], $reshare_id, $uids); $channels = $this->channelRepository->getMatchingChannels($engagement['searchtext'], $language, $tags, $engagement['media-type'], $engagement['owner-id'], $reshare_id, $uids, $circles);
if (!($channels instanceof \Friendica\Content\Conversation\Collection\UserDefinedChannels) || $channels->count() === 0) { if (!($channels instanceof \Friendica\Content\Conversation\Collection\UserDefinedChannels) || $channels->count() === 0) {
$this->logger->debug('No channels found', ['uri-id' => $engagement['uri-id'], 'uids' => $uids, 'reshare_id' => $reshare_id]); $this->logger->debug('No channels found', ['uri-id' => $engagement['uri-id'], 'uids' => $uids, 'reshare_id' => $reshare_id]);
return; return;
} }
foreach ($channels as $channel) { foreach ($channels as $channel) {
if (in_array($channel->circle, [-3, -4, -5]) && !Post::exists(['parent-uri-id' => $engagement['uri-id'], 'uid' => $channel->uid])) { $in_timeline = Post::exists(["`parent-uri-id` = ? AND `uid` = ? AND NOT `verb` IN (?, ?, ?)", $engagement['uri-id'], $channel->uid, Activity::FOLLOW, Activity::VIEW, Activity::READ]);
if ($engagement['restricted'] && !$in_timeline) {
continue; continue;
} }
if ($channel->circle === -1 && !Contact::isSharing($engagement['owner-id'], $channel->uid)) { if (in_array($channel->circle, [UserDefinedChannelEntity::CIRCLE_CREATION, UserDefinedChannelEntity::CIRCLE_POSTS, UserDefinedChannelEntity::CIRCLE_ACTIVITY]) && !$in_timeline) {
continue; continue;
} }
if ($channel->circle === -2 && (!Contact::isFollower($engagement['owner-id'], $channel->uid) || Contact::isSharing($engagement['owner-id'], $channel->uid))) { if ($channel->circle === UserDefinedChannelEntity::CIRCLE_FOLLOWING && !Contact::isSharing($engagement['owner-id'], $channel->uid)) {
continue;
}
if ($channel->circle === UserDefinedChannelEntity::CIRCLE_FOLLOWERS && (!Contact::isFollower($engagement['owner-id'], $channel->uid) || Contact::isSharing($engagement['owner-id'], $channel->uid))) {
continue; continue;
} }
$cache = [ $cache = [
'channel' => (int)$channel->code, 'channel' => (int)$channel->code,
'uid' => $channel->uid, 'uid' => $channel->uid,
'uri-id' => $engagement['uri-id'], 'uri-id' => $engagement['uri-id'],
'created' => $post['created'], 'in-timeline' => $in_timeline,
'received' => $post['received'], 'created' => $post['created'],
'commented' => $post['commented'], 'received' => $post['received'],
'commented' => $post['commented'],
]; ];
$ret = $this->dba->insert('channel-post', $cache, Database::INSERT_IGNORE); $ret = $this->dba->insert('channel-post', $cache, Database::INSERT_UPDATE);
$this->logger->debug('Added channel post', ['uri-id' => $engagement['uri-id'], 'cache' => $cache, 'ret' => $ret]); $this->logger->debug('Added channel post', ['uri-id' => $engagement['uri-id'], 'cache' => $cache, 'ret' => $ret]);
} }
} }

View file

@ -17,6 +17,7 @@ use Friendica\Model\Contact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Protocol\Activity;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -91,14 +92,24 @@ final class SystemChannelPost
$uids = $this->channelRepository->getUsersForPost($engagement['uri-id'], $post_uid, $post['network'], $post['private']); $uids = $this->channelRepository->getUsersForPost($engagement['uri-id'], $post_uid, $post['network'], $post['private']);
foreach ($uids as $uid) { foreach ($uids as $uid) {
if ($engagement['restricted'] && !Post::exists(['parent-uri-id' => $engagement['uri-id'], 'uid' => $uid])) {
continue;
}
foreach ($channels as $channel) { foreach ($channels as $channel) {
if ($this->dba->exists('system-channel-post', ['channel' => $channel, 'uid' => $uid, 'uri-id' => $engagement['uri-id']])) { if ($this->dba->exists('system-channel-post', ['channel' => $channel, 'uid' => $uid, 'uri-id' => $engagement['uri-id']])) {
continue; continue;
} }
$wanted = User::getWantedLanguages($uid);
if ($channel !== Channel::LANGUAGE && $wanted && !in_array($engagement['language'], $wanted)) {
continue;
}
$store = false; $store = false;
switch ($channel) { switch ($channel) {
case Channel::WHATSHOT: case Channel::WHATSHOT:
$store = ($engagement['comments'] > $this->channelRepository->getMedianComments($uid, 4, $network) || $engagement['activities'] > $this->channelRepository->getMedianActivities($uid, 4, $network)) && $engagement['contact-type'] != Contact::TYPE_COMMUNITY; $store = ($engagement['comments'] > $this->channelRepository->getMedianComments($uid, 4, $network) || $engagement['activities'] > $this->channelRepository->getMedianActivities($uid, 4, $network) || $engagement['views'] > $this->channelRepository->getMedianViews($uid, 4, $network)) && $engagement['contact-type'] != Contact::TYPE_COMMUNITY;
break; break;
case Channel::FORYOU: case Channel::FORYOU:
@ -107,7 +118,7 @@ final class SystemChannelPost
if ($relation = $this->dba->selectFirst('contact-relation', ['relation-thread-score', 'follows'], ['relation-cid' => $cid, 'cid' => $owner])) { if ($relation = $this->dba->selectFirst('contact-relation', ['relation-thread-score', 'follows'], ['relation-cid' => $cid, 'cid' => $owner])) {
$store = $relation['relation-thread-score'] > $this->channelRepository->getMedianRelationThreadScore($cid, 4); $store = $relation['relation-thread-score'] > $this->channelRepository->getMedianRelationThreadScore($cid, 4);
if (!$store && $relation['follows']) { if (!$store && $relation['follows']) {
$store = ($engagement['comments'] >= $this->channelRepository->getMedianComments($uid, 4, $network) || $engagement['activities'] >= $this->channelRepository->getMedianActivities($uid, 4, $network)); $store = ($engagement['comments'] >= $this->channelRepository->getMedianComments($uid, 4, $network) || $engagement['activities'] >= $this->channelRepository->getMedianActivities($uid, 4, $network) || $engagement['views'] >= $this->channelRepository->getMedianViews($uid, 4, $network));
} }
} }
@ -129,7 +140,7 @@ final class SystemChannelPost
$store = $this->dba->exists('contact-relation', ["`relation-cid` = ? AND `cid` = ? AND `follows` AND `relation-thread-score` > ?", $store = $this->dba->exists('contact-relation', ["`relation-cid` = ? AND `cid` = ? AND `follows` AND `relation-thread-score` > ?",
$owner, $cid, 0]); $owner, $cid, 0]);
} }
if (!$store && ($engagement['comments'] >= $this->channelRepository->getMedianComments($uid, 4, $network) || $engagement['activities'] >= $this->channelRepository->getMedianActivities($uid, 4, $network))) { if (!$store && ($engagement['comments'] >= $this->channelRepository->getMedianComments($uid, 4, $network) || $engagement['activities'] >= $this->channelRepository->getMedianActivities($uid, 4, $network)) || $engagement['views'] >= $this->channelRepository->getMedianViews($uid, 4, $network)) {
$store = $this->dba->exists('contact-relation', ["`relation-cid` = ? AND `cid` = ? AND `relation-thread-score` > ?", $owner, $cid, 0]); $store = $this->dba->exists('contact-relation', ["`relation-cid` = ? AND `cid` = ? AND `relation-thread-score` > ?", $owner, $cid, 0]);
if (!$store) { if (!$store) {
$store = $this->dba->exists('contact-relation', ["`cid` = ? AND `relation-cid` = ? AND `relation-thread-score` > ?", $owner, $cid, 0]); $store = $this->dba->exists('contact-relation', ["`cid` = ? AND `relation-cid` = ? AND `relation-thread-score` > ?", $owner, $cid, 0]);
@ -195,14 +206,15 @@ final class SystemChannelPost
} }
$cache = [ $cache = [
'channel' => $channel, 'channel' => $channel,
'uid' => $uid, 'uid' => $uid,
'uri-id' => $engagement['uri-id'], 'uri-id' => $engagement['uri-id'],
'created' => $post['created'], 'in-timeline' => Post::exists(["`parent-uri-id` = ? AND `uid` = ? AND NOT `verb` IN (?, ?, ?)", $engagement['uri-id'], $uid, Activity::FOLLOW, Activity::VIEW, Activity::READ]),
'received' => $post['received'], 'created' => $post['created'],
'commented' => $post['commented'], 'received' => $post['received'],
'commented' => $post['commented'],
]; ];
$ret = $this->dba->insert('system-channel-post', $cache, Database::INSERT_IGNORE); $ret = $this->dba->insert('system-channel-post', $cache, Database::INSERT_UPDATE);
$this->logger->debug('Added system channel post', ['uri-id' => $engagement['uri-id'], 'cache' => $cache, 'ret' => $ret]); $this->logger->debug('Added system channel post', ['uri-id' => $engagement['uri-id'], 'cache' => $cache, 'ret' => $ret]);
} }
} }

View file

@ -316,9 +316,10 @@ class UserDefinedChannel extends BaseRepository
* @param int $owner_id Owner contact id. * @param int $owner_id Owner contact id.
* @param int $reshare_id Reshare contact id. * @param int $reshare_id Reshare contact id.
* @param array $uids User IDs to filter channels. * @param array $uids User IDs to filter channels.
* @param array $circles circle IDs to filter channels.
* @return UserDefinedChannels|null Collection of matching channels or null. * @return UserDefinedChannels|null Collection of matching channels or null.
*/ */
public function getMatchingChannels(string $searchtext, string $language, array $tags, int $media_type, int $owner_id, int $reshare_id, array $uids): ?UserDefinedChannels public function getMatchingChannels(string $searchtext, string $language, array $tags, int $media_type, int $owner_id, int $reshare_id, array $uids, array $circles): ?UserDefinedChannels
{ {
if (!in_array($language, User::getLanguages())) { if (!in_array($language, User::getLanguages())) {
$this->logger->debug('Unwanted language found. No matched channel found.', ['language' => $language, 'searchtext' => $searchtext]); $this->logger->debug('Unwanted language found. No matched channel found.', ['language' => $language, 'searchtext' => $searchtext]);
@ -327,7 +328,12 @@ class UserDefinedChannel extends BaseRepository
$disposableFullTextSearch = new DisposableFullTextSearch($this->db, $searchtext); $disposableFullTextSearch = new DisposableFullTextSearch($this->db, $searchtext);
$filteredChannels = $this->select(['uid' => $uids, 'valid' => true])->filter( $condition = ['uid' => $uids, 'valid' => true];
if ($circles) {
$condition = DBA::mergeConditions($condition, ['circle' => $circles]);
}
$filteredChannels = $this->select($condition)->filter(
function (UserDefinedChannelEntity $channel) use ($owner_id, $reshare_id, $language, $tags, $media_type, $disposableFullTextSearch, $searchtext) { function (UserDefinedChannelEntity $channel) use ($owner_id, $reshare_id, $language, $tags, $media_type, $disposableFullTextSearch, $searchtext) {
if ( if (
($channel->circle > 0) ($channel->circle > 0)
@ -478,9 +484,9 @@ class UserDefinedChannel extends BaseRepository
$condition = []; $condition = [];
if (!empty($channel->circle)) { if (!empty($channel->circle)) {
if ($channel->circle == -1) { if ($channel->circle == UserDefinedChannelEntity::CIRCLE_FOLLOWING) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?))", $uid, Contact::SHARING, Contact::FRIEND]; $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?))", $uid, Contact::SHARING, Contact::FRIEND];
} elseif ($channel->circle == -2) { } elseif ($channel->circle == UserDefinedChannelEntity::CIRCLE_FOLLOWERS) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER]; $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER];
} elseif ($channel->circle > 0) { } elseif ($channel->circle > 0) {
$condition = DBA::mergeConditions($condition, ["`owner-id` IN (SELECT `pid` FROM `group_member` INNER JOIN `account-user-view` ON `group_member`.`contact-id` = `account-user-view`.`id` WHERE `gid` = ? AND `account-user-view`.`uid` = ?)", $channel->circle, $uid]); $condition = DBA::mergeConditions($condition, ["`owner-id` IN (SELECT `pid` FROM `group_member` INNER JOIN `account-user-view` ON `group_member`.`contact-id` = `account-user-view`.`id` WHERE `gid` = ? AND `account-user-view`.`uid` = ?)", $channel->circle, $uid]);
@ -542,7 +548,7 @@ class UserDefinedChannel extends BaseRepository
$condition = DBA::mergeConditions($condition, ["`size` <= ?", $channel->maxSize]); $condition = DBA::mergeConditions($condition, ["`size` <= ?", $channel->maxSize]);
} }
if (in_array($channel->circle, [-3, -4, -5])) { if (in_array($channel->circle, [UserDefinedChannelEntity::CIRCLE_CREATION, UserDefinedChannelEntity::CIRCLE_POSTS, UserDefinedChannelEntity::CIRCLE_ACTIVITY])) {
$condition = DBA::mergeConditions($condition, ['uid' => $uid]); $condition = DBA::mergeConditions($condition, ['uid' => $uid]);
} }
@ -743,6 +749,42 @@ class UserDefinedChannel extends BaseRepository
return $activities; return $activities;
} }
/**
* Compute median views for a user's wanted languages.
*
* @param int $uid User id.
* @param int $divider Divider used to determine median index.
* @param string $network Optional network filter.
* @return int Median views value.
*/
public function getMedianViews(int $uid, int $divider, string $network = ''): int
{
$languages = User::getWantedLanguages($uid);
$cache_key = 'Channel:getMedianViews:' . $divider . ':' . implode(':', $languages) . $network;
$views = $this->cache->get($cache_key);
if (!empty($views)) {
return $views;
}
$condition = ["`contact-type` != ? AND `views` > ? AND NOT `restricted`", Contact::TYPE_COMMUNITY, 0];
$condition = $this->addLanguageCondition($uid, $condition);
if ($network) {
$condition = DBA::mergeConditions($condition, ["`network` = ?", $network]);
}
$limit = $this->db->count('post-engagement', $condition) / $divider;
$post = $this->db->selectToArray('post-engagement', ['views'], $condition, ['order' => ['views' => true], 'limit' => [$limit, 1]]);
$views = $post[0]['views'] ?? 0;
if (empty($views)) {
return 0;
}
$this->cache->set($cache_key, $views, Duration::HALF_HOUR);
$this->logger->debug('Calculated median views', ['divider' => $divider, 'languages' => $languages, 'network' => $network, 'median' => $views]);
return $views;
}
/** /**
* Compute median relation thread score for a contact id. * Compute median relation thread score for a contact id.
* *

View file

@ -12,25 +12,25 @@ use Friendica\Event\ArrayFilterEvent;
class Feature class Feature
{ {
const ADD_ABSTRACT = 'add_abstract'; public const ADD_ABSTRACT = 'add_abstract';
const CATEGORIES = 'categories'; public const CATEGORIES = 'categories';
const COMMUNITY = 'community'; public const COMMUNITY = 'community';
const EXPLICIT_MENTIONS = 'explicit_mentions'; public const EXPLICIT_MENTIONS = 'explicit_mentions';
const MEMBER_SINCE = 'profile_membersince'; public const MEMBER_SINCE = 'profile_membersince';
const PHOTO_LOCATION = 'photo_location'; public const PUBLIC_CALENDAR = 'public_calendar';
const PUBLIC_CALENDAR = 'public_calendar'; public const SUMMARY = 'summary';
const TAGCLOUD = 'tagadelic'; public const TAGCLOUD = 'tagadelic';
// The different widgets: // The different widgets:
const ACCOUNTS = 'accounts'; public const ACCOUNTS = 'accounts';
const ARCHIVE = 'archive'; public const ARCHIVE = 'archive';
const CIRCLES = 'circles'; public const CIRCLES = 'circles';
const CHANNELS = 'channels'; public const CHANNELS = 'channels';
const FOLDERS = 'folders'; public const FOLDERS = 'folders';
const GROUPS = 'forumlist_profile'; public const GROUPS = 'forumlist_profile';
const NETWORKS = 'networks'; public const NETWORKS = 'networks';
const NOSHARER = 'nosharer'; public const NOSHARER = 'nosharer';
const SEARCHES = 'searches'; public const SEARCHES = 'searches';
const TRENDING_TAGS = 'trending_tags'; public const TRENDING_TAGS = 'trending_tags';
/** /**
* check if feature is enabled * check if feature is enabled
@ -56,10 +56,10 @@ class Feature
$arr = ['uid' => $uid, 'feature' => $feature, 'enabled' => $enabled]; $arr = ['uid' => $uid, 'feature' => $feature, 'enabled' => $enabled];
$arr = $eventDispatcher->dispatch( $arr = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::FEATURE_ENABLED, $arr) new ArrayFilterEvent(ArrayFilterEvent::FEATURE_ENABLED, $arr),
)->getArray(); )->getArray();
return (bool)$arr['enabled']; return (bool) $arr['enabled'];
} }
/** /**
@ -104,7 +104,6 @@ class Feature
'general' => [ 'general' => [
$l10n->t('General Features'), $l10n->t('General Features'),
//array('expire', $l10n->t('Content Expiration'), $l10n->t('Remove old posts/comments after a period of time')), //array('expire', $l10n->t('Content Expiration'), $l10n->t('Remove old posts/comments after a period of time')),
[self::PHOTO_LOCATION, $l10n->t('Photo Location'), $l10n->t("Photo metadata is normally stripped. This extracts the location \x28if present\x29 prior to stripping metadata and links it to a map."), false, $config->get('feature_lock', self::PHOTO_LOCATION, false)],
[self::COMMUNITY, $l10n->t('Display the community in the navigation'), $l10n->t('If enabled, the community can be accessed via the navigation menu. Independent from this setting, the community timelines can always be accessed via the channels.'), true, $config->get('feature_lock', self::COMMUNITY, false)], [self::COMMUNITY, $l10n->t('Display the community in the navigation'), $l10n->t('If enabled, the community can be accessed via the navigation menu. Independent from this setting, the community timelines can always be accessed via the channels.'), true, $config->get('feature_lock', self::COMMUNITY, false)],
], ],
@ -119,6 +118,7 @@ class Feature
'tools' => [ 'tools' => [
$l10n->t('Post/Comment Tools'), $l10n->t('Post/Comment Tools'),
[self::CATEGORIES, $l10n->t('Post Categories'), $l10n->t('Add categories to your posts'), false, $config->get('feature_lock', self::CATEGORIES, false)], [self::CATEGORIES, $l10n->t('Post Categories'), $l10n->t('Add categories to your posts'), false, $config->get('feature_lock', self::CATEGORIES, false)],
[self::SUMMARY, $l10n->t('Summary'), $l10n->t('Add a summary, abstract or spoiler text to your posts'), false, $config->get('feature_lock', self::SUMMARY, false)],
], ],
// Widget visibility on the network stream // Widget visibility on the network stream
@ -147,7 +147,7 @@ class Feature
'advanced_calendar' => [ 'advanced_calendar' => [
$l10n->t('Advanced Calendar Settings'), $l10n->t('Advanced Calendar Settings'),
[self::PUBLIC_CALENDAR, $l10n->t('Allow anonymous access to your calendar'), $l10n->t('Allows anonymous visitors to consult your calendar and your public events. Contact birthday events are private to you.'), false, $config->get('feature_lock', self::PUBLIC_CALENDAR, false)], [self::PUBLIC_CALENDAR, $l10n->t('Allow anonymous access to your calendar'), $l10n->t('Allows anonymous visitors to consult your calendar and your public events. Contact birthday events are private to you.'), false, $config->get('feature_lock', self::PUBLIC_CALENDAR, false)],
] ],
]; ];
// removed any locked features and remove the entire category if this makes it empty // removed any locked features and remove the entire category if this makes it empty
@ -172,7 +172,7 @@ class Feature
} }
$arr = $eventDispatcher->dispatch( $arr = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::FEATURE_GET, $arr) new ArrayFilterEvent(ArrayFilterEvent::FEATURE_GET, $arr),
)->getArray(); )->getArray();
return $arr; return $arr;

View file

@ -151,7 +151,7 @@ class Item
'url' => $url, 'url' => $url,
'removeurl' => $this->userSession->getLocalUserId() == $uid ? 'filerm/' . $item['id'] . '?cat=' . rawurlencode($savedFolderName) : '', 'removeurl' => $this->userSession->getLocalUserId() == $uid ? 'filerm/' . $item['id'] . '?cat=' . rawurlencode($savedFolderName) : '',
'first' => $first, 'first' => $first,
'last' => false 'last' => false,
]; ];
$first = false; $first = false;
} }
@ -167,7 +167,7 @@ class Item
'url' => "#", 'url' => "#",
'removeurl' => $this->userSession->getLocalUserId() == $uid ? 'filerm/' . $item['id'] . '?term=' . rawurlencode($savedFolderName) : '', 'removeurl' => $this->userSession->getLocalUserId() == $uid ? 'filerm/' . $item['id'] . '?term=' . rawurlencode($savedFolderName) : '',
'first' => $first, 'first' => $first,
'last' => false 'last' => false,
]; ];
$first = false; $first = false;
} }
@ -303,7 +303,7 @@ class Item
if ($this->activity->match($item['verb'], Activity::TAG)) { if ($this->activity->match($item['verb'], Activity::TAG)) {
$fields = [ $fields = [
'author-id', 'author-link', 'author-name', 'author-network', 'author-link', 'author-alias', 'author-id', 'author-link', 'author-name', 'author-network', 'author-link', 'author-alias',
'verb', 'object-type', 'resource-id', 'body', 'plink' 'verb', 'object-type', 'resource-id', 'body', 'plink',
]; ];
$obj = Post::selectFirst($fields, ['uri' => $item['parent-uri']]); $obj = Post::selectFirst($fields, ['uri' => $item['parent-uri']]);
if (!DBA::isResult($obj)) { if (!DBA::isResult($obj)) {
@ -452,8 +452,8 @@ class Item
$menu[$this->l10n->t('Raw content')] = 'javascript:displaySearchText(' . $item['uri-id'] . ');'; $menu[$this->l10n->t('Raw content')] = 'javascript:displaySearchText(' . $item['uri-id'] . ');';
if ((($cid == 0) || ($rel == Contact::FOLLOWER)) && if ((($cid == 0) || ($rel == Contact::FOLLOWER))
in_array($item['network'], Protocol::FEDERATED) && in_array($item['network'], Protocol::FEDERATED)
) { ) {
$menu[$this->l10n->t('Connect/Follow')] = 'contact/follow?url=' . urlencode($item['author-link']) . '&auto=1'; $menu[$this->l10n->t('Connect/Follow')] = 'contact/follow?url=' . urlencode($item['author-link']) . '&auto=1';
} }
@ -496,10 +496,10 @@ class Item
} }
// Check conditions // Check conditions
return (!($this->activity->match($item['verb'], Activity::FOLLOW) && return (!($this->activity->match($item['verb'], Activity::FOLLOW)
$item['object-type'] === Activity\ObjectType::NOTE && && $item['object-type'] === Activity\ObjectType::NOTE
empty($item['self']) && && empty($item['self'])
$item['uid'] == $this->userSession->getLocalUserId()) && $item['uid'] == $this->userSession->getLocalUserId())
); );
} }
@ -741,7 +741,7 @@ class Item
if (is_array($shared)) { if (is_array($shared)) {
return [ return [
'comment' => BBCode::removeSharedData($item['body'] ?? ''), 'comment' => BBCode::removeSharedData($item['body'] ?? ''),
'post' => $shared 'post' => $shared,
]; ];
} }
} }
@ -752,7 +752,7 @@ class Item
if (is_array($shared)) { if (is_array($shared)) {
return [ return [
'comment' => $attributes['comment'], 'comment' => $attributes['comment'],
'post' => $shared 'post' => $shared,
]; ];
} }
} }
@ -845,8 +845,8 @@ class Item
0 => [ 0 => [
'src' => $attachment_img_src, 'src' => $attachment_img_src,
'width' => $attachment_img_width, 'width' => $attachment_img_width,
'height' => $attachment_img_height 'height' => $attachment_img_height,
] ],
]; ];
} else { } else {
unset($attachment['images']); unset($attachment['images']);
@ -940,16 +940,16 @@ class Item
public function initializePost(array $post): array public function initializePost(array $post): array
{ {
$post['network'] = Protocol::DFRN; $post['network'] = Protocol::DFRN;
$post['protocol'] = Conversation::PARCEL_DIRECT; $post['protocol'] = Conversation::PARCEL_DIRECT;
$post['direction'] = Conversation::PUSH; $post['direction'] = Conversation::PUSH;
$post['received'] = DateTimeFormat::utcNow(); $post['received'] = DateTimeFormat::utcNow();
$post['origin'] = true; $post['origin'] = true;
$post['wall'] = $post['wall'] ?? true; $post['wall'] ??= true;
$post['guid'] = $post['guid'] ?? System::createUUID(); $post['guid'] ??= System::createUUID();
$post['verb'] = $post['verb'] ?? Activity::POST; $post['verb'] ??= Activity::POST;
$post['uri'] = $post['uri'] ?? ItemModel::newURI($post['guid']); $post['uri'] ??= ItemModel::newURI($post['guid']);
$post['thr-parent'] = $post['thr-parent'] ?? $post['uri']; $post['thr-parent'] ??= $post['uri'];
if (empty($post['gravity'])) { if (empty($post['gravity'])) {
$post['gravity'] = ($post['uri'] == $post['thr-parent']) ? ItemModel::GRAVITY_PARENT : ItemModel::GRAVITY_COMMENT; $post['gravity'] = ($post['uri'] == $post['thr-parent']) ? ItemModel::GRAVITY_PARENT : ItemModel::GRAVITY_COMMENT;
@ -1031,7 +1031,7 @@ class Item
]; ];
$hook_data = $this->eventDispatcher->dispatch( $hook_data = $this->eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL_END, $hook_data) new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL_END, $hook_data),
)->getArray(); )->getArray();
$post = $hook_data['item'] ?? $post; $post = $hook_data['item'] ?? $post;
@ -1050,7 +1050,7 @@ class Item
$this->baseURL, $this->baseURL,
$post, $post,
$address, $address,
$author['thumb'] ?? '' $author['thumb'] ?? '',
)); ));
} }
} }
@ -1335,7 +1335,7 @@ class Item
\IntlChar::BLOCK_CODE_BASIC_LATIN, \IntlChar::BLOCK_CODE_LATIN_1_SUPPLEMENT, \IntlChar::BLOCK_CODE_BASIC_LATIN, \IntlChar::BLOCK_CODE_LATIN_1_SUPPLEMENT,
\IntlChar::BLOCK_CODE_LATIN_EXTENDED_A, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_B, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_A, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_B,
\IntlChar::BLOCK_CODE_LATIN_EXTENDED_C, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_D, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_C, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_D,
\IntlChar::BLOCK_CODE_LATIN_EXTENDED_E, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_ADDITIONAL \IntlChar::BLOCK_CODE_LATIN_EXTENDED_E, \IntlChar::BLOCK_CODE_LATIN_EXTENDED_ADDITIONAL,
]); ]);
} }
@ -1363,4 +1363,24 @@ class Item
$used_languages = $this->l10n->t("Detected languages in this post:\n%s", $used_languages); $used_languages = $this->l10n->t("Detected languages in this post:\n%s", $used_languages);
return $used_languages; return $used_languages;
} }
public function setViewed(int $uri_id, int $uid)
{
if (Post::exists(['thr-parent-id' => $uri_id, 'verb' => Activity::VIEW, 'uid' => $uid])) {
return;
}
$item = Post::selectFirst(['id'], ['uri-id' => $uri_id, 'gravity' => ItemModel::GRAVITY_PARENT, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (!DBA::isResult($item)) {
return;
}
$self = DBA::selectFirst('contact', ['id'], ['uid' => $uid, 'self' => true]);
if (!DBA::isResult($self)) {
return;
}
$this->logger->debug('Marking item as viewed.', ['uri-id' => $uri_id, 'uid' => $uid]);
ItemModel::performActivity($item['id'], 'view', $uid, '<' . $self['id'] . '>', '', '', '');
}
} }

View file

@ -224,10 +224,9 @@ class Nav
if ($this->session->isAuthenticated()) { if ($this->session->isAuthenticated()) {
// user menu // user menu
$nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname(), $this->l10n->t('Conversations'), '', $this->l10n->t('Conversations you started'), 'fa-commenting'];
$nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname() . '/profile', $this->l10n->t('Profile'), '', $this->l10n->t('Your profile page'), 'fa-user']; $nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname() . '/profile', $this->l10n->t('Profile'), '', $this->l10n->t('Your profile page'), 'fa-user'];
$nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname(), $this->l10n->t('Conversations'), '', $this->l10n->t('Conversations you started'), 'fa-commenting'];
$nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname() . '/photos', $this->l10n->t('Photos'), '', $this->l10n->t('Your photos'), 'fa-picture-o']; $nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname() . '/photos', $this->l10n->t('Photos'), '', $this->l10n->t('Your photos'), 'fa-picture-o'];
$nav['usermenu'][] = ['profile/' . $this->session->getLocalUserNickname() . '/media', $this->l10n->t('Media'), '', $this->l10n->t('Your postings with media'), 'fa-edit'];
$nav['usermenu'][] = ['calendar/', $this->l10n->t('Calendar'), '', $this->l10n->t('Your calendar'), 'fa-calendar']; $nav['usermenu'][] = ['calendar/', $this->l10n->t('Calendar'), '', $this->l10n->t('Your calendar'), 'fa-calendar'];
$nav['usermenu'][] = ['notes/', $this->l10n->t('Personal notes'), '', $this->l10n->t('Your personal notes'), 'fa-book']; $nav['usermenu'][] = ['notes/', $this->l10n->t('Personal notes'), '', $this->l10n->t('Your personal notes'), 'fa-book'];
@ -318,8 +317,12 @@ class Nav
$nav['messages']['outbox'] = ['message/sent', $this->l10n->t('Outbox'), '', $this->l10n->t('Outbox')]; $nav['messages']['outbox'] = ['message/sent', $this->l10n->t('Outbox'), '', $this->l10n->t('Outbox')];
$nav['messages']['new'] = ['message/new', $this->l10n->t('New Message'), '', $this->l10n->t('New Message')]; $nav['messages']['new'] = ['message/new', $this->l10n->t('New Message'), '', $this->l10n->t('New Message')];
$nav_accounts_name = $this->l10n->t('Accounts');
$nav_accounts_description = $this->l10n->t('Manage other accounts, including groups and pages');
if (User::hasIdentities($this->session->getSubManagedUserId() ?: $this->session->getLocalUserId())) { if (User::hasIdentities($this->session->getSubManagedUserId() ?: $this->session->getLocalUserId())) {
$nav['delegation'] = ['delegation', $this->l10n->t('Accounts'), '', $this->l10n->t('Manage other accounts, including groups and pages')]; $nav['delegation'] = ['delegation', $nav_accounts_name, '', $nav_accounts_description];
} else {
$nav['delegation'] = ['settings/delegation', $nav_accounts_name, '', $nav_accounts_description];
} }
$nav['settings'] = ['settings', $this->l10n->t('Settings'), '', $this->l10n->t('Account settings')]; $nav['settings'] = ['settings', $this->l10n->t('Settings'), '', $this->l10n->t('Account settings')];

View file

@ -103,12 +103,12 @@ class PageInfo
// It maybe is a rich content, but if it does have everything that a link has, // It maybe is a rich content, but if it does have everything that a link has,
// then treat it that way // then treat it that way
if (($data['type'] == 'rich') && is_string($data['title']) && if (($data['type'] == 'rich') && is_string($data['title'])
is_string($data['text']) && !empty($data['images'])) { && is_string($data['text']) && !empty($data['images'])) {
$data['type'] = 'link'; $data['type'] = 'link';
} }
$data['title'] = $data['title'] ?? ''; $data['title'] ??= '';
if ((($data['type'] != 'link') && ($data['type'] != 'video') && ($data['type'] != 'photo')) || ($data['title'] == $data['url'])) { if ((($data['type'] != 'link') && ($data['type'] != 'video') && ($data['type'] != 'photo')) || ($data['title'] == $data['url'])) {
return ''; return '';
@ -223,7 +223,7 @@ class PageInfo
$hashtag = str_replace( $hashtag = str_replace(
[' ', '+', '/', '.', '#', "'"], [' ', '+', '/', '.', '#', "'"],
['', '', '', '', '', ''], ['', '', '', '', '', ''],
$keyword $keyword,
); );
$taglist[] = $hashtag; $taglist[] = $hashtag;

View file

@ -461,6 +461,9 @@ class PostMedia extends BaseRepository
} else { } else {
$player = $this->getLinkAttachment($media); $player = $this->getLinkAttachment($media);
} }
if ($player === '') {
continue;
}
@$tmp->loadHTML(mb_convert_encoding($player, 'HTML-ENTITIES', "UTF-8"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); @$tmp->loadHTML(mb_convert_encoding($player, 'HTML-ENTITIES', "UTF-8"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$div = $tmp->documentElement; $div = $tmp->documentElement;
$imported = $doc->importNode($div, true); $imported = $doc->importNode($div, true);

View file

@ -36,29 +36,29 @@ use GuzzleHttp\Psr7\Uri;
class BBCode class BBCode
{ {
// Update this value to the current date whenever changes are made to BBCode::convert // Update this value to the current date whenever changes are made to BBCode::convert
const VERSION = '2024-04-07'; public const VERSION = '2024-04-07';
const INTERNAL = 0; public const INTERNAL = 0;
const EXTERNAL = 1; public const EXTERNAL = 1;
const MASTODON_API = 2; public const MASTODON_API = 2;
const DIASPORA = 3; public const DIASPORA = 3;
const CONNECTORS = 4; public const CONNECTORS = 4;
const TWITTER_API = 5; public const TWITTER_API = 5;
const NPF = 6; public const NPF = 6;
const TWITTER = 8; public const TWITTER = 8;
const BACKLINK = 8; public const BACKLINK = 8;
const ACTIVITYPUB = 9; public const ACTIVITYPUB = 9;
const BLUESKY = 10; public const BLUESKY = 10;
const SHARED_ANCHOR = '<hr class="shared-anchor">'; public const SHARED_ANCHOR = '<hr class="shared-anchor">';
const TOP_ANCHOR = '<br class="top-anchor">'; public const TOP_ANCHOR = '<br class="top-anchor">';
const BOTTOM_ANCHOR = '<br class="button-anchor">'; public const BOTTOM_ANCHOR = '<br class="button-anchor">';
const PREVIEW_NONE = 0; public const PREVIEW_NONE = 0;
const PREVIEW_NO_IMAGE = 1; public const PREVIEW_NO_IMAGE = 1;
const PREVIEW_LARGE = 2; public const PREVIEW_LARGE = 2;
const PREVIEW_SMALL = 3; public const PREVIEW_SMALL = 3;
const PREVIEW_AUTO = 4; public const PREVIEW_AUTO = 4;
/** /**
* Fetches attachment data that were generated with the "attachment" element * Fetches attachment data that were generated with the "attachment" element
@ -183,7 +183,7 @@ class BBCode
$attach_data = self::getAttachmentData($match[0]); $attach_data = self::getAttachmentData($match[0]);
if (empty($attach_data['url'])) { if (empty($attach_data['url'])) {
return $match[0]; return $match[0];
} elseif (strpos(str_replace($match[0], '', $body), $attach_data['url']) !== false) { } elseif (strpos(str_replace($match[0], '', $body), (string) $attach_data['url']) !== false) {
return ''; return '';
} elseif (empty($attach_data['title']) || $no_link_desc) { } elseif (empty($attach_data['title']) || $no_link_desc) {
return " \n[url]" . $attach_data['url'] . "[/url]\n"; return " \n[url]" . $attach_data['url'] . "[/url]\n";
@ -191,7 +191,7 @@ class BBCode
return " \n[url=" . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n"; return " \n[url=" . $attach_data['url'] . ']' . $attach_data['title'] . "[/url]\n";
} }
}, },
$body $body,
); );
} }
@ -245,12 +245,12 @@ class BBCode
// Add text from attached media // Add text from attached media
if (!empty($uri_id)) { if (!empty($uri_id)) {
foreach (Post\Media::getByURIId($uri_id) as $media) { foreach (Post\Media::getByURIId($uri_id) as $media) {
if (!empty($media['description']) && (stripos($text, $media['description']) === false)) { if (!empty($media['description']) && (stripos($text, (string) $media['description']) === false)) {
$text .= ' ' . $media['description']; $text .= ' ' . $media['description'];
} }
if (in_array($media['type'], [Post\Media::HTML, Post\Media::ACTIVITY])) { if (in_array($media['type'], [Post\Media::HTML, Post\Media::ACTIVITY])) {
foreach (['name', 'author-name', 'publisher-name'] as $key) { foreach (['name', 'author-name', 'publisher-name'] as $key) {
if (!empty($media[$key] && stripos($text, $media[$key]) === false)) { if (!empty($media[$key] && stripos($text, (string) $media[$key]) === false)) {
$text .= ' ' . $media[$key]; $text .= ' ' . $media[$key];
} }
} }
@ -473,14 +473,19 @@ class BBCode
if (!empty($data['title']) && !empty($data['url'])) { if (!empty($data['title']) && !empty($data['url'])) {
$preview_class = in_array($preview_mode, [self::PREVIEW_AUTO, self::PREVIEW_LARGE]) ? 'attachment-image' : 'attachment-preview'; $preview_class = in_array($preview_mode, [self::PREVIEW_AUTO, self::PREVIEW_LARGE]) ? 'attachment-image' : 'attachment-preview';
if (!empty($data['image']) && empty($data['text']) && ($data['type'] == 'photo')) {
$return .= sprintf('<a href="%s" target="_blank" rel="noopener noreferrer"><img src="%s" alt="" title="%s" class="' . $preview_class . '" /></a>', $data['url'], self::proxyUrl($data['image'], $simplehtml, $uriid), $data['title']); $is_photo = ($data['type'] === 'photo');
$link_attributes = $is_photo ? 'data-fancybox="gallery" rel="noopener noreferrer"' : 'target="_blank" rel="noopener noreferrer"';
if (!empty($data['image']) && empty($data['text']) && $is_photo) {
$return .= sprintf('<a href="%s" %s><img src="%s" alt="" title="%s" class="%s" /></a>', $data['url'], $link_attributes, self::proxyUrl($data['image'], $simplehtml, $uriid), $data['title'], $preview_class);
} else { } else {
if (!empty($data['image'])) { if (!empty($data['image'])) {
$return .= sprintf('<a href="%s" target="_blank" rel="noopener noreferrer"><img src="%s" alt="" title="%s" class="' . $preview_class . '" /></a><br>', $data['url'], self::proxyUrl($data['image'], $simplehtml, $uriid), $data['title']); $return .= sprintf('<a href="%s" %s><img src="%s" alt="" title="%s" class="%s" /></a><br>', $data['url'], $link_attributes, self::proxyUrl($data['image'], $simplehtml, $uriid), $data['title'], $preview_class);
} elseif (!empty($data['preview'])) { } elseif (!empty($data['preview'])) {
$return .= sprintf('<a href="%s" target="_blank" rel="noopener noreferrer"><img src="%s" alt="" title="%s" class="attachment-preview" /></a><br>', $data['url'], self::proxyUrl($data['preview'], $simplehtml, $uriid), $data['title']); $return .= sprintf('<a href="%s" %s><img src="%s" alt="" title="%s" class="attachment-preview" /></a><br>', $data['url'], $link_attributes, self::proxyUrl($data['preview'], $simplehtml, $uriid), $data['title']);
} }
$return .= sprintf('<h4><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></h4>', $data['url'], $data['title']); $return .= sprintf('<h4><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></h4>', $data['url'], $data['title']);
} }
} }
@ -535,7 +540,7 @@ class BBCode
} }
// If the link already is included in the post, don't add it again // If the link already is included in the post, don't add it again
if (!empty($data['url']) && strpos($data['text'], $data['url'])) { if (!empty($data['url']) && strpos($data['text'], (string) $data['url'])) {
DI::profiler()->stopRecording(); DI::profiler()->stopRecording();
return $data['text'] . $data['after']; return $data['text'] . $data['after'];
} }
@ -600,11 +605,11 @@ class BBCode
$res = [ $res = [
'start' => [ 'start' => [
'open' => $start_open, 'open' => $start_open,
'close' => $start_close 'close' => $start_close,
], ],
'end' => [ 'end' => [
'open' => $end_open, 'open' => $end_open,
'close' => $end_open + strlen('[/' . $name . ']') 'close' => $end_open + strlen('[/' . $name . ']'),
], ],
]; ];
@ -701,7 +706,7 @@ class BBCode
$newbody = str_replace( $newbody = str_replace(
'[$#saved_image' . $cnt . '#$]', '[$#saved_image' . $cnt . '#$]',
'<img src="' . self::proxyUrl($image, self::INTERNAL, $uriid) . '" alt="" class="empty-description"/>', '<img src="' . self::proxyUrl($image, self::INTERNAL, $uriid) . '" alt="" class="empty-description"/>',
$newbody $newbody,
); );
$cnt++; $cnt++;
} }
@ -811,9 +816,9 @@ class BBCode
function ($match) use ($callback, $uriid) { function ($match) use ($callback, $uriid) {
$attributes = self::extractShareAttributes($match[2]); $attributes = self::extractShareAttributes($match[2]);
$author_contact = Contact::getByURL($attributes['profile'], false, ['id', 'url', 'addr', 'name', 'micro']); $author_contact = Contact::getByURL($attributes['profile'], false, ['id', 'url', 'addr', 'name', 'micro']);
$author_contact['url'] = ($author_contact['url'] ?? $attributes['profile']); $author_contact['url'] ??= $attributes['profile'];
$author_contact['addr'] = ($author_contact['addr'] ?? ''); $author_contact['addr'] ??= '';
$attributes['author'] = ($author_contact['name'] ?? '') ?: $attributes['author']; $attributes['author'] = ($author_contact['name'] ?? '') ?: $attributes['author'];
$attributes['avatar'] = ($author_contact['micro'] ?? '') ?: $attributes['avatar']; $attributes['avatar'] = ($author_contact['micro'] ?? '') ?: $attributes['avatar'];
@ -829,7 +834,7 @@ class BBCode
return $match[1] . $callback($attributes, $author_contact, $content ?? '', trim($match[1]) != ''); return $match[1] . $callback($attributes, $author_contact, $content ?? '', trim($match[1]) != '');
}, },
$text $text,
); );
DI::profiler()->stopRecording(); DI::profiler()->stopRecording();
@ -861,7 +866,7 @@ class BBCode
$img_str .= ' ' . empty($attributes['alt']) ? 'class="empty-description"' : 'class="has-alt-description"'; $img_str .= ' ' . empty($attributes['alt']) ? 'class="empty-description"' : 'class="has-alt-description"';
return $img_str . '>'; return $img_str . '>';
}, },
$text $text,
); );
DI::profiler()->stopRecording(); DI::profiler()->stopRecording();
@ -890,9 +895,9 @@ class BBCode
switch ($simplehtml) { switch ($simplehtml) {
case self::MASTODON_API: case self::MASTODON_API:
case self::TWITTER_API: case self::TWITTER_API:
$text = ($is_quote_share ? '<br>' : '') . $text = ($is_quote_share ? '<br>' : '')
'<b><a href="' . $attributes['link'] . '">' . html_entity_decode('&#x2672;', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . "</a>:</b><br>\n" . . '<b><a href="' . $attributes['link'] . '">' . html_entity_decode('&#x2672;', ENT_QUOTES, 'UTF-8') . ' ' . $author_contact['addr'] . "</a>:</b><br>\n"
'<blockquote class="shared_content" dir="auto">' . $content . '</blockquote>'; . '<blockquote class="shared_content" dir="auto">' . $content . '</blockquote>';
break; break;
case self::DIASPORA: case self::DIASPORA:
if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0) { if (stripos(Strings::normaliseLink($attributes['link']), 'http://twitter.com/') === 0) {
@ -1027,7 +1032,7 @@ class BBCode
*/ */
private static function expandLinksCallback(array $match): string private static function expandLinksCallback(array $match): string
{ {
if (($match[3] == '') || ($match[2] == $match[3]) || stristr($match[2], $match[3])) { if (($match[3] == '') || ($match[2] == $match[3]) || stristr($match[2], (string) $match[3])) {
return ($match[1] . '[url]' . $match[2] . '[/url]'); return ($match[1] . '[url]' . $match[2] . '[/url]');
} else { } else {
return ($match[1] . $match[3] . ' [url]' . $match[2] . '[/url]'); return ($match[1] . $match[3] . ' [url]' . $match[2] . '[/url]');
@ -1414,7 +1419,7 @@ class BBCode
return $return; return $return;
}, },
$text $text,
); );
if (strpos($text, '<p>') !== false || strpos($text, '</p>') !== false) { if (strpos($text, '<p>') !== false || strpos($text, '</p>') !== false) {
@ -1485,11 +1490,11 @@ class BBCode
if (DI::config()->get('system', 'remove_multiplicated_lines')) { if (DI::config()->get('system', 'remove_multiplicated_lines')) {
$search = [ $search = [
"\n\n\n", "[/quote]\n\n", "\n[/quote]", "\n[ul]", "[/ul]\n", "\n[ol]", "[/ol]\n", "\n\n[share ", "[/attachment]\n", "\n\n\n", "[/quote]\n\n", "\n[/quote]", "\n[ul]", "[/ul]\n", "\n[ol]", "[/ol]\n", "\n\n[share ", "[/attachment]\n",
"\n[h1]", "[/h1]\n", "\n[h2]", "[/h2]\n", "\n[h3]", "[/h3]\n", "\n[h4]", "[/h4]\n", "\n[h5]", "[/h5]\n", "\n[h6]", "[/h6]\n" "\n[h1]", "[/h1]\n", "\n[h2]", "[/h2]\n", "\n[h3]", "[/h3]\n", "\n[h4]", "[/h4]\n", "\n[h5]", "[/h5]\n", "\n[h6]", "[/h6]\n",
]; ];
$replace = [ $replace = [
"\n\n", "[/quote]\n", "[/quote]", "[ul]", "[/ul]", "[ol]", "[/ol]", "\n[share ", "[/attachment]", "\n\n", "[/quote]\n", "[/quote]", "[ul]", "[/ul]", "[ol]", "[/ol]", "\n[share ", "[/attachment]",
"[h1]", "[/h1]", "[h2]", "[/h2]", "[h3]", "[/h3]", "[h4]", "[/h4]", "[h5]", "[/h5]", "[h6]", "[/h6]" "[h1]", "[/h1]", "[h2]", "[/h2]", "[h3]", "[/h3]", "[h4]", "[/h4]", "[h5]", "[/h5]", "[h6]", "[/h6]",
]; ];
do { do {
$oldtext = $text; $oldtext = $text;
@ -1552,7 +1557,7 @@ class BBCode
function ($match) use ($simple_html) { function ($match) use ($simple_html) {
return str_replace($match[0], '<p class="map">' . Map::byLocation($match[1], $simple_html) . '</p>', $match[0]); return str_replace($match[0], '<p class="map">' . Map::byLocation($match[1], $simple_html) . '</p>', $match[0]);
}, },
$text $text,
); );
} }
@ -1562,7 +1567,7 @@ class BBCode
function ($match) use ($simple_html) { function ($match) use ($simple_html) {
return str_replace($match[0], '<p class="map">' . Map::byCoordinates(str_replace('/', ' ', $match[1]), $simple_html) . '</p>', $match[0]); return str_replace($match[0], '<p class="map">' . Map::byCoordinates(str_replace('/', ' ', $match[1]), $simple_html) . '</p>', $match[0]);
}, },
$text $text,
); );
} }
@ -1631,7 +1636,7 @@ class BBCode
$elements = [ $elements = [
'del' => 's', 'ins' => 'em', 'kbd' => 'code', 'mark' => 'strong', 'del' => 's', 'ins' => 'em', 'kbd' => 'code', 'mark' => 'strong',
'samp' => 'code', 'u' => 'em', 'var' => 'em' 'samp' => 'code', 'u' => 'em', 'var' => 'em',
]; ];
foreach ($elements as $bbcode => $html) { foreach ($elements as $bbcode => $html) {
$text = preg_replace("(\[" . $bbcode . "\](.*?)\[\/" . $bbcode . "\])ism", '<' . $html . '>$1</' . $html . '>', $text); $text = preg_replace("(\[" . $bbcode . "\](.*?)\[\/" . $bbcode . "\])ism", '<' . $html . '>$1</' . $html . '>', $text);
@ -1642,7 +1647,7 @@ class BBCode
// @todo add the new elements to the documentation by the end of 2024 so that most systems will support them. // @todo add the new elements to the documentation by the end of 2024 so that most systems will support them.
$elements = [ $elements = [
'b', 'del', 'em', 'i', 'ins', 'kbd', 'mark', 'b', 'del', 'em', 'i', 'ins', 'kbd', 'mark',
's', 'samp', 'small', 'strong', 'sub', 'sup', 'u', 'var' 's', 'samp', 'small', 'strong', 'sub', 'sup', 'u', 'var',
]; ];
foreach ($elements as $element) { foreach ($elements as $element) {
$text = preg_replace("(\[" . $element . "\](.*?)\[\/" . $element . "\])ism", '<' . $element . '>$1</' . $element . '>', $text); $text = preg_replace("(\[" . $element . "\](.*?)\[\/" . $element . "\])ism", '<' . $element . '>$1</' . $element . '>', $text);
@ -1706,10 +1711,10 @@ class BBCode
// handle nested lists // handle nested lists
$endlessloop = 0; $endlessloop = 0;
while ((((strpos($text, "[/list]") !== false) && (strpos($text, "[list") !== false)) || while ((((strpos($text, "[/list]") !== false) && (strpos($text, "[list") !== false))
((strpos($text, "[/ol]") !== false) && (strpos($text, "[ol]") !== false)) || || ((strpos($text, "[/ol]") !== false) && (strpos($text, "[ol]") !== false))
((strpos($text, "[/ul]") !== false) && (strpos($text, "[ul]") !== false)) || || ((strpos($text, "[/ul]") !== false) && (strpos($text, "[ul]") !== false))
((strpos($text, "[/li]") !== false) && (strpos($text, "[li]") !== false))) && (++$endlessloop < 20)) { || ((strpos($text, "[/li]") !== false) && (strpos($text, "[li]") !== false))) && (++$endlessloop < 20)) {
$text = preg_replace("/\[list\](.*?)\[\/list\]/ism", '</p><ul class="listbullet" style="list-style-type: circle;">$1</ul><p>', $text); $text = preg_replace("/\[list\](.*?)\[\/list\]/ism", '</p><ul class="listbullet" style="list-style-type: circle;">$1</ul><p>', $text);
$text = preg_replace("/\[list=\](.*?)\[\/list\]/ism", '</p><ul class="listnone" style="list-style-type: none;">$1</ul><p>', $text); $text = preg_replace("/\[list=\](.*?)\[\/list\]/ism", '</p><ul class="listnone" style="list-style-type: none;">$1</ul><p>', $text);
$text = preg_replace("/\[list=1\](.*?)\[\/list\]/ism", '</p><ul class="listdecimal" style="list-style-type: decimal;">$1</ul><p>', $text); $text = preg_replace("/\[list=1\](.*?)\[\/list\]/ism", '</p><ul class="listdecimal" style="list-style-type: decimal;">$1</ul><p>', $text);
@ -1749,7 +1754,7 @@ class BBCode
$text = preg_replace( $text = preg_replace(
"/\[spoiler=[\"\']*(.*?)[\"\']*\](.*?)\[\/spoiler\]/ism", "/\[spoiler=[\"\']*(.*?)[\"\']*\](.*?)\[\/spoiler\]/ism",
'<details class="spoiler"><summary>$1</summary>$2</details>', '<details class="spoiler"><summary>$1</summary>$2</details>',
$text $text,
); );
} }
@ -1795,7 +1800,7 @@ class BBCode
$text = preg_replace( $text = preg_replace(
"/\[quote=[\"\']*(.*?)[\"\']*\](.*?)\[\/quote\]/ism", "/\[quote=[\"\']*(.*?)[\"\']*\](.*?)\[\/quote\]/ism",
"<p><strong class=" . '"author"' . ">" . $t_wrote . "</strong></p><blockquote>$2</blockquote>", "<p><strong class=" . '"author"' . ">" . $t_wrote . "</strong></p><blockquote>$2</blockquote>",
$text $text,
); );
} }
@ -1815,7 +1820,7 @@ class BBCode
$matches[3] = self::proxyUrl($matches[3], $simple_html, $uriid); $matches[3] = self::proxyUrl($matches[3], $simple_html, $uriid);
return "[img=" . $matches[1] . "x" . $matches[2] . "]" . $matches[3] . "[/img]"; return "[img=" . $matches[1] . "x" . $matches[2] . "]" . $matches[3] . "[/img]";
}, },
$text $text,
); );
$text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" alt="" class="empty-description">', $text); $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '<img src="$3" style="width: $1px;" alt="" class="empty-description">', $text);
@ -1833,7 +1838,7 @@ class BBCode
return '<img src="' . $matches[1] . '" alt="' . $alt . '">'; return '<img src="' . $matches[1] . '" alt="' . $alt . '">';
} }
}, },
$text $text,
); );
// Images // Images
@ -1848,7 +1853,7 @@ class BBCode
$matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid); $matches[1] = self::proxyUrl($matches[1], $simple_html, $uriid);
return "[img]" . $matches[1] . "[/img]"; return "[img]" . $matches[1] . "[/img]";
}, },
$text $text,
); );
$text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '<img src="$1" alt="" class="empty-description"/>', $text); $text = preg_replace("/\[img\](.*?)\[\/img\]/ism", '<img src="$1" alt="" class="empty-description"/>', $text);
@ -1878,12 +1883,12 @@ class BBCode
$text = preg_replace( $text = preg_replace(
"/\[video\](.*?)\[\/video\]/ism", "/\[video\](.*?)\[\/video\]/ism",
'</p><video src="$1" controls width="100%" height="auto">$1</video><p>', '</p><video src="$1" controls width="100%" height="auto">$1</video><p>',
$text $text,
); );
$text = preg_replace( $text = preg_replace(
"/\[audio\](.*?)\[\/audio\]/ism", "/\[audio\](.*?)\[\/audio\]/ism",
'</p><audio src="$1" controls>$1">$1</audio><p>', '</p><audio src="$1" controls>$1">$1</audio><p>',
$text $text,
); );
} else { } else {
$text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[embed]$1[/embed]', $text); $text = preg_replace("/\[video\](.*?)\[\/video\]/ism", '[embed]$1[/embed]', $text);
@ -1916,7 +1921,7 @@ class BBCode
function ($match) use ($max_length) { function ($match) use ($max_length) {
return '<a class="embed" href="' . self::escapeUrl(Network::sanitizeUrl($match[1])) . '">' . Strings::getStyledURL(Network::sanitizeUrl($match[1]), $max_length) . "</a>"; return '<a class="embed" href="' . self::escapeUrl(Network::sanitizeUrl($match[1])) . '">' . Strings::getStyledURL(Network::sanitizeUrl($match[1]), $max_length) . "</a>";
}, },
$text $text,
); );
return $text; return $text;
@ -1933,24 +1938,24 @@ class BBCode
$text = preg_replace( $text = preg_replace(
"/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", "/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'@<a href="$2">$3</a>', '@<a href="$2">$3</a>',
$text $text,
); );
} elseif (in_array($simple_html, [self::ACTIVITYPUB])) { } elseif (in_array($simple_html, [self::ACTIVITYPUB])) {
$text = preg_replace( $text = preg_replace(
"/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", "/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'<span class="h-card"><a href="$2" class="u-url mention">$1<span>$3</span></a></span>', '<span class="h-card"><a href="$2" class="u-url mention">$1<span>$3</span></a></span>',
$text $text,
); );
$text = preg_replace( $text = preg_replace(
"/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism", "/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'<a href="$2" class="mention hashtag" rel="tag">$1<span>$3</span></a>', '<a href="$2" class="mention hashtag" rel="tag">$1<span>$3</span></a>',
$text $text,
); );
} elseif (in_array($simple_html, [self::EXTERNAL, self::TWITTER_API])) { } elseif (in_array($simple_html, [self::EXTERNAL, self::TWITTER_API])) {
$text = preg_replace( $text = preg_replace(
"/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", "/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'<bdi>$1<a href="$2" class="userinfo mention" title="$3">$3</a></bdi>', '<bdi>$1<a href="$2" class="userinfo mention" title="$3">$3</a></bdi>',
$text $text,
); );
} elseif ($simple_html == self::INTERNAL) { } elseif ($simple_html == self::INTERNAL) {
if (preg_match_all("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", $text, $matches, PREG_SET_ORDER)) { if (preg_match_all("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", $text, $matches, PREG_SET_ORDER)) {
@ -1969,12 +1974,12 @@ class BBCode
$text = preg_replace( $text = preg_replace(
"/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", "/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'<a class="u-url mention status-link" href="$2" rel="nofollow noopener noreferrer" target="_blank" title="$3">$1<span>$3</span></a>', '<a class="u-url mention status-link" href="$2" rel="nofollow noopener noreferrer" target="_blank" title="$3">$1<span>$3</span></a>',
$text $text,
); );
$text = preg_replace( $text = preg_replace(
"/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism", "/([#])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'<a class="mention hashtag status-link" href="$2" rel="tag">$1<span>$3</span></a>', '<a class="mention hashtag status-link" href="$2" rel="tag">$1<span>$3</span></a>',
$text $text,
); );
} else { } else {
$text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text); $text = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $text);
@ -1991,7 +1996,7 @@ class BBCode
$text = preg_replace( $text = preg_replace(
"/#\[url\=.*?\]\^\[\/url\]\[url\=(.*?)\](.*?)\[\/url\]/i", "/#\[url\=.*?\]\^\[\/url\]\[url\=(.*?)\](.*?)\[\/url\]/i",
"[bookmark=$1]$2[/bookmark]", "[bookmark=$1]$2[/bookmark]",
$text $text,
); );
if (in_array($simple_html, [self::TWITTER, self::BLUESKY])) { if (in_array($simple_html, [self::TWITTER, self::BLUESKY])) {
@ -2008,7 +2013,7 @@ class BBCode
function ($match) { function ($match) {
return "[url=" . DI::baseUrl() . "/display/" . $match[1] . "]" . $match[2] . "[/url]"; return "[url=" . DI::baseUrl() . "/display/" . $match[1] . "]" . $match[2] . "[/url]";
}, },
$text $text,
); );
$text = preg_replace_callback( $text = preg_replace_callback(
@ -2016,7 +2021,7 @@ class BBCode
function ($match) { function ($match) {
return "[url=" . DI::baseUrl() . "/search?search=%40" . $match[1] . "]" . $match[2] . "[/url]"; return "[url=" . DI::baseUrl() . "/search?search=%40" . $match[1] . "]" . $match[2] . "[/url]";
}, },
$text $text,
); );
// Server independent link to posts and comments // Server independent link to posts and comments
@ -2083,7 +2088,7 @@ class BBCode
$parts['host'] = idn_to_ascii(urldecode($parts['host'])); $parts['host'] = idn_to_ascii(urldecode($parts['host']));
try { try {
return (string)Uri::fromParts($parts); return (string) Uri::fromParts($parts);
} catch (\Throwable $th) { } catch (\Throwable $th) {
DI::logger()->notice('Exception on unparsing url', ['url' => $url, 'parts' => $parts, 'code' => $th->getCode(), 'message' => $th->getMessage()]); DI::logger()->notice('Exception on unparsing url', ['url' => $url, 'parts' => $parts, 'code' => $th->getCode(), 'message' => $th->getMessage()]);
return $url; return $url;
@ -2097,7 +2102,7 @@ class BBCode
function ($match) { function ($match) {
return "[url=" . self::escapeUrl($match[1]) . "]" . $match[1] . "[/url]"; return "[url=" . self::escapeUrl($match[1]) . "]" . $match[1] . "[/url]";
}, },
$text $text,
); );
} }
@ -2114,7 +2119,7 @@ class BBCode
function ($match) use ($max_length) { function ($match) use ($max_length) {
return "[url=" . self::escapeUrl($match[1]) . "]" . Strings::getStyledURL($match[1], $max_length) . "[/url]"; return "[url=" . self::escapeUrl($match[1]) . "]" . Strings::getStyledURL($match[1], $max_length) . "[/url]";
}, },
$text $text,
); );
$text = preg_replace_callback( $text = preg_replace_callback(
"/\[url\=(.*?)\](.*?)\[\/url\]/ism", "/\[url\=(.*?)\](.*?)\[\/url\]/ism",
@ -2125,7 +2130,7 @@ class BBCode
return "[url=" . self::escapeUrl($match[1]) . "]" . $match[2] . "[/url]"; return "[url=" . self::escapeUrl($match[1]) . "]" . $match[2] . "[/url]";
} }
}, },
$text $text,
); );
return $text; return $text;
} }
@ -2151,7 +2156,7 @@ class BBCode
function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) { function (array $attributes, array $author_contact, $content, $is_quote_share) use ($simple_html) {
return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html); return self::convertShareCallback($attributes, $author_contact, $content, $is_quote_share, $simple_html);
}, },
$uriid $uriid,
); );
return $text; return $text;
@ -2179,7 +2184,7 @@ class BBCode
$text = preg_replace( $text = preg_replace(
'#<([^>]*?)(src)="(?!' . implode('|', $allowed_src_protocols) . ')(.*?)"(.*?)>#ism', '#<([^>]*?)(src)="(?!' . implode('|', $allowed_src_protocols) . ')(.*?)"(.*?)>#ism',
'<$1$2=""$4 data-original-src="$3" class="invalid-src" title="' . DI::l10n()->t('Invalid source protocol') . '">', '<$1$2=""$4 data-original-src="$3" class="invalid-src" title="' . DI::l10n()->t('Invalid source protocol') . '">',
$text $text,
); );
// sanitize href attributes (only allowlisted protocols URLs) // sanitize href attributes (only allowlisted protocols URLs)
@ -2302,7 +2307,7 @@ class BBCode
function ($matches) { function ($matches) {
return '#' . str_replace(' ', '_', $matches[2]); return '#' . str_replace(' ', '_', $matches[2]);
}, },
$text $text,
); );
// Converting images with size parameters to simple images. Markdown doesn't know it. // Converting images with size parameters to simple images. Markdown doesn't know it.
@ -2348,7 +2353,7 @@ class BBCode
$text = preg_replace_callback( $text = preg_replace_callback(
"/([@!])\[(.*?)\]\(([$url_search_string]*?)\)/ism", "/([@!])\[(.*?)\]\(([$url_search_string]*?)\)/ism",
[self::class, 'bbCodeMention2DiasporaCallback'], [self::class, 'bbCodeMention2DiasporaCallback'],
$text $text,
); );
} }
@ -2463,7 +2468,7 @@ class BBCode
return $match[1] . '[url=' . DI::baseUrl() . '/search?tag=' . urlencode($match[2]) . ']' . $match[2] . '[/url]'; return $match[1] . '[url=' . DI::baseUrl() . '/search?tag=' . urlencode($match[2]) . ']' . $match[2] . '[/url]';
} }
}, },
$body $body,
); );
} }
@ -2544,11 +2549,11 @@ class BBCode
public static function getShareOpeningTag(string $author, string $profile, string $avatar, string $link, string $posted, string $guid = null, string $uri = null): string public static function getShareOpeningTag(string $author, string $profile, string $avatar, string $link, string $posted, string $guid = null, string $uri = null): string
{ {
DI::profiler()->startRecording('rendering'); DI::profiler()->startRecording('rendering');
$header = "[share author='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $author) . $header = "[share author='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $author)
"' profile='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $profile) . . "' profile='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $profile)
"' avatar='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $avatar) . . "' avatar='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $avatar)
"' link='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $link) . . "' link='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $link)
"' posted='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $posted); . "' posted='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $posted);
if ($guid) { if ($guid) {
$header .= "' guid='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $guid); $header .= "' guid='" . str_replace(["'", "[", "]"], ["&#x27;", "&#x5B;", "&#x5D;"], $guid);
@ -2599,7 +2604,7 @@ class BBCode
} }
} }
$result = sprintf('[bookmark=%s]%s[/bookmark]%s', $url, ($title) ? $title : $url, $description) . $str_tags; $result = sprintf('[bookmark=%s]%s[/bookmark]%s', $url, $title ?: $url, $description) . $str_tags;
DI::logger()->info('(unparsed): returns: ' . $result); DI::logger()->info('(unparsed): returns: ' . $result);

View file

@ -153,7 +153,7 @@ class HTML
"<li>", "<li>",
"</li>", "</li>",
], ],
$message $message,
); );
// remove namespaces // remove namespaces
@ -209,7 +209,7 @@ class HTML
'div', 'div',
['style' => 'border:none;border-left:solid blue 1.5pt;padding:0cm 0cm 0cm 4.0pt'], ['style' => 'border:none;border-left:solid blue 1.5pt;padding:0cm 0cm 0cm 4.0pt'],
'[quote]', '[quote]',
'[/quote]' '[/quote]',
); );
// MyBB-Stuff // MyBB-Stuff
@ -244,7 +244,7 @@ class HTML
$elements = [ $elements = [
'b', 'del', 'em', 'i', 'ins', 'kbd', 'mark', 'b', 'del', 'em', 'i', 'ins', 'kbd', 'mark',
's', 'samp', 'strong', 'sub', 'sup', 'u', 'var' 's', 'samp', 'strong', 'sub', 'sup', 'u', 'var',
]; ];
foreach ($elements as $element) { foreach ($elements as $element) {
self::tagToBBCode($doc, $element, [], '[' . $element . ']', '[/' . $element . ']'); self::tagToBBCode($doc, $element, [], '[' . $element . ']', '[/' . $element . ']');
@ -348,7 +348,7 @@ class HTML
$message = str_replace( $message = str_replace(
['[b][b]', '[/b][/b]', '[i][i]', '[/i][/i]'], ['[b][b]', '[/b][/b]', '[i][i]', '[/i][/i]'],
['[b]', '[/b]', '[i]', '[/i]'], ['[b]', '[/b]', '[i]', '[/i]'],
$message $message,
); );
// Handling Yahoo style of mails // Handling Yahoo style of mails
@ -367,7 +367,7 @@ class HTML
return $prefix . "\n" . html_entity_decode($matches[2]) . "\n" . '[/code]'; return $prefix . "\n" . html_entity_decode($matches[2]) . "\n" . '[/code]';
}, },
$message $message,
); );
$message = trim($message); $message = trim($message);
@ -402,7 +402,7 @@ class HTML
} }
$parts = array_merge($base, parse_url($url)); $parts = array_merge($base, parse_url($url));
$url2 = (string)Uri::fromParts((array)$parts); $url2 = (string) Uri::fromParts((array) $parts);
return str_replace($url, $url2, $link); return str_replace($url, $url2, $link);
} }
@ -436,7 +436,7 @@ class HTML
function ($match) use ($basepath) { function ($match) use ($basepath) {
return self::qualifyURLsSub($match, $basepath); return self::qualifyURLsSub($match, $basepath);
}, },
$body $body,
); );
} }
return $body; return $body;
@ -531,7 +531,7 @@ class HTML
// A list of some links that should be ignored // A list of some links that should be ignored
$list = [ $list = [
"/user/", "/tag/", "/group/", "/circle/", "/profile/", "/search?search=", "/search?tag=", "mailto:", "/u/", "/node/", "/user/", "/tag/", "/group/", "/circle/", "/profile/", "/search?search=", "/search?tag=", "mailto:", "/u/", "/node/",
"//plus.google.com/", "//twitter.com/" "//plus.google.com/", "//twitter.com/",
]; ];
foreach ($list as $listitem) { foreach ($list as $listitem) {
if (strpos($treffer[1], $listitem) !== false) { if (strpos($treffer[1], $listitem) !== false) {
@ -660,7 +660,7 @@ class HTML
if (!$compact && ($message != '')) { if (!$compact && ($message != '')) {
foreach ($urls as $id => $url) { foreach ($urls as $id => $url) {
if ($url != '' && strpos($message, $url) === false) { if ($url != '' && strpos($message, (string) $url) === false) {
$message .= "\n" . $url . ' '; $message .= "\n" . $url . ' ';
} }
} }
@ -708,19 +708,19 @@ class HTML
$s = preg_replace( $s = preg_replace(
'#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism', '#<object[^>]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)</object>#ism',
'[youtube]$2[/youtube]', '[youtube]$2[/youtube]',
$s $s,
); );
$s = preg_replace( $s = preg_replace(
'#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism', '#<iframe[^>](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)</iframe>#ism',
'[youtube]$2[/youtube]', '[youtube]$2[/youtube]',
$s $s,
); );
$s = preg_replace( $s = preg_replace(
'#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism', '#<iframe[^>](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)</iframe>#ism',
'[vimeo]$2[/vimeo]', '[vimeo]$2[/vimeo]',
$s $s,
); );
return $s; return $s;
@ -772,12 +772,19 @@ class HTML
* @return string html for loader * @return string html for loader
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function scrollLoader(): string public static function scrollLoader(array $request = []): string
{ {
if (in_array($request['mode'] ?? '', ['minimal', 'raw'])) {
return '';
}
$tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
$loader = Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]);
$tpl = Renderer::getMarkupTemplate("scroll_loader.tpl"); $tpl = Renderer::getMarkupTemplate("scroll_loader.tpl");
return Renderer::replaceMacros($tpl, [ return $loader . Renderer::replaceMacros($tpl, [
'wait' => DI::l10n()->t('Loading more entries...'), 'wait' => DI::l10n()->t('Loading more entries...'),
'end' => DI::l10n()->t('The end') 'end' => DI::l10n()->t('The end'),
]); ]);
} }
@ -832,7 +839,7 @@ class HTML
'$name' => $contact['name'], '$name' => $contact['name'],
'title' => $contact['name'] . ' [' . $contact['addr'] . ']', 'title' => $contact['name'] . ' [' . $contact['addr'] . ']',
'$parkle' => $sparkle, '$parkle' => $sparkle,
'$redir' => $redir '$redir' => $redir,
]); ]);
} }
@ -870,7 +877,7 @@ class HTML
$values['$search_options'] = [ $values['$search_options'] = [
'fulltext' => DI::l10n()->t('Full Text'), 'fulltext' => DI::l10n()->t('Full Text'),
'tags' => DI::l10n()->t('Tags'), 'tags' => DI::l10n()->t('Tags'),
'contacts' => DI::l10n()->t('Contacts') 'contacts' => DI::l10n()->t('Contacts'),
]; ];
if (DI::config()->get('system', 'poco_local_search')) { if (DI::config()->get('system', 'poco_local_search')) {
@ -899,7 +906,7 @@ class HTML
'$reasons' => $reasons, '$reasons' => $reasons,
'$rnd' => Strings::getRandomHex(8), '$rnd' => Strings::getRandomHex(8),
'$openclose' => DI::l10n()->t('Click to open/close'), '$openclose' => DI::l10n()->t('Click to open/close'),
'$html' => $html '$html' => $html,
]); ]);
} }
@ -947,7 +954,7 @@ class HTML
' . implode('|', $allowedIframeDomains) . ' ' . implode('|', $allowedIframeDomains) . '
) )
(?:/|$) # Prevents bogus domains like youtube.com.fake.tld (?:/|$) # Prevents bogus domains like youtube.com.fake.tld
%xi' %xi',
); );
$config->set('Attr.AllowedRel', [ $config->set('Attr.AllowedRel', [
@ -1073,7 +1080,7 @@ class HTML
* *
* @param string $html * @param string $html
* @param string $className * @param string $className
* @return string the HTML without the removed HTML element * @return string the HTML without the removed HTML element
*/ */
public static function removeElementByClass(string $html, string $className): string public static function removeElementByClass(string $html, string $className): string
{ {

View file

@ -16,9 +16,12 @@ class MarkdownParser extends MarkdownExtra
{ {
$text = parent::doAutoLinks($text); $text = parent::doAutoLinks($text);
$text = preg_replace_callback(Strings::autoLinkRegEx(), $text = preg_replace_callback(
array($this, '_doAutoLinks_url_callback'), $text); Strings::autoLinkRegEx(),
[$this, '_doAutoLinks_url_callback'],
$text,
);
return $text; return $text;
} }
} }

View file

@ -47,7 +47,7 @@ class NPF
$element = $doc->getElementsByTagName('body')->item(0); $element = $doc->getElementsByTagName('body')->item(0);
list($npf, $text, $formatting) = self::routeChildren($element, $uri_id, true, []); [$npf, $text, $formatting] = self::routeChildren($element, $uri_id, true, []);
return self::addLinkBlockForUriId($uri_id, 0, $npf); return self::addLinkBlockForUriId($uri_id, 0, $npf);
} }
@ -130,7 +130,7 @@ class NPF
private static function routeChildren(DOMElement $element, int $uri_id, bool $parse_structure, array $callstack, array $npf = [], string $text = '', array $formatting = []): array private static function routeChildren(DOMElement $element, int $uri_id, bool $parse_structure, array $callstack, array $npf = [], string $text = '', array $formatting = []): array
{ {
if ($parse_structure && $text) { if ($parse_structure && $text) {
list($npf, $text, $formatting) = self::addBlock($text, $formatting, $npf, $callstack); [$npf, $text, $formatting] = self::addBlock($text, $formatting, $npf, $callstack);
} }
$callstack[] = $element->nodeName; $callstack[] = $element->nodeName;
@ -140,21 +140,21 @@ class NPF
switch ($child->nodeName) { switch ($child->nodeName) {
case 'b': case 'b':
case 'strong': case 'strong':
list($npf, $text, $formatting) = self::addFormatting($child, $uri_id, 'bold', $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::addFormatting($child, $uri_id, 'bold', $callstack, $npf, $text, $formatting);
break; break;
case 'i': case 'i':
case 'em': case 'em':
list($npf, $text, $formatting) = self::addFormatting($child, $uri_id, 'italic', $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::addFormatting($child, $uri_id, 'italic', $callstack, $npf, $text, $formatting);
break; break;
case 's': case 's':
list($npf, $text, $formatting) = self::addFormatting($child, $uri_id, 'strikethrough', $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::addFormatting($child, $uri_id, 'strikethrough', $callstack, $npf, $text, $formatting);
break; break;
case 'u': case 'u':
case 'span': case 'span':
list($npf, $text, $formatting) = self::addFormatting($child, $uri_id, '', $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::addFormatting($child, $uri_id, '', $callstack, $npf, $text, $formatting);
break; break;
case 'hr': case 'hr':
@ -174,7 +174,7 @@ class NPF
break; break;
case 'a': case 'a':
list($npf, $text, $formatting) = self::addInlineLink($child, $uri_id, $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::addInlineLink($child, $uri_id, $callstack, $npf, $text, $formatting);
break; break;
case 'img': case 'img':
@ -187,13 +187,13 @@ class NPF
break; break;
default: default:
list($npf, $text, $formatting) = self::routeChildren($child, $uri_id, true, $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::routeChildren($child, $uri_id, true, $callstack, $npf, $text, $formatting);
break; break;
} }
} }
if ($parse_structure && $text) { if ($parse_structure && $text) {
list($npf, $text, $formatting) = self::addBlock($text, $formatting, $npf, $callstack); [$npf, $text, $formatting] = self::addBlock($text, $formatting, $npf, $callstack);
} }
return [$npf, $text, $formatting]; return [$npf, $text, $formatting];
} }
@ -291,7 +291,7 @@ class NPF
{ {
$start = mb_strlen($text); $start = mb_strlen($text);
list($npf, $text, $formatting) = self::routeChildren($element, $uri_id, false, $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::routeChildren($element, $uri_id, false, $callstack, $npf, $text, $formatting);
if (!empty($type)) { if (!empty($type)) {
$formatting[] = [ $formatting[] = [
@ -318,7 +318,7 @@ class NPF
{ {
$start = mb_strlen($text); $start = mb_strlen($text);
list($npf, $text, $formatting) = self::routeChildren($element, $uri_id, false, $callstack, $npf, $text, $formatting); [$npf, $text, $formatting] = self::routeChildren($element, $uri_id, false, $callstack, $npf, $text, $formatting);
$attributes = []; $attributes = [];
foreach ($element->attributes as $attribute) { foreach ($element->attributes as $attribute) {

View file

@ -17,7 +17,7 @@ use IntlChar;
class Plaintext class Plaintext
{ {
// Assumed length of an URL when shortened via the network's own url shortener (e.g. Twitter) // Assumed length of an URL when shortened via the network's own url shortener (e.g. Twitter)
const URL_LENGTH = 23; public const URL_LENGTH = 23;
/** /**
* Shortens message * Shortens message
@ -37,8 +37,8 @@ class Plaintext
return mb_substr(mb_substr(trim($msg), 0, $limit), 0, -3) . $ellipsis; return mb_substr(mb_substr(trim($msg), 0, $limit), 0, -3) . $ellipsis;
} }
$lines = explode("\n", $msg); $lines = explode("\n", $msg);
$msg = ""; $msg = "";
$recycle = html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8'); $recycle = html_entity_decode("&#x2672; ", ENT_QUOTES, 'UTF-8');
foreach ($lines as $row => $line) { foreach ($lines as $row => $line) {
if (mb_strlen(trim($msg . "\n" . $line)) <= $limit) { if (mb_strlen(trim($msg . "\n" . $line)) <= $limit) {
@ -138,13 +138,13 @@ class Plaintext
if ($post['type'] == 'text') { if ($post['type'] == 'text') {
$post['type'] = 'link'; $post['type'] = 'link';
$post['url'] = $item['plink']; $post['url'] = $item['plink'];
} }
} }
$html = BBCode::convertForUriId($item['uri-id'], $post['text'] . ($post['after'] ?? ''), $htmlmode); $html = BBCode::convertForUriId($item['uri-id'], $post['text'] . ($post['after'] ?? ''), $htmlmode);
$msg = HTML::toPlaintext($html, 0, true); $msg = HTML::toPlaintext($html, 0, true);
$msg = trim(html_entity_decode($msg, ENT_QUOTES, 'UTF-8')); $msg = trim(html_entity_decode($msg, ENT_QUOTES, 'UTF-8'));
$complete_msg = $msg; $complete_msg = $msg;
@ -170,8 +170,8 @@ class Plaintext
// If the link is already contained in the post, then it needn't to be added again // If the link is already contained in the post, then it needn't to be added again
// But: if the link is beyond the limit, then it has to be added. // But: if the link is beyond the limit, then it has to be added.
if (($link != '') && strstr($msg, $link)) { if (($link != '') && strstr($msg, (string) $link)) {
$pos = strpos($msg, $link); $pos = strpos($msg, (string) $link);
// Will the text be shortened in the link? // Will the text be shortened in the link?
// Or is the link the last item in the post? // Or is the link the last item in the post?
@ -200,7 +200,7 @@ class Plaintext
$msg = str_replace(' ', ' ', $msg); $msg = str_replace(' ', ' ', $msg);
} }
if (!in_array($link, ['', $item['plink']]) && ($post['type'] != 'photo') && (strpos($complete_msg, $link) === false)) { if (!in_array($link, ['', $item['plink']]) && ($post['type'] != 'photo') && (strpos($complete_msg, (string) $link) === false)) {
$complete_msg .= "\n" . $link; $complete_msg .= "\n" . $link;
} }
@ -215,7 +215,7 @@ class Plaintext
if (($post['type'] == 'text') && isset($post['url'])) { if (($post['type'] == 'text') && isset($post['url'])) {
$post['url'] = $item['plink']; $post['url'] = $item['plink'];
} elseif (!isset($post['url'])) { } elseif (!isset($post['url'])) {
$limit = $limit - self::URL_LENGTH; $limit = $limit - self::URL_LENGTH;
$post['url'] = $item['plink']; $post['url'] = $item['plink'];
} elseif (strpos($item['body'], '[share') !== false) { } elseif (strpos($item['body'], '[share') !== false) {
$post['url'] = $item['plink']; $post['url'] = $item['plink'];
@ -250,7 +250,7 @@ class Plaintext
$limit = $baselimit; $limit = $baselimit;
while ($message) { while ($message) {
$pos_word = mb_strpos($message, ' '); $pos_word = mb_strpos($message, ' ');
$pos_paragraph = mb_strpos($message, "\n"); $pos_paragraph = mb_strpos($message, "\n");
if (($pos_word !== false) && ($pos_paragraph !== false)) { if (($pos_word !== false) && ($pos_paragraph !== false)) {
@ -260,8 +260,8 @@ class Plaintext
} elseif ($pos_paragraph !== false) { } elseif ($pos_paragraph !== false) {
$pos = $pos_paragraph + 1; $pos = $pos_paragraph + 1;
} else { } else {
$word = $message; $word = $message;
$message = ''; $message = '';
} }
if (trim($message)) { if (trim($message)) {
@ -324,7 +324,7 @@ class Plaintext
// Remove mentions and hashtag links // Remove mentions and hashtag links
$URLSearchString = '^\[\]'; $URLSearchString = '^\[\]';
$post['text'] = preg_replace("/([#!@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $item['body']); $post['text'] = preg_replace("/([#!@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $item['body']);
// Remove abstract // Remove abstract
$post['text'] = BBCode::stripAbstract($post['text']); $post['text'] = BBCode::stripAbstract($post['text']);
@ -382,10 +382,10 @@ class Plaintext
$page = Post\Media::getByURIId($item['quote-uri-id'], [Post\Media::HTML]); $page = Post\Media::getByURIId($item['quote-uri-id'], [Post\Media::HTML]);
} }
if (!empty($page)) { if (!empty($page)) {
$post['type'] = 'link'; $post['type'] = 'link';
$post['url'] = $page[0]['url']; $post['url'] = $page[0]['url'];
$post['description'] = $page[0]['description']; $post['description'] = $page[0]['description'];
$post['title'] = $page[0]['name']; $post['title'] = $page[0]['name'];
if (empty($post['image']) && !empty($page[0]['preview'])) { if (empty($post['image']) && !empty($page[0]['preview'])) {
$post['image'] = $page[0]['preview']; $post['image'] = $page[0]['preview'];

View file

@ -37,7 +37,7 @@ class Widget
'$desc' => DI::l10n()->t('Enter address or web location'), '$desc' => DI::l10n()->t('Enter address or web location'),
'$hint' => DI::l10n()->t('user@x.tld, x.tld/user'), '$hint' => DI::l10n()->t('user@x.tld, x.tld/user'),
'$value' => $value, '$value' => $value,
'$follow' => DI::l10n()->t('Connect') '$follow' => DI::l10n()->t('Connect'),
]); ]);
} }
@ -48,6 +48,12 @@ class Widget
*/ */
public static function findPeople(): string public static function findPeople(): string
{ {
// Langs in friendica are all lowercase and dash separated (e.g. da-dk) - in the directory
// they're underscore separated, with the latter part being uppercase (e.g. da_DK)
$directory_user_lang_string = "";
if (DI::userSession()->get('language')) {
$directory_user_lang_string = "&lang=" . DI::l10n()->langToLocaleCode(DI::userSession()->get('language'));
}
$global_dir = Search::getGlobalDirectory(); $global_dir = Search::getGlobalDirectory();
if (DI::config()->get('system', 'invitation_only')) { if (DI::config()->get('system', 'invitation_only')) {
@ -70,7 +76,7 @@ class Widget
$nv['random'] = DI::l10n()->t('Random Profile'); $nv['random'] = DI::l10n()->t('Random Profile');
$nv['inv'] = DI::l10n()->t('Invite Friends'); $nv['inv'] = DI::l10n()->t('Invite Friends');
$nv['directory'] = DI::l10n()->t('Global Directory'); $nv['directory'] = DI::l10n()->t('Global Directory');
$nv['global_dir'] = OpenWebAuth::getZrlUrl($global_dir, true); $nv['global_dir'] = OpenWebAuth::getZrlUrl($global_dir, true) . $directory_user_lang_string;
$nv['local_directory'] = DI::l10n()->t('Local Directory'); $nv['local_directory'] = DI::l10n()->t('Local Directory');
$aside = []; $aside = [];
@ -224,7 +230,7 @@ class Widget
$options = array_map(function ($circle) { $options = array_map(function ($circle) {
return [ return [
'ref' => $circle['id'], 'ref' => $circle['id'],
'name' => $circle['name'] 'name' => $circle['name'],
]; ];
}, Circle::getByUserId(DI::userSession()->getLocalUserId())); }, Circle::getByUserId(DI::userSession()->getLocalUserId()));
@ -235,7 +241,7 @@ class Widget
DI::l10n()->t('Everyone'), DI::l10n()->t('Everyone'),
$baseurl, $baseurl,
$options, $options,
$selected $selected,
); );
} }
@ -267,7 +273,7 @@ class Widget
DI::l10n()->t('All Contacts'), DI::l10n()->t('All Contacts'),
$baseurl, $baseurl,
$options, $options,
$selected $selected,
); );
} }
@ -308,7 +314,7 @@ class Widget
DI::l10n()->t('All Protocols'), DI::l10n()->t('All Protocols'),
$baseurl, $baseurl,
$nets, $nets,
$selected $selected,
); );
} }
@ -338,7 +344,7 @@ class Widget
DI::l10n()->t('Everything'), DI::l10n()->t('Everything'),
$baseurl, $baseurl,
$terms, $terms,
$selected $selected,
); );
} }
@ -369,7 +375,7 @@ class Widget
DI::l10n()->t('Everything'), DI::l10n()->t('Everything'),
$baseurl, $baseurl,
$terms, $terms,
$selected $selected,
); );
} }
@ -426,7 +432,7 @@ class Widget
'$nickname' => $nickname, '$nickname' => $nickname,
'$linkmore' => $total > 5 ? 'true' : '', '$linkmore' => $total > 5 ? 'true' : '',
'$more' => DI::l10n()->t('show more'), '$more' => DI::l10n()->t('show more'),
'$contacts' => $entries '$contacts' => $entries,
]); ]);
} }
@ -475,7 +481,7 @@ class Widget
$ret = []; $ret = [];
$cachekey = 'Widget::postedByYear' . $uid . '-' . (int)$wall; $cachekey = 'Widget::postedByYear' . $uid . '-' . (int) $wall;
$dthen = DI::cache()->get($cachekey); $dthen = DI::cache()->get($cachekey);
if (empty($dthen)) { if (empty($dthen)) {
$dthen = Item::firstPostDate($uid, $wall); $dthen = Item::firstPostDate($uid, $wall);
@ -505,7 +511,7 @@ class Widget
$dend = substr($dnow, 0, 8) . Temporal::getDaysInMonth(intval($dnow), intval(substr($dnow, 5))); $dend = substr($dnow, 0, 8) . Temporal::getDaysInMonth(intval($dnow), intval(substr($dnow, 5)));
$start_month = DateTimeFormat::utc($dstart, 'Y-m-d'); $start_month = DateTimeFormat::utc($dstart, 'Y-m-d');
$end_month = DateTimeFormat::utc($dend, 'Y-m-d'); $end_month = DateTimeFormat::utc($dend, 'Y-m-d');
$str = DI::l10n()->getDay(DateTimeFormat::utc($dnow, 'F')); $str = DI::l10n()->formatDateTimeByPattern($dnow, 'LLLL');
if (empty($ret[$dyear])) { if (empty($ret[$dyear])) {
$ret[$dyear] = []; $ret[$dyear] = [];
@ -535,7 +541,7 @@ class Widget
'$onthisdate' => DI::l10n()->t('On this date'), '$onthisdate' => DI::l10n()->t('On this date'),
'$thisday' => $thisday, '$thisday' => $thisday,
'$nextday' => $nextday, '$nextday' => $nextday,
'$cutoffday' => $cutoffday '$cutoffday' => $cutoffday,
]); ]);
return $o; return $o;
@ -566,7 +572,7 @@ class Widget
DI::l10n()->t('All'), DI::l10n()->t('All'),
$base, $base,
$accounts, $accounts,
$accounttype $accounttype,
); );
} }
@ -611,9 +617,9 @@ class Widget
$widget_timelineorder = json_decode(DI::pConfig()->get($uid, 'system', 'widget_timeline_order')); $widget_timelineorder = json_decode(DI::pConfig()->get($uid, 'system', 'widget_timeline_order'));
if (!empty($widget_timelineorder)) { if (!empty($widget_timelineorder)) {
$tmp = []; $tmp = [];
foreach($widget_timelineorder as $order) { foreach ($widget_timelineorder as $order) {
foreach($channels as $channel) { foreach ($channels as $channel) {
if($channel['ref'] == $order) { if ($channel['ref'] == $order) {
$tmp[] = $channel; $tmp[] = $channel;
} }
} }
@ -628,7 +634,7 @@ class Widget
'', '',
$base, $base,
$channels, $channels,
$channelname $channelname,
); );
} }
} }

View file

@ -38,7 +38,7 @@ class TagCloud
$r = self::tagadelic($uid, $count, $owner_id, $flags, $type); $r = self::tagadelic($uid, $count, $owner_id, $flags, $type);
if (count($r)) { if (count($r)) {
$contact = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]); $contact = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
$url = DI::baseUrl()->remove($contact['url']); $url = DI::baseUrl()->remove($contact['url']);
$tags = []; $tags = [];
foreach ($r as $rr) { foreach ($r as $rr) {
@ -50,9 +50,9 @@ class TagCloud
} }
$tpl = Renderer::getMarkupTemplate('widget/tagcloud.tpl'); $tpl = Renderer::getMarkupTemplate('widget/tagcloud.tpl');
$o = Renderer::replaceMacros($tpl, [ $o = Renderer::replaceMacros($tpl, [
'$title' => DI::l10n()->t('Tags'), '$title' => DI::l10n()->t('Tags'),
'$tags' => $tags '$tags' => $tags,
]); ]);
} }
return $o; return $o;
@ -74,7 +74,7 @@ class TagCloud
private static function tagadelic($uid, $count = 0, $owner_id = 0, $flags = '', $type = Tag::HASHTAG) private static function tagadelic($uid, $count = 0, $owner_id = 0, $flags = '', $type = Tag::HASHTAG)
{ {
$sql_options = Item::getPermissionsSQLByUserId($uid, 'post-user-view'); $sql_options = Item::getPermissionsSQLByUserId($uid, 'post-user-view');
$limit = $count ? sprintf('LIMIT %d', intval($count)) : ''; $limit = $count ? sprintf('LIMIT %d', intval($count)) : '';
if ($flags) { if ($flags) {
if ($flags === 'wall') { if ($flags === 'wall') {
@ -87,13 +87,14 @@ class TagCloud
} }
// Fetch tags // Fetch tags
$tag_stmt = DBA::p("SELECT `name`, COUNT(`name`) AS `total` FROM `tag-search-view` $tag_stmt = DBA::p(
"SELECT `name`, COUNT(`name`) AS `total` FROM `tag-search-view`
LEFT JOIN `post-user-view` ON `tag-search-view`.`uri-id` = `post-user-view`.`uri-id` AND `tag-search-view`.`uid` = `post-user-view`.`uid` LEFT JOIN `post-user-view` ON `tag-search-view`.`uri-id` = `post-user-view`.`uri-id` AND `tag-search-view`.`uid` = `post-user-view`.`uid`
WHERE `tag-search-view`.`uid` = ? WHERE `tag-search-view`.`uid` = ?
AND `post-user-view`.`visible` AND NOT `post-user-view`.`deleted` AND `post-user-view`.`visible` AND NOT `post-user-view`.`deleted`
$sql_options $sql_options
GROUP BY `name` ORDER BY `total` DESC $limit", GROUP BY `name` ORDER BY `total` DESC $limit",
$uid $uid,
); );
if (!DBA::isResult($tag_stmt)) { if (!DBA::isResult($tag_stmt)) {
return []; return [];
@ -113,9 +114,9 @@ class TagCloud
private static function tagCalc(array $arr) private static function tagCalc(array $arr)
{ {
$tags = []; $tags = [];
$min = 1000000000.0; $min = 1000000000.0;
$max = -1000000000.0; $max = -1000000000.0;
$x = 0; $x = 0;
if (!$arr) { if (!$arr) {
return []; return [];
@ -125,15 +126,15 @@ class TagCloud
$tags[$x][0] = $rr['name']; $tags[$x][0] = $rr['name'];
$tags[$x][1] = log($rr['total']); $tags[$x][1] = log($rr['total']);
$tags[$x][2] = 0; $tags[$x][2] = 0;
$min = min($min, $tags[$x][1]); $min = min($min, $tags[$x][1]);
$max = max($max, $tags[$x][1]); $max = max($max, $tags[$x][1]);
$x ++; $x++;
} }
usort($tags, [self::class, 'tagsSort']); usort($tags, [self::class, 'tagsSort']);
$range = max(0.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);
} }
@ -150,9 +151,6 @@ class TagCloud
*/ */
private static function tagsSort($a, $b) private static function tagsSort($a, $b)
{ {
if (strtolower($a[0]) == strtolower($b[0])) { return strtolower($a[0]) <=> strtolower($b[0]);
return 0;
}
return ((strtolower($a[0]) < strtolower($b[0])) ? -1 : 1);
} }
} }

View file

@ -13,18 +13,18 @@ use Friendica\Util\Strings;
/** /**
* Some functions to handle addons * Some functions to handle addons
* *
* @deprecated 2025.07 Use implementation of `Friendica\Core\Addon\AddonHelper` instead * @deprecated 2026.01 Use implementation of `Friendica\Core\Addon\AddonHelper` instead
*/ */
class Addon class Addon
{ {
/** /**
* The addon sub-directory * The addon sub-directory
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::getAddonPath()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::getAddonPath()` instead
* *
* @var string * @var string
*/ */
const DIRECTORY = 'addon'; public const DIRECTORY = 'addon';
/** /**
* List of the names of enabled addons * List of the names of enabled addons
@ -38,22 +38,22 @@ class Addon
* This list is made from scanning the addon/ folder. * This list is made from scanning the addon/ folder.
* Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set. * Unsupported addons are excluded unless they already are enabled or system.show_unsupported_addon is set.
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::getAvailableAddons()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::getAvailableAddons()` instead
* *
* @return array * @return array
* @throws \Exception * @throws \Exception
*/ */
public static function getAvailableList(): array public static function getAvailableList(): array
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$addons = []; $addons = [];
$files = glob('addon/*/'); $files = glob('addon/*/');
if (is_array($files)) { if (is_array($files)) {
foreach ($files as $file) { foreach ($files as $file) {
if (is_dir($file)) { if (is_dir($file)) {
list($tmp, $addon) = array_map('trim', explode('/', $file)); [$tmp, $addon] = array_map('trim', explode('/', $file));
$info = self::getInfo($addon); $info = self::getInfo($addon);
if (DI::config()->get('system', 'show_unsupported_addons') if (DI::config()->get('system', 'show_unsupported_addons')
|| strtolower($info['status']) != 'unsupported' || strtolower($info['status']) != 'unsupported'
@ -72,14 +72,14 @@ class Addon
* Returns a list of addons that can be configured at the node level. * Returns a list of addons that can be configured at the node level.
* The list is formatted for display in the admin panel aside. * The list is formatted for display in the admin panel aside.
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::getEnabledAddonsWithAdminSettings()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::getEnabledAddonsWithAdminSettings()` instead
* *
* @return array * @return array
* @throws \Exception * @throws \Exception
*/ */
public static function getAdminList(): array public static function getAdminList(): array
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$addons_admin = []; $addons_admin = [];
$addons = array_filter(DI::config()->get('addons') ?? []); $addons = array_filter(DI::config()->get('addons') ?? []);
@ -93,7 +93,7 @@ class Addon
$addons_admin[$name] = [ $addons_admin[$name] = [
'url' => 'admin/addons/' . $name, 'url' => 'admin/addons/' . $name,
'name' => $name, 'name' => $name,
'class' => 'addon' 'class' => 'addon',
]; ];
} }
@ -111,11 +111,11 @@ class Addon
* Then go through the config list and if we have a addon that isn't installed, * Then go through the config list and if we have a addon that isn't installed,
* call the install procedure and add it to the database. * call the install procedure and add it to the database.
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::loadAddons()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::loadAddons()` instead
*/ */
public static function loadAddons() public static function loadAddons()
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
self::$addons = array_keys(array_filter(DI::config()->get('addons') ?? [])); self::$addons = array_keys(array_filter(DI::config()->get('addons') ?? []));
} }
@ -123,7 +123,7 @@ class Addon
/** /**
* uninstalls an addon. * uninstalls an addon.
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::uninstallAddon()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::uninstallAddon()` instead
* *
* @param string $addon name of the addon * @param string $addon name of the addon
* @return void * @return void
@ -131,7 +131,7 @@ class Addon
*/ */
public static function uninstall(string $addon) public static function uninstall(string $addon)
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$addon = Strings::sanitizeFilePathItem($addon); $addon = Strings::sanitizeFilePathItem($addon);
@ -153,7 +153,7 @@ class Addon
/** /**
* installs an addon. * installs an addon.
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::installAddon()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::installAddon()` instead
* *
* @param string $addon name of the addon * @param string $addon name of the addon
* @return bool * @return bool
@ -161,7 +161,7 @@ class Addon
*/ */
public static function install(string $addon): bool public static function install(string $addon): bool
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$addon = Strings::sanitizeFilePathItem($addon); $addon = Strings::sanitizeFilePathItem($addon);
@ -195,7 +195,7 @@ class Addon
/** /**
* reload all updated addons * reload all updated addons
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::reloadAddons()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::reloadAddons()` instead
* *
* @return void * @return void
* @throws \Exception * @throws \Exception
@ -203,7 +203,7 @@ class Addon
*/ */
public static function reload() public static function reload()
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$addons = array_filter(DI::config()->get('addons') ?? []); $addons = array_filter(DI::config()->get('addons') ?? []);
@ -236,7 +236,7 @@ class Addon
* * * *
* *\endcode * *\endcode
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::getAddonInfo()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::getAddonInfo()` instead
* *
* @param string $addon the name of the addon * @param string $addon the name of the addon
* @return array with the addon information * @return array with the addon information
@ -244,7 +244,7 @@ class Addon
*/ */
public static function getInfo(string $addon): array public static function getInfo(string $addon): array
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$addon = Strings::sanitizeFilePathItem($addon); $addon = Strings::sanitizeFilePathItem($addon);
@ -254,7 +254,7 @@ class Addon
'author' => [], 'author' => [],
'maintainer' => [], 'maintainer' => [],
'version' => "", 'version' => "",
'status' => "" 'status' => "",
]; ];
if (!is_file("addon/$addon/$addon.php")) { if (!is_file("addon/$addon/$addon.php")) {
@ -277,8 +277,8 @@ class Addon
continue; continue;
} }
list($type, $v) = $addon_info; [$type, $v] = $addon_info;
$type = strtolower($type); $type = strtolower($type);
if ($type == "author" || $type == "maintainer") { if ($type == "author" || $type == "maintainer") {
$r = preg_match("|([^<]+)<([^>]+)>|", $v, $m); $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);
if ($r) { if ($r) {
@ -300,14 +300,14 @@ class Addon
/** /**
* Checks if the provided addon is enabled * Checks if the provided addon is enabled
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::isAddonEnabled()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::isAddonEnabled()` instead
* *
* @param string $addon * @param string $addon
* @return boolean * @return boolean
*/ */
public static function isEnabled(string $addon): bool public static function isEnabled(string $addon): bool
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
return in_array($addon, self::$addons); return in_array($addon, self::$addons);
} }
@ -315,13 +315,13 @@ class Addon
/** /**
* Returns a list of the enabled addon names * Returns a list of the enabled addon names
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::getEnabledAddons()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::getEnabledAddons()` instead
* *
* @return array * @return array
*/ */
public static function getEnabledList(): array public static function getEnabledList(): array
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
return self::$addons; return self::$addons;
} }
@ -329,14 +329,14 @@ class Addon
/** /**
* Returns the list of non-hidden enabled addon names * Returns the list of non-hidden enabled addon names
* *
* @deprecated 2025.07 Use `Friendica\Core\Addon\AddonHelper::getVisibleEnabledAddons()` instead * @deprecated 2026.01 Use `Friendica\Core\Addon\AddonHelper::getVisibleEnabledAddons()` instead
* *
* @return array * @return array
* @throws \Exception * @throws \Exception
*/ */
public static function getVisibleList(): array public static function getVisibleList(): array
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$visible_addons = []; $visible_addons = [];
$addons = array_filter(DI::config()->get('addons') ?? []); $addons = array_filter(DI::config()->get('addons') ?? []);

View file

@ -51,8 +51,8 @@ final class AddonInfo
continue; continue;
} }
list($type, $v) = $addon_info; [$type, $v] = $addon_info;
$type = strtolower($type); $type = strtolower($type);
if ($type === 'author' || $type === 'maintainer') { if ($type === 'author' || $type === 'maintainer') {
$r = preg_match("|([^<]+)<([^>]+)>|", $v, $m); $r = preg_match("|([^<]+)<([^>]+)>|", $v, $m);

View file

@ -10,14 +10,14 @@ namespace Friendica\Core\Addon\Capability;
/** /**
* Interface for loading Addons specific content * Interface for loading Addons specific content
* *
* @deprecated 2025.07 Use implementation of `\Friendica\Core\Addon\AddonHelper` instead. * @deprecated 2026.01 Use implementation of `\Friendica\Core\Addon\AddonHelper` instead.
*/ */
interface ICanLoadAddons interface ICanLoadAddons
{ {
/** /**
* Returns a merged config array of all active addons for a given config-name * Returns a merged config array of all active addons for a given config-name
* *
* @deprecated 2025.07 Use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead. * @deprecated 2026.01 Use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead.
* *
* @param string $configName The config-name (config-file at the static directory, like 'hooks' => '{addon}/static/hooks.config.php) * @param string $configName The config-name (config-file at the static directory, like 'hooks' => '{addon}/static/hooks.config.php)
* *

View file

@ -15,11 +15,11 @@ use Friendica\Util\Strings;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
* @deprecated 2025.07 Use implementation of `\Friendica\Core\Addon\AddonHelper` instead. * @deprecated 2026.01 Use implementation of `\Friendica\Core\Addon\AddonHelper` instead.
*/ */
class AddonLoader implements ICanLoadAddons class AddonLoader implements ICanLoadAddons
{ {
const STATIC_PATH = 'static'; public const STATIC_PATH = 'static';
/** @var string */ /** @var string */
protected $basePath; protected $basePath;
/** @var IManageConfigValues */ /** @var IManageConfigValues */
@ -27,18 +27,18 @@ class AddonLoader implements ICanLoadAddons
public function __construct(string $basePath, IManageConfigValues $config) public function __construct(string $basePath, IManageConfigValues $config)
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use implementation of `Friendica\Core\Addon\AddonHelper` instead.', E_USER_DEPRECATED);
$this->basePath = $basePath; $this->basePath = $basePath;
$this->config = $config; $this->config = $config;
} }
/** /**
* @deprecated 2025.07 Use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead. * @deprecated 2026.01 Use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead.
*/ */
public function getActiveAddonConfig(string $configName): array public function getActiveAddonConfig(string $configName): array
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use `\Friendica\Core\Addon\AddonHelper::getAddonDependencyConfig()` instead.', E_USER_DEPRECATED);
$addons = array_keys(array_filter($this->config->get('addons') ?? [])); $addons = array_keys(array_filter($this->config->get('addons') ?? []));
$returnConfig = []; $returnConfig = [];
@ -63,14 +63,14 @@ class AddonLoader implements ICanLoadAddons
foreach ($config as $classname => $rule) { foreach ($config as $classname => $rule) {
if ($classname === LoggerInterface::class) { if ($classname === LoggerInterface::class) {
@trigger_error(sprintf( @trigger_error(sprintf(
'Providing a strategy for `%s` is deprecated since 2025.07 and will stop working in 5 months, please provide an implementation for `%s` via `dependency.config.php` and remove the `strategies.config.php` file in the `%s` addon.', 'Providing a strategy for `%s` is deprecated since 2026.01 and will stop working in 5 months, please provide an implementation for `%s` via `dependency.config.php` and remove the `strategies.config.php` file in the `%s` addon.',
$classname, $classname,
LoggerFactory::class, LoggerFactory::class,
$addonName, $addonName,
), \E_USER_DEPRECATED); ), \E_USER_DEPRECATED);
} else { } else {
@trigger_error(sprintf( @trigger_error(sprintf(
'Providing strategies for `%s` via addons is deprecated since 2025.07 and will stop working in 5 months, please stop using this and remove the `strategies.config.php` file in the `%s` addon.', 'Providing strategies for `%s` via addons is deprecated since 2026.01 and will stop working in 5 months, please stop using this and remove the `strategies.config.php` file in the `%s` addon.',
$classname, $classname,
$addonName, $addonName,
), \E_USER_DEPRECATED); ), \E_USER_DEPRECATED);

View file

@ -16,7 +16,7 @@ use Friendica\Core\Cache\Enum;
class ArrayCache extends AbstractCache implements ICanCacheInMemory class ArrayCache extends AbstractCache implements ICanCacheInMemory
{ {
use CompareDeleteTrait; use CompareDeleteTrait;
const NAME = 'array'; public const NAME = 'array';
/** @var array Array with the cached data */ /** @var array Array with the cached data */
protected $cachedData = []; protected $cachedData = [];
@ -34,10 +34,7 @@ class ArrayCache extends AbstractCache implements ICanCacheInMemory
*/ */
public function get(string $key) public function get(string $key)
{ {
if (isset($this->cachedData[$key])) { return $this->cachedData[$key] ?? null;
return $this->cachedData[$key];
}
return null;
} }
/** /**

View file

@ -25,21 +25,21 @@ class ConfigFileManager
* *
* @var string * @var string
*/ */
const CONFIG_HTCONFIG = 'htconfig'; public const CONFIG_HTCONFIG = 'htconfig';
/** /**
* The config file, where overrides per admin page/console are saved at * The config file, where overrides per admin page/console are saved at
* *
* @var string * @var string
*/ */
const CONFIG_DATA_FILE = 'node.config.php'; public const CONFIG_DATA_FILE = 'node.config.php';
/** /**
* The sample string inside the configs, which shouldn't get loaded * The sample string inside the configs, which shouldn't get loaded
* *
* @var string * @var string
*/ */
const SAMPLE_END = '-sample'; public const SAMPLE_END = '-sample';
/** /**
* @var string * @var string
@ -161,10 +161,10 @@ class ConfigFileManager
*/ */
public function loadAddonConfig(string $name): array public function loadAddonConfig(string $name): array
{ {
$filepath = $this->addonDir . DIRECTORY_SEPARATOR . // /var/www/html/addon/ $filepath = $this->addonDir . DIRECTORY_SEPARATOR // /var/www/html/addon/
$name . DIRECTORY_SEPARATOR . // openstreetmap/ . $name . DIRECTORY_SEPARATOR // openstreetmap/
'config' . DIRECTORY_SEPARATOR . // config/ . 'config' . DIRECTORY_SEPARATOR // config/
$name . ".config.php"; // openstreetmap.config.php . $name . ".config.php"; // openstreetmap.config.php
if (!file_exists($filepath)) { if (!file_exists($filepath)) {
return []; return [];
@ -182,8 +182,8 @@ class ConfigFileManager
*/ */
protected function loadEnvConfig(): array protected function loadEnvConfig(): array
{ {
$filepath = $this->staticDir . DIRECTORY_SEPARATOR . // /var/www/html/static/ $filepath = $this->staticDir . DIRECTORY_SEPARATOR // /var/www/html/static/
"env.config.php"; // env.config.php . "env.config.php"; // env.config.php
if (!file_exists($filepath)) { if (!file_exists($filepath)) {
return []; return [];
@ -220,9 +220,9 @@ class ConfigFileManager
$sampleEnd = self::SAMPLE_END . ($ini ? '.ini.php' : '.config.php'); $sampleEnd = self::SAMPLE_END . ($ini ? '.ini.php' : '.config.php');
foreach ($files as $filename) { foreach ($files as $filename) {
if (fnmatch($filePattern, $filename) && if (fnmatch($filePattern, $filename)
substr_compare($filename, $sampleEnd, -strlen($sampleEnd)) && && substr_compare($filename, $sampleEnd, -strlen($sampleEnd))
$filename !== self::CONFIG_DATA_FILE) { && $filename !== self::CONFIG_DATA_FILE) {
$found[] = $this->configDir . '/' . $filename; $found[] = $this->configDir . '/' . $filename;
} }
} }
@ -257,7 +257,6 @@ class ConfigFileManager
// map the legacy configuration structure to the current structure // map the legacy configuration structure to the current structure
foreach ($htConfigCategories as $htConfigCategory) { foreach ($htConfigCategories as $htConfigCategory) {
/** @phpstan-ignore-next-line $a->config could be modified after `include $fullName` */
if (is_array($a->config[$htConfigCategory])) { if (is_array($a->config[$htConfigCategory])) {
$keys = array_keys($a->config[$htConfigCategory]); $keys = array_keys($a->config[$htConfigCategory]);

View file

@ -24,9 +24,9 @@ class StrategiesFileManager
* The default hook-file-key of strategies * The default hook-file-key of strategies
* -> it's an empty string to cover empty/missing config values * -> it's an empty string to cover empty/missing config values
*/ */
const STRATEGY_DEFAULT_KEY = ''; public const STRATEGY_DEFAULT_KEY = '';
const STATIC_DIR = 'static'; public const STATIC_DIR = 'static';
const CONFIG_NAME = 'strategies'; public const CONFIG_NAME = 'strategies';
private IManageConfigValues $configuration; private IManageConfigValues $configuration;
protected array $config = []; protected array $config = [];
@ -84,7 +84,7 @@ class StrategiesFileManager
} }
/** /**
* @deprecated 2025.07 Providing strategies via addons is deprecated and will be removed in 5 months. * @deprecated 2026.01 Providing strategies via addons is deprecated and will be removed in 5 months.
*/ */
$this->config = array_merge_recursive($config, $this->getActiveAddonConfig()); $this->config = array_merge_recursive($config, $this->getActiveAddonConfig());
} }
@ -113,14 +113,14 @@ class StrategiesFileManager
foreach ($config as $classname => $rule) { foreach ($config as $classname => $rule) {
if ($classname === LoggerInterface::class) { if ($classname === LoggerInterface::class) {
@trigger_error(sprintf( @trigger_error(sprintf(
'Providing a strategy for `%s` is deprecated since 2025.07 and will stop working in 5 months, please provide an implementation for `%s` via `dependency.config.php` and remove the `strategies.config.php` file in the `%s` addon.', 'Providing a strategy for `%s` is deprecated since 2026.01 and will stop working in 5 months, please provide an implementation for `%s` via `dependency.config.php` and remove the `strategies.config.php` file in the `%s` addon.',
$classname, $classname,
LoggerFactory::class, LoggerFactory::class,
$addonName, $addonName,
), \E_USER_DEPRECATED); ), \E_USER_DEPRECATED);
} else { } else {
@trigger_error(sprintf( @trigger_error(sprintf(
'Providing strategies for `%s` via addons is deprecated since 2025.07 and will stop working in 5 months, please stop using this and remove the `strategies.config.php` file in the `%s` addon.', 'Providing strategies for `%s` via addons is deprecated since 2026.01 and will stop working in 5 months, please stop using this and remove the `strategies.config.php` file in the `%s` addon.',
$classname, $classname,
$addonName, $addonName,
), \E_USER_DEPRECATED); ), \E_USER_DEPRECATED);

View file

@ -7,10 +7,13 @@
namespace Friendica\Core; namespace Friendica\Core;
use DateTime;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Session\Capability\IHandleSessions; use Friendica\Core\Session\Capability\IHandleSessions;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use IntlDateFormatter;
use Locale;
/** /**
* Provide Language, Translation, and Localization functions to the application * Provide Language, Translation, and Localization functions to the application
@ -19,9 +22,9 @@ use Friendica\Util\Strings;
class L10n class L10n
{ {
/** @var string The default language */ /** @var string The default language */
const DEFAULT = 'en'; public const DEFAULT = 'en';
/** @var string[] The language names in their language */ /** @var string[] The language names in their language */
const LANG_NAMES = [ public const LANG_NAMES = [
'ar' => 'العربية', 'ar' => 'العربية',
'bg' => 'Български', 'bg' => 'Български',
'ca' => 'Català', 'ca' => 'Català',
@ -51,13 +54,13 @@ class L10n
'zh-cn' => '简体中文', 'zh-cn' => '简体中文',
]; ];
const LANG_PARENTS = [ public const LANG_PARENTS = [
'en-gb' => 'en', 'da-dk' => 'da', 'fi-fi' => 'fi', 'en-gb' => 'en', 'da-dk' => 'da', 'fi-fi' => 'fi',
'nb-no' => 'nb', 'pt-br' => 'pt', 'zh-cn' => 'zh' 'nb-no' => 'nb', 'pt-br' => 'pt', 'zh-cn' => 'zh',
]; ];
/** @var string Undetermined language */ /** @var string Undetermined language */
const UNDETERMINED_LANGUAGE = 'un'; public const UNDETERMINED_LANGUAGE = 'un';
/** /**
* A string indicating the current language used for translation: * A string indicating the current language used for translation:
@ -68,6 +71,8 @@ class L10n
*/ */
private $lang = ''; private $lang = '';
private string $locale = '';
/** /**
* An array of translation strings whose key is the neutral english message. * An array of translation strings whose key is the neutral english message.
* *
@ -83,13 +88,16 @@ class L10n
* @var IManageConfigValues * @var IManageConfigValues
*/ */
private $config; private $config;
private IHandleSessions $session;
public function __construct(IManageConfigValues $config, Database $dba, IHandleSessions $session, array $server, array $get) public function __construct(IManageConfigValues $config, Database $dba, IHandleSessions $session, array $server, array $get)
{ {
$this->dba = $dba; $this->dba = $dba;
$this->config = $config; $this->config = $config;
$this->session = $session;
$this->loadTranslationTable(L10n::detectLanguage($server, $get, $config->get('system', 'language', self::DEFAULT))); $this->loadTranslationTable(L10n::detectLanguage($server, $get, $config->get('system', 'language', self::DEFAULT)));
$this->setLocale($server);
$this->setSessionVariable($session); $this->setSessionVariable($session);
$this->setLangFromSession($session); $this->setLangFromSession($session);
} }
@ -104,6 +112,24 @@ class L10n
return $this->lang; return $this->lang;
} }
/**
* Set the instance locale based on the HTTP Accept-Language header.
*
* Reads the `HTTP_ACCEPT_LANGUAGE` value from the provided server array
* and stores the accepted locale string in `$this->locale` using
* `Locale::acceptFromHttp()`.
*
* @param array $server The $_SERVER-like array containing HTTP headers
* @return void
*/
private function setLocale(array $server)
{
if (!isset($server['HTTP_ACCEPT_LANGUAGE'])) {
return;
}
$this->locale = Locale::acceptFromHttp($server['HTTP_ACCEPT_LANGUAGE']);
}
/** /**
* Sets the language session variable * Sets the language session variable
*/ */
@ -111,6 +137,7 @@ class L10n
{ {
if ($session->get('authenticated') && !$session->get('language')) { if ($session->get('authenticated') && !$session->get('language')) {
$session->set('language', $this->lang); $session->set('language', $this->lang);
$session->set('locale', $this->locale);
// we haven't loaded user data yet, but we need user language // we haven't loaded user data yet, but we need user language
if ($session->get('uid')) { if ($session->get('uid')) {
$user = $this->dba->selectFirst('user', ['language'], ['uid' => $_SESSION['uid']]); $user = $this->dba->selectFirst('user', ['language'], ['uid' => $_SESSION['uid']]);
@ -219,7 +246,7 @@ class L10n
$res = preg_match( $res = preg_match(
'/^([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;\s*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i', '/^([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;\s*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i',
$acceptedLanguage, $acceptedLanguage,
$matches $matches,
); );
// Invalid language? -> skip // Invalid language? -> skip
@ -232,7 +259,7 @@ class L10n
// determine the quality of the guess // determine the quality of the guess
if (isset($matches[2])) { if (isset($matches[2])) {
$lang_quality = (float)$matches[2]; $lang_quality = (float) $matches[2];
} else { } else {
// fallback so without a quality parameter, it's probably the best // fallback so without a quality parameter, it's probably the best
$lang_quality = 1; $lang_quality = 1;
@ -429,7 +456,7 @@ class L10n
if (in_array('cld2', get_loaded_extensions())) { if (in_array('cld2', get_loaded_extensions())) {
$additional_langs = array_merge( $additional_langs = array_merge(
$additional_langs, $additional_langs,
['dv', 'kn', 'lo', 'ml', 'or', 'pa', 'sd', 'si', 'te', 'yi'] ['dv', 'kn', 'lo', 'ml', 'or', 'pa', 'sd', 'si', 'te', 'yi'],
); );
} }
@ -469,6 +496,17 @@ class L10n
return $languages; return $languages;
} }
/**
* Converts e.g. en-gb to en_GB and da-dk to da_DK, which is the format some other systems expect
*
* @param string $lang
* @return string
* */
public function langToLocaleCode($lang)
{
return preg_replace_callback("/([a-z]+)-([a-z]+)/", fn ($m) => $m[1] . "_" . strtoupper($m[2]), $lang);
}
/** /**
* Convert the language code to ISO639-1 * Convert the language code to ISO639-1
* It also converts old codes to their new counterparts. * It also converts old codes to their new counterparts.
@ -499,52 +537,6 @@ class L10n
return $code; return $code;
} }
/**
* Translate days and months names.
*
* @param string $s String with day or month name.
* @return string Translated string.
*/
public function getDay(string $s): string
{
$ret = str_replace(
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
[$this->t('Monday'), $this->t('Tuesday'), $this->t('Wednesday'), $this->t('Thursday'), $this->t('Friday'), $this->t('Saturday'), $this->t('Sunday')],
$s
);
$ret = str_replace(
['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
[$this->t('January'), $this->t('February'), $this->t('March'), $this->t('April'), $this->t('May'), $this->t('June'), $this->t('July'), $this->t('August'), $this->t('September'), $this->t('October'), $this->t('November'), $this->t('December')],
$ret
);
return $ret;
}
/**
* Translate short days and months names.
*
* @param string $s String with short day or month name.
* @return string Translated string.
*/
public function getDayShort(string $s): string
{
$ret = str_replace(
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
[$this->t('Mon'), $this->t('Tue'), $this->t('Wed'), $this->t('Thu'), $this->t('Fri'), $this->t('Sat'), $this->t('Sun')],
$s
);
$ret = str_replace(
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
[$this->t('Jan'), $this->t('Feb'), $this->t('Mar'), $this->t('Apr'), $this->t('May'), $this->t('Jun'), $this->t('Jul'), $this->t('Aug'), $this->t('Sep'), $this->t('Oct'), $this->t('Nov'), $this->t('Dec')],
$ret
);
return $ret;
}
/** /**
* Creates a new L10n instance based on the given langauge * Creates a new L10n instance based on the given langauge
* *
@ -564,4 +556,126 @@ class L10n
$newL10n->loadTranslationTable($lang); $newL10n->loadTranslationTable($lang);
return $newL10n; return $newL10n;
} }
/**
* Format a date/time string using RELATIVE_FULL date and SHORT time.
*
* This will produce relative strings where supported by the ICU implementation
* (for example "yesterday", "in 2 days") according to the current locale.
*
* @param string $datestring Date/time string (e.g. ISO 8601)
* @return string Formatted relative date/time string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function relativeDateTime(string $datestring): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::RELATIVE_FULL, IntlDateFormatter::SHORT);
}
/**
* Format a date/time string using FULL date and SHORT time according to current locale.
*
* @param string $datestring Date/time string (e.g. ISO 8601)
* @return string Formatted date/time string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function fullDateTime(string $datestring): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::FULL, IntlDateFormatter::SHORT);
}
/**
* Format a date/time string using MEDIUM date and SHORT time according to current locale.
*
* @param string $datestring Date/time string
* @return string Formatted date/time string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function dateTime(string $datestring): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
}
/**
* Format a date string (date only) using FULL date format according to current locale.
*
* @param string $datestring Date string
* @return string Formatted date string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function fullDate(string $datestring): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::FULL, IntlDateFormatter::NONE);
}
/**
* Format a date string (date only) using MEDIUM date format according to current locale.
*
* @param string $datestring Date string
* @return string Formatted date string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function mediumDate(string $datestring): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
}
/**
* Format a date string (date only) using LONG date format according to current locale.
*
* @param string $datestring Date string
* @return string Formatted date string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function longDate(string $datestring): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::LONG, IntlDateFormatter::NONE);
}
/**
* Format a date/time string using a custom ICU pattern.
* @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
*
* The provided pattern is passed directly to the underlying
* `IntlDateFormatter` instance. This allows callers to specify
* arbitrary formatting rules beyond the standard date/time styles.
*
* @param string $datestring Date/time string (e.g. ISO 8601)
* @param string $pattern ICU date/time pattern
* @return string Formatted date/time string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function formatDateTimeByPattern(string $datestring, string $pattern): string
{
return $this->formatDateTime($datestring, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $pattern);
}
/**
* General date/time formatting helper.
*
* Creates an IntlDateFormatter using the instance locale and the timezone
* stored in session (if any) and formats the provided date/time string.
*
* @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax for details on the supported pattern syntax when using the $pattern parameter.
*
* @param string $datestring Date/time string (e.g. ISO 8601)
* @param int $dateType One of IntlDateFormatter::SHORT|MEDIUM|LONG|FULL|NONE
* @param int $timeType One of IntlDateFormatter::SHORT|MEDIUM|LONG|FULL|NONE
* @param string $pattern Optional ICU date/time pattern to use instead of the standard styles
* @return string Formatted date/time string
* @throws \Exception If the date string cannot be parsed into a DateTime
*/
public function formatDateTime(string $datestring, int $dateType, int $timeType, ?string $pattern = null): string
{
$formatter = new IntlDateFormatter(
$this->session->get('language') ?? $this->locale ?: $this->config->get('system', 'language', 'en_US'),
$dateType,
$timeType,
$this->session->get('timezone') ?? null,
null,
$pattern,
);
return $formatter->format(new DateTime($datestring));
}
} }

View file

@ -19,7 +19,7 @@ class CacheLock extends AbstractLock
* *
* @var string * @var string
*/ */
const CACHE_PREFIX = 'lock:'; public const CACHE_PREFIX = 'lock:';
/** /**
* @var ICanCacheInMemory * @var ICanCacheInMemory
@ -50,7 +50,7 @@ class CacheLock extends AbstractLock
do { do {
$lock = $this->cache->get($lockKey); $lock = $this->cache->get($lockKey);
// When we do want to lock something that was already locked by us. // When we do want to lock something that was already locked by us.
if ((int)$lock == getmypid()) { if ((int) $lock == getmypid()) {
$got_lock = true; $got_lock = true;
} }
@ -66,7 +66,7 @@ class CacheLock extends AbstractLock
} }
if (!$got_lock && ($timeout > 0)) { if (!$got_lock && ($timeout > 0)) {
usleep(rand(10000, 200000)); usleep(random_int(10000, 200000));
} }
} while (!$got_lock && ((time() - $start) < $timeout)); } while (!$got_lock && ((time() - $start) < $timeout));
} catch (CachePersistenceException $exception) { } catch (CachePersistenceException $exception) {

View file

@ -51,7 +51,7 @@ class DatabaseLock extends AbstractLock
do { do {
$this->dba->lock('locks'); $this->dba->lock('locks');
$lock = $this->dba->selectFirst('locks', ['locked', 'pid'], [ $lock = $this->dba->selectFirst('locks', ['locked', 'pid'], [
'`name` = ? AND `expires` >= ?', $key,DateTimeFormat::utcNow() '`name` = ? AND `expires` >= ?', $key,DateTimeFormat::utcNow(),
]); ]);
if ($this->dba->isResult($lock)) { if ($this->dba->isResult($lock)) {
@ -65,7 +65,7 @@ class DatabaseLock extends AbstractLock
$this->dba->update('locks', [ $this->dba->update('locks', [
'locked' => true, 'locked' => true,
'pid' => $this->pid, 'pid' => $this->pid,
'expires' => DateTimeFormat::utc('now + ' . $ttl . 'seconds') 'expires' => DateTimeFormat::utc('now + ' . $ttl . 'seconds'),
], ['name' => $key]); ], ['name' => $key]);
$got_lock = true; $got_lock = true;
} }
@ -82,7 +82,7 @@ class DatabaseLock extends AbstractLock
$this->dba->unlock(); $this->dba->unlock();
if (!$got_lock && ($timeout > 0)) { if (!$got_lock && ($timeout > 0)) {
usleep(rand(100000, 2000000)); usleep(random_int(100000, 2000000));
} }
} while (!$got_lock && ((time() - $start) < $timeout)); } while (!$got_lock && ((time() - $start) < $timeout));
} catch (\Exception $exception) { } catch (\Exception $exception) {

View file

@ -13,7 +13,7 @@ use Psr\Log\LoggerInterface;
/** /**
* Logger functions * Logger functions
* *
* @deprecated 2025.07 Use constructor injection or `DI::logger()` instead * @deprecated 2026.01 Use constructor injection or `DI::logger()` instead
*/ */
class Logger class Logger
{ {
@ -34,7 +34,7 @@ class Logger
*/ */
public static function emergency(string $message, array $context = []) public static function emergency(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->emergency($message, $context); self::getInstance()->emergency($message, $context);
} }
@ -53,7 +53,7 @@ class Logger
*/ */
public static function alert(string $message, array $context = []) public static function alert(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->alert($message, $context); self::getInstance()->alert($message, $context);
} }
@ -71,7 +71,7 @@ class Logger
*/ */
public static function critical(string $message, array $context = []) public static function critical(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->critical($message, $context); self::getInstance()->critical($message, $context);
} }
@ -88,7 +88,7 @@ class Logger
*/ */
public static function error(string $message, array $context = []) public static function error(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->error($message, $context); self::getInstance()->error($message, $context);
} }
@ -107,7 +107,7 @@ class Logger
*/ */
public static function warning(string $message, array $context = []) public static function warning(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->warning($message, $context); self::getInstance()->warning($message, $context);
} }
@ -123,7 +123,7 @@ class Logger
*/ */
public static function notice(string $message, array $context = []) public static function notice(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->notice($message, $context); self::getInstance()->notice($message, $context);
} }
@ -142,7 +142,7 @@ class Logger
*/ */
public static function info(string $message, array $context = []) public static function info(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->info($message, $context); self::getInstance()->info($message, $context);
} }
@ -158,7 +158,7 @@ class Logger
*/ */
public static function debug(string $message, array $context = []) public static function debug(string $message, array $context = [])
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, use constructor injection or `DI::logger()` instead.', E_USER_DEPRECATED);
self::getInstance()->debug($message, $context); self::getInstance()->debug($message, $context);
} }

View file

@ -13,7 +13,7 @@ use Psr\Log\LogLevel;
/** /**
* Abstract class for creating logger types, which includes common necessary logic/content * Abstract class for creating logger types, which includes common necessary logic/content
* *
* @deprecated 2025.07 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead * @deprecated 2026.01 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead
*/ */
abstract class AbstractLoggerTypeFactory abstract class AbstractLoggerTypeFactory
{ {
@ -27,7 +27,7 @@ abstract class AbstractLoggerTypeFactory
*/ */
public function __construct(IHaveCallIntrospections $introspection, string $channel) public function __construct(IHaveCallIntrospections $introspection, string $channel)
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED);
$this->channel = $channel; $this->channel = $channel;
$this->introspection = $introspection; $this->introspection = $introspection;

View file

@ -48,10 +48,10 @@ final class DelegatingLoggerFactory implements LoggerFactory
$factoryName = $this->config->get('system', 'logger_config') ?? ''; $factoryName = $this->config->get('system', 'logger_config') ?? '';
/** /**
* @deprecated 2025.07 The value `monolog` for `system.logger_config` inside the `config/local.config.php` file is deprecated, please use `stream` or `syslog` instead. * @deprecated 2026.01 The value `monolog` for `system.logger_config` inside the `config/local.config.php` file is deprecated, please use `stream` or `syslog` instead.
*/ */
if ($factoryName === 'monolog') { if ($factoryName === 'monolog') {
@trigger_error('The config `system.logger_config` with value `monolog` is deprecated since 2025.07 and will stop working in 5 months, please change the value to `stream` or `syslog` in the `config/local.config.php` file.', \E_USER_DEPRECATED); @trigger_error('The config `system.logger_config` with value `monolog` is deprecated since 2026.01 and will stop working in 5 months, please change the value to `stream` or `syslog` in the `config/local.config.php` file.', \E_USER_DEPRECATED);
$factoryName = 'stream'; $factoryName = 'stream';
} }

View file

@ -19,7 +19,7 @@ use Throwable;
/** /**
* The logger factory for the core logging instances * The logger factory for the core logging instances
* *
* @deprecated 2025.07 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead * @deprecated 2026.01 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead
*/ */
class Logger class Logger
{ {
@ -28,7 +28,7 @@ class Logger
public function __construct(string $channel = LogChannel::DEFAULT) public function __construct(string $channel = LogChannel::DEFAULT)
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED);
$this->channel = $channel; $this->channel = $channel;
} }

View file

@ -20,7 +20,7 @@ use Psr\Log\NullLogger;
/** /**
* The logger factory for the StreamLogger instance * The logger factory for the StreamLogger instance
* *
* @deprecated 2025.07 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead * @deprecated 2026.01 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead
* @see StreamLoggerFactory * @see StreamLoggerFactory
* @see StreamLoggerClass * @see StreamLoggerClass
*/ */
@ -40,11 +40,11 @@ class StreamLogger extends AbstractLoggerTypeFactory
*/ */
public function create(IManageConfigValues $config, string $logfile = null, string $channel = null): LoggerInterface public function create(IManageConfigValues $config, string $logfile = null, string $channel = null): LoggerInterface
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED);
$fileSystem = new FileSystem(); $fileSystem = new FileSystem();
$logfile = $logfile ?? $config->get('system', 'logfile'); $logfile ??= $config->get('system', 'logfile');
if (!@file_exists($logfile) || !@is_writable($logfile)) { if (!@file_exists($logfile) || !@is_writable($logfile)) {
throw new LoggerArgumentException(sprintf('%s is not a valid logfile', $logfile)); throw new LoggerArgumentException(sprintf('%s is not a valid logfile', $logfile));
} }
@ -80,8 +80,8 @@ class StreamLogger extends AbstractLoggerTypeFactory
$logfile = $config->get('system', 'dlogfile'); $logfile = $config->get('system', 'dlogfile');
$developerIp = $config->get('system', 'dlogip'); $developerIp = $config->get('system', 'dlogip');
if ((!isset($developerIp) || !$debugging) && if ((!isset($developerIp) || !$debugging)
(!is_file($logfile) || is_writable($logfile))) { && (!is_file($logfile) || is_writable($logfile))) {
return new NullLogger(); return new NullLogger();
} }

View file

@ -16,7 +16,7 @@ use Psr\Log\LoggerInterface;
/** /**
* The logger factory for the SyslogLogger instance * The logger factory for the SyslogLogger instance
* *
* @deprecated 2025.07 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead * @deprecated 2026.01 Implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead
* @see SyslogLoggerFactory * @see SyslogLoggerFactory
* @see SyslogLoggerClass * @see SyslogLoggerClass
*/ */
@ -33,7 +33,7 @@ class SyslogLogger extends AbstractLoggerTypeFactory
*/ */
public function create(IManageConfigValues $config): LoggerInterface public function create(IManageConfigValues $config): LoggerInterface
{ {
@trigger_error('Class `' . __CLASS__ . '` is deprecated since 2025.07 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED); @trigger_error('Class `' . self::class . '` is deprecated since 2026.01 and will be removed after 5 months, implement `\Friendica\Core\Logger\Factory\LoggerFactory` instead.', E_USER_DEPRECATED);
$logOpts = $config->get('system', 'syslog_flags') ?? SyslogLoggerClass::DEFAULT_FLAGS; $logOpts = $config->get('system', 'syslog_flags') ?? SyslogLoggerClass::DEFAULT_FLAGS;
$logFacility = $config->get('system', 'syslog_facility') ?? SyslogLoggerClass::DEFAULT_FACILITY; $logFacility = $config->get('system', 'syslog_facility') ?? SyslogLoggerClass::DEFAULT_FACILITY;

View file

@ -160,7 +160,7 @@ class ErrorHandler
register_shutdown_function([$this, 'handleFatalError']); register_shutdown_function([$this, 'handleFatalError']);
$this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
$this->fatalLevel = null === $level ? LogLevel::ALERT : $level; $this->fatalLevel = $level ?? LogLevel::ALERT;
$this->hasFatalErrorHandler = true; $this->hasFatalErrorHandler = true;
return $this; return $this;
@ -219,7 +219,7 @@ class ErrorHandler
$this->logger->log( $this->logger->log(
$level, $level,
sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
['exception' => $e] ['exception' => $e],
); );
if ($this->previousExceptionHandler) { if ($this->previousExceptionHandler) {
@ -262,14 +262,14 @@ class ErrorHandler
$message .= sprintf( $message .= sprintf(
' It was called in `%s`%s.', ' It was called in `%s`%s.',
$calledPlace['file'], $calledPlace['file'],
isset($calledPlace['line']) ? ' in line ' . $calledPlace['line'] : '' isset($calledPlace['line']) ? ' in line ' . $calledPlace['line'] : '',
); );
} }
// fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) { if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
$level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL; $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
$this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]); $this->logger->log($level, self::codeToString($code) . ': ' . $message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]);
} else { } else {
$this->lastFatalTrace = $trace; $this->lastFatalTrace = $trace;
} }
@ -294,8 +294,8 @@ class ErrorHandler
if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) { if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
$this->logger->log( $this->logger->log(
$this->fatalLevel, $this->fatalLevel,
'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], 'Fatal Error (' . self::codeToString($lastError['type']) . '): ' . $lastError['message'],
['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace] ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace],
); );
} }
} }

View file

@ -73,7 +73,7 @@ class Introspection implements IHaveCallIntrospections
'line' => $trace[$i - 1]['line'] ?? null, 'line' => $trace[$i - 1]['line'] ?? null,
'function' => $trace[$i]['function'] ?? null, 'function' => $trace[$i]['function'] ?? null,
'request-id' => $this->requestId, 'request-id' => $this->requestId,
'stack' => System::callstack(15, 1, ['Friendica\Core\Logger\Type\StreamLogger', 'Friendica\Core\Logger\Type\AbstractLogger', 'Friendica\Core\Logger\Type\WorkerLogger', 'Friendica\Core\Logger']), 'stack' => System::callstack(15, 1, [\Friendica\Core\Logger\Type\StreamLogger::class, \Friendica\Core\Logger\Type\AbstractLogger::class, \Friendica\Core\Logger\Type\WorkerLogger::class, \Friendica\Core\Logger::class]),
]; ];
} }

View file

@ -19,7 +19,7 @@ use Friendica\Core\PConfig\ValueObject;
*/ */
class JitPConfig extends AbstractPConfigValues class JitPConfig extends AbstractPConfigValues
{ {
const NAME = 'jit'; public const NAME = 'jit';
/** /**
* @var array Array of already loaded db values (even if there was no value) * @var array Array of already loaded db values (even if there was no value)
@ -71,8 +71,8 @@ class JitPConfig extends AbstractPConfigValues
} }
// if the value isn't loaded or refresh is needed, load it to the cache // if the value isn't loaded or refresh is needed, load it to the cache
if ($this->configModel->isConnected() && if ($this->configModel->isConnected()
(empty($this->db_loaded[$uid][$cat][$key]) || $refresh)) { && (empty($this->db_loaded[$uid][$cat][$key]) || $refresh)) {
$dbValue = $this->configModel->get($uid, $cat, $key); $dbValue = $this->configModel->get($uid, $cat, $key);
if (isset($dbValue)) { if (isset($dbValue)) {
@ -86,7 +86,7 @@ class JitPConfig extends AbstractPConfigValues
// use the config cache for return // use the config cache for return
$result = $this->configCache->get($uid, $cat, $key); $result = $this->configCache->get($uid, $cat, $key);
return (isset($result)) ? $result : $default_value; return $result ?? $default_value;
} }
/** /**

View file

@ -18,7 +18,7 @@ use Friendica\Core\PConfig\ValueObject;
*/ */
class PreloadPConfig extends AbstractPConfigValues class PreloadPConfig extends AbstractPConfigValues
{ {
const NAME = 'preload'; public const NAME = 'preload';
/** @var array */ /** @var array */
private $config_loaded; private $config_loaded;
@ -83,7 +83,7 @@ class PreloadPConfig extends AbstractPConfigValues
// use the config cache for return // use the config cache for return
$result = $this->configCache->get($uid, $cat, $key); $result = $this->configCache->get($uid, $cat, $key);
return (isset($result)) ? $result : $default_value; return $result ?? $default_value;
} }
/** /**

View file

@ -22,43 +22,43 @@ use Friendica\Protocol\Diaspora;
class Protocol class Protocol
{ {
// Native support // Native support
const ACTIVITYPUB = 'apub'; // ActivityPub (Pleroma, Mastodon, Osada, ...) public const ACTIVITYPUB = 'apub'; // ActivityPub (Pleroma, Mastodon, Osada, ...)
const DFRN = 'dfrn'; // Friendica, Mistpark, other DFRN implementations public const DFRN = 'dfrn'; // Friendica, Mistpark, other DFRN implementations
const DIASPORA = 'dspr'; // Diaspora, Hubzilla, Socialhome, Ganggo public const DIASPORA = 'dspr'; // Diaspora, Hubzilla, Socialhome, Ganggo
const FEED = 'feed'; // RSS/Atom feeds with no known "post/notify" protocol public const FEED = 'feed'; // RSS/Atom feeds with no known "post/notify" protocol
const MAIL = 'mail'; // IMAP/POP public const MAIL = 'mail'; // IMAP/POP
const NATIVE_SUPPORT = [self::DFRN, self::DIASPORA, self::FEED, self::MAIL, self::ACTIVITYPUB]; public const NATIVE_SUPPORT = [self::DFRN, self::DIASPORA, self::FEED, self::MAIL, self::ACTIVITYPUB];
const FEDERATED = [self::DFRN, self::DIASPORA, self::ACTIVITYPUB]; public const FEDERATED = [self::DFRN, self::DIASPORA, self::ACTIVITYPUB];
const SUPPORT_PRIVATE = [self::DFRN, self::DIASPORA, self::MAIL, self::ACTIVITYPUB, self::PUMPIO]; public const SUPPORT_PRIVATE = [self::DFRN, self::DIASPORA, self::MAIL, self::ACTIVITYPUB, self::PUMPIO];
// Supported through a connector // Supported through a connector
const BLUESKY = 'bsky'; // Bluesky public const BLUESKY = 'bsky'; // Bluesky
const DIASPORA2 = 'dspc'; // Diaspora connector public const DIASPORA2 = 'dspc'; // Diaspora connector
const DISCOURSE = 'dscs'; // Discourse public const DISCOURSE = 'dscs'; // Discourse
const PNUT = 'pnut'; // pnut.io public const PNUT = 'pnut'; // pnut.io
const PUMPIO = 'pump'; // pump.io public const PUMPIO = 'pump'; // pump.io
const TUMBLR = 'tmbl'; // Tumblr public const TUMBLR = 'tmbl'; // Tumblr
const TWITTER = 'twit'; // Twitter public const TWITTER = 'twit'; // Twitter
// Dead protocols // Dead protocols
const APPNET = 'apdn'; // app.net - Dead protocol public const APPNET = 'apdn'; // app.net - Dead protocol
const FACEBOOK = 'face'; // Facebook API - Not working anymore, API is closed public const FACEBOOK = 'face'; // Facebook API - Not working anymore, API is closed
const GPLUS = 'goog'; // Google+ - Dead in 2019 public const GPLUS = 'goog'; // Google+ - Dead in 2019
const OSTATUS = 'stat'; // GNU Social and other OStatus implementations public const OSTATUS = 'stat'; // GNU Social and other OStatus implementations
const STATUSNET = 'stac'; // Statusnet connector public const STATUSNET = 'stac'; // Statusnet connector
// Currently unsupported // Currently unsupported
const ICALENDAR = 'ical'; // iCalendar public const ICALENDAR = 'ical'; // iCalendar
const LINKEDIN = 'lnkd'; // LinkedIn public const LINKEDIN = 'lnkd'; // LinkedIn
const MYSPACE = 'mysp'; // MySpace public const MYSPACE = 'mysp'; // MySpace
const NEWS = 'nntp'; // Network News Transfer Protocol public const NEWS = 'nntp'; // Network News Transfer Protocol
const XMPP = 'xmpp'; // XMPP public const XMPP = 'xmpp'; // XMPP
const ZOT = 'zot!'; // Zot! public const ZOT = 'zot!'; // Zot!
const PHANTOM = 'unkn'; // Place holder public const PHANTOM = 'unkn'; // Place holder
/** /**
* Returns whether the provided protocol supports following * Returns whether the provided protocol supports following
@ -75,7 +75,7 @@ class Protocol
$hook_data = [ $hook_data = [
'protocol' => $protocol, 'protocol' => $protocol,
'result' => null 'result' => null,
]; ];
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
@ -102,7 +102,7 @@ class Protocol
$hook_data = [ $hook_data = [
'protocol' => $protocol, 'protocol' => $protocol,
'result' => null 'result' => null,
]; ];
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
@ -131,7 +131,7 @@ class Protocol
return true; return true;
} }
$protocol = $protocol ?? $contact['protocol']; $protocol ??= $contact['protocol'];
if ($protocol == self::DIASPORA) { if ($protocol == self::DIASPORA) {
$contact = Diaspora::sendShare($owner, $contact); $contact = Diaspora::sendShare($owner, $contact);
@ -340,7 +340,7 @@ class Protocol
$hook_data = [ $hook_data = [
'protocol' => $protocol, 'protocol' => $protocol,
'result' => null 'result' => null,
]; ];
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();

View file

@ -34,11 +34,11 @@ use Psr\Log\LoggerInterface;
class StorageManager class StorageManager
{ {
// Default tables to look for data // Default tables to look for data
const TABLES = ['photo', 'attach']; public const TABLES = ['photo', 'attach'];
// Default storage backends // Default storage backends
/** @var string[] */ /** @var string[] */
const DEFAULT_BACKENDS = [ public const DEFAULT_BACKENDS = [
Type\Filesystem::NAME, Type\Filesystem::NAME,
Type\Database::NAME, Type\Database::NAME,
]; ];
@ -240,12 +240,12 @@ class StorageManager
*/ */
public function isValidBackend(string $name = null, array $validBackends = null): bool public function isValidBackend(string $name = null, array $validBackends = null): bool
{ {
$validBackends = $validBackends ?? array_merge( $validBackends ??= array_merge(
$this->validBackends, $this->validBackends,
[ [
Type\SystemResource::getName(), Type\SystemResource::getName(),
Type\ExternalResource::getName(), Type\ExternalResource::getName(),
] ],
); );
return in_array($name, $validBackends); return in_array($name, $validBackends);
} }
@ -373,7 +373,7 @@ class StorageManager
$table, $table,
['id', 'data', 'backend-class', 'backend-ref'], ['id', 'data', 'backend-class', 'backend-ref'],
['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()], ['`backend-class` IS NULL or `backend-class` != ?', $destination::getName()],
['limit' => $limit] ['limit' => $limit],
); );
while ($resource = $this->dba->fetch($resources)) { while ($resource = $this->dba->fetch($resources)) {

View file

@ -83,8 +83,8 @@ class Theme
foreach ($comment_lines as $comment_line) { foreach ($comment_lines as $comment_line) {
$comment_line = trim($comment_line, "\t\n\r */"); $comment_line = trim($comment_line, "\t\n\r */");
if (strpos($comment_line, ':') !== false) { if (strpos($comment_line, ':') !== false) {
list($key, $value) = array_map("trim", explode(":", $comment_line, 2)); [$key, $value] = array_map("trim", explode(":", $comment_line, 2));
$key = strtolower($key); $key = strtolower($key);
if ($key == "author") { if ($key == "author") {
$result = preg_match("|([^<]+)<([^>]+)>|", $value, $matches); $result = preg_match("|([^<]+)<([^>]+)>|", $value, $matches);
if ($result) { if ($result) {

View file

@ -27,25 +27,25 @@ class Worker
* Process priority for the worker * Process priority for the worker
* @{ * @{
*/ */
const PRIORITY_UNDEFINED = 0; public const PRIORITY_UNDEFINED = 0;
const PRIORITY_CRITICAL = 10; public const PRIORITY_CRITICAL = 10;
const PRIORITY_HIGH = 20; public const PRIORITY_HIGH = 20;
const PRIORITY_MEDIUM = 30; public const PRIORITY_MEDIUM = 30;
const PRIORITY_LOW = 40; public const PRIORITY_LOW = 40;
const PRIORITY_NEGLIGIBLE = 50; public const PRIORITY_NEGLIGIBLE = 50;
const PRIORITIES = [self::PRIORITY_CRITICAL, self::PRIORITY_HIGH, self::PRIORITY_MEDIUM, self::PRIORITY_LOW, self::PRIORITY_NEGLIGIBLE]; public const PRIORITIES = [self::PRIORITY_CRITICAL, self::PRIORITY_HIGH, self::PRIORITY_MEDIUM, self::PRIORITY_LOW, self::PRIORITY_NEGLIGIBLE];
/* @}*/ /* @}*/
const STATE_STARTUP = 1; // Worker is in startup. This takes most time. public const STATE_STARTUP = 1; // Worker is in startup. This takes most time.
const STATE_LONG_LOOP = 2; // Worker is processing the whole - long - loop. public const STATE_LONG_LOOP = 2; // Worker is processing the whole - long - loop.
const STATE_REFETCH = 3; // Worker had refetched jobs in the execution loop. public const STATE_REFETCH = 3; // Worker had refetched jobs in the execution loop.
const STATE_SHORT_LOOP = 4; // Worker is processing preassigned jobs, thus saving much time. public const STATE_SHORT_LOOP = 4; // Worker is processing preassigned jobs, thus saving much time.
const FAST_COMMANDS = ['APDelivery', 'Delivery']; public const FAST_COMMANDS = ['APDelivery', 'Delivery'];
const LOCK_PROCESS = 'worker_process'; public const LOCK_PROCESS = 'worker_process';
const LOCK_WORKER = 'worker'; public const LOCK_WORKER = 'worker';
const LAST_CHECK = 'worker::check'; public const LAST_CHECK = 'worker::check';
private static $up_start; private static $up_start;
private static $db_duration = 0; private static $db_duration = 0;
@ -82,7 +82,7 @@ class Worker
// Kill stale processes every 5 minutes // Kill stale processes every 5 minutes
$last_cleanup = DI::keyValue()->get('worker_last_cleaned') ?? 0; $last_cleanup = DI::keyValue()->get('worker_last_cleaned') ?? 0;
if (time() > ($last_cleanup + 300)) { if (time() > ($last_cleanup + 300)) {
DI::keyValue()->set( 'worker_last_cleaned', time()); DI::keyValue()->set('worker_last_cleaned', time());
Worker\Cron::killStaleWorkers(); Worker\Cron::killStaleWorkers();
} }
@ -212,7 +212,7 @@ class Worker
*/ */
public static function entriesExists(): bool public static function entriesExists(): bool
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$exists = DBA::exists('workerqueue', ["NOT `done` AND `pid` = 0 AND `next_try` < ?", DateTimeFormat::utcNow()]); $exists = DBA::exists('workerqueue', ["NOT `done` AND `pid` = 0 AND `next_try` < ?", DateTimeFormat::utcNow()]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
return $exists; return $exists;
@ -226,7 +226,7 @@ class Worker
*/ */
public static function deferredEntries(): int public static function deferredEntries(): int
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$count = DBA::count('workerqueue', ["NOT `done` AND `pid` = 0 AND `retrial` > ?", 0]); $count = DBA::count('workerqueue', ["NOT `done` AND `pid` = 0 AND `retrial` > ?", 0]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_count += (microtime(true) - $stamp); self::$db_duration_count += (microtime(true) - $stamp);
@ -241,7 +241,7 @@ class Worker
*/ */
public static function totalEntries(): int public static function totalEntries(): int
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$count = DBA::count('workerqueue', ['done' => false, 'pid' => 0]); $count = DBA::count('workerqueue', ['done' => false, 'pid' => 0]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_count += (microtime(true) - $stamp); self::$db_duration_count += (microtime(true) - $stamp);
@ -256,7 +256,7 @@ class Worker
*/ */
private static function highestPriority(): int private static function highestPriority(): int
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$condition = ["`pid` = 0 AND NOT `done` AND `next_try` < ?", DateTimeFormat::utcNow()]; $condition = ["`pid` = 0 AND NOT `done` AND `next_try` < ?", DateTimeFormat::utcNow()];
$workerqueue = DBA::selectFirst('workerqueue', ['priority'], $condition, ['order' => ['priority']]); $workerqueue = DBA::selectFirst('workerqueue', ['priority'], $condition, ['order' => ['priority']]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
@ -294,7 +294,7 @@ class Worker
$file = realpath($file); $file = realpath($file);
if (strpos($file, getcwd()) !== 0) { if (strpos($file, (string) getcwd()) !== 0) {
return false; return false;
} }
@ -367,7 +367,7 @@ class Worker
self::$last_update = time(); self::$last_update = time();
if ($age > 1) { if ($age > 1) {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
DBA::update('workerqueue', ['executed' => DateTimeFormat::utcNow()], ['pid' => $mypid, 'done' => false]); DBA::update('workerqueue', ['executed' => DateTimeFormat::utcNow()], ['pid' => $mypid, 'done' => false]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
@ -377,7 +377,7 @@ class Worker
self::execFunction($queue, $include, $argv, true); self::execFunction($queue, $include, $argv, true);
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$condition = ["`id` = ? AND `next_try` < ?", $queue['id'], DateTimeFormat::utcNow()]; $condition = ["`id` = ? AND `next_try` < ?", $queue['id'], DateTimeFormat::utcNow()];
if (DBA::update('workerqueue', ['done' => true], $condition)) { if (DBA::update('workerqueue', ['done' => true], $condition)) {
DI::keyValue()->set('last_worker_execution', DateTimeFormat::utcNow()); DI::keyValue()->set('last_worker_execution', DateTimeFormat::utcNow());
@ -390,7 +390,7 @@ class Worker
if (!self::validateInclude($include)) { if (!self::validateInclude($include)) {
DI::logger()->warning('Include file is not valid', ['file' => $argv[0]]); DI::logger()->warning('Include file is not valid', ['file' => $argv[0]]);
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
DBA::delete('workerqueue', ['id' => $queue['id']]); DBA::delete('workerqueue', ['id' => $queue['id']]);
self::$db_duration = (microtime(true) - $stamp); self::$db_duration = (microtime(true) - $stamp);
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
@ -399,7 +399,7 @@ class Worker
require_once $include; require_once $include;
$funcname = str_replace('.php', '', basename($argv[0])) .'_run'; $funcname = str_replace('.php', '', basename($argv[0])) . '_run';
if (function_exists($funcname)) { if (function_exists($funcname)) {
// We constantly update the "executed" date every minute to avoid being killed too soon // We constantly update the "executed" date every minute to avoid being killed too soon
@ -411,7 +411,7 @@ class Worker
self::$last_update = time(); self::$last_update = time();
if ($age > 1) { if ($age > 1) {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
DBA::update('workerqueue', ['executed' => DateTimeFormat::utcNow()], ['pid' => $mypid, 'done' => false]); DBA::update('workerqueue', ['executed' => DateTimeFormat::utcNow()], ['pid' => $mypid, 'done' => false]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
@ -419,7 +419,7 @@ class Worker
self::execFunction($queue, $funcname, $argv, false); self::execFunction($queue, $funcname, $argv, false);
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
if (DBA::update('workerqueue', ['done' => true], ['id' => $queue['id']])) { if (DBA::update('workerqueue', ['done' => true], ['id' => $queue['id']])) {
DI::keyValue()->set('last_worker_execution', DateTimeFormat::utcNow()); DI::keyValue()->set('last_worker_execution', DateTimeFormat::utcNow());
} }
@ -427,7 +427,7 @@ class Worker
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
} else { } else {
DI::logger()->warning('Function does not exist', ['function' => $funcname]); DI::logger()->warning('Function does not exist', ['function' => $funcname]);
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
DBA::delete('workerqueue', ['id' => $queue['id']]); DBA::delete('workerqueue', ['id' => $queue['id']]);
self::$db_duration = (microtime(true) - $stamp); self::$db_duration = (microtime(true) - $stamp);
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
@ -551,7 +551,7 @@ class Worker
DI::logger()->info('Process start.', ['priority' => $queue['priority'], 'id' => $queue['id']]); DI::logger()->info('Process start.', ['priority' => $queue['priority'], 'id' => $queue['id']]);
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
// We use the callstack here to analyze the performance of executed worker entries. // We use the callstack here to analyze the performance of executed worker entries.
// For this reason the variables have to be initialized. // For this reason the variables have to be initialized.
@ -648,7 +648,7 @@ class Worker
$max = $r['Value']; $max = $r['Value'];
} }
// Or it can be granted. This overrides the system variable // Or it can be granted. This overrides the system variable
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$r = DBA::p('SHOW GRANTS'); $r = DBA::p('SHOW GRANTS');
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
while ($grants = DBA::fetch($r)) { while ($grants = DBA::fetch($r)) {
@ -662,7 +662,7 @@ class Worker
DBA::close($r); DBA::close($r);
} }
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$used = 0; $used = 0;
$sleep = 0; $sleep = 0;
$data = DBA::p("SHOW PROCESSLIST"); $data = DBA::p("SHOW PROCESSLIST");
@ -684,7 +684,7 @@ class Worker
$level = ($used / $max) * 100; $level = ($used / $max) * 100;
if ($level >= $maxlevel) { if ($level >= $maxlevel) {
DI::logger()->warning('Maximum level (' . $maxlevel . '%) of user connections reached: ' . $used .'/' . $max); DI::logger()->warning('Maximum level (' . $maxlevel . '%) of user connections reached: ' . $used . '/' . $max);
return true; return true;
} }
} }
@ -756,16 +756,16 @@ class Worker
if ($interval == 0) { if ($interval == 0) {
continue; continue;
} else { } else {
$interval = (int)$interval; $interval = (int) $interval;
} }
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$jobs = DBA::count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - ' . $interval . ' minute')]); $jobs = DBA::count('workerqueue', ["`done` AND `executed` > ?", DateTimeFormat::utc('now - ' . $interval . ' minute')]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_stat += (microtime(true) - $stamp); self::$db_duration_stat += (microtime(true) - $stamp);
$jobs_per_minute[$interval] = number_format($jobs / $interval, 0); $jobs_per_minute[$interval] = number_format($jobs / $interval, 0);
} }
$processlist = ' - jpm: '.implode('/', $jobs_per_minute); $processlist = ' - jpm: ' . implode('/', $jobs_per_minute);
} }
// Create a list of queue entries grouped by their priority // Create a list of queue entries grouped by their priority
@ -778,12 +778,12 @@ class Worker
if (DI::config()->get('system', 'worker_debug')) { if (DI::config()->get('system', 'worker_debug')) {
$waiting_processes = 0; $waiting_processes = 0;
// Now adding all processes with workerqueue entries // Now adding all processes with workerqueue entries
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$jobs = DBA::p("SELECT COUNT(*) AS `entries`, `priority` FROM `workerqueue` WHERE NOT `done` GROUP BY `priority`"); $jobs = DBA::p("SELECT COUNT(*) AS `entries`, `priority` FROM `workerqueue` WHERE NOT `done` GROUP BY `priority`");
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_stat += (microtime(true) - $stamp); self::$db_duration_stat += (microtime(true) - $stamp);
while ($entry = DBA::fetch($jobs)) { while ($entry = DBA::fetch($jobs)) {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$running = DBA::count('workerqueue-view', ['priority' => $entry['priority']]); $running = DBA::count('workerqueue-view', ['priority' => $entry['priority']]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_stat += (microtime(true) - $stamp); self::$db_duration_stat += (microtime(true) - $stamp);
@ -794,7 +794,7 @@ class Worker
DBA::close($jobs); DBA::close($jobs);
} else { } else {
$waiting_processes = self::totalEntries(); $waiting_processes = self::totalEntries();
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$jobs = DBA::p("SELECT COUNT(*) AS `running`, `priority` FROM `workerqueue-view` GROUP BY `priority` ORDER BY `priority`"); $jobs = DBA::p("SELECT COUNT(*) AS `running`, `priority` FROM `workerqueue-view` GROUP BY `priority` ORDER BY `priority`");
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_stat += (microtime(true) - $stamp); self::$db_duration_stat += (microtime(true) - $stamp);
@ -810,7 +810,7 @@ class Worker
$listitem[0] = '0:' . max(0, $idle_workers); $listitem[0] = '0:' . max(0, $idle_workers);
$processlist .= ' ('.implode(', ', $listitem).')'; $processlist .= ' (' . implode(', ', $listitem) . ')';
if (DI::config()->get('system', 'worker_fastlane', false) && ($queues > 0) && ($active >= $queues) && self::entriesExists()) { if (DI::config()->get('system', 'worker_fastlane', false) && ($queues > 0) && ($active >= $queues) && self::entriesExists()) {
$top_priority = self::highestPriority(); $top_priority = self::highestPriority();
@ -866,7 +866,7 @@ class Worker
*/ */
public static function activeWorkers(): int public static function activeWorkers(): int
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$count = DI::process()->countCommand('Worker.php'); $count = DI::process()->countCommand('Worker.php');
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_count += (microtime(true) - $stamp); self::$db_duration_count += (microtime(true) - $stamp);
@ -882,7 +882,7 @@ class Worker
private static function getWorkerPIDList(): array private static function getWorkerPIDList(): array
{ {
$ids = []; $ids = [];
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$queues = DBA::p("SELECT `process`.`pid`, COUNT(`workerqueue`.`pid`) AS `entries` FROM `process` $queues = DBA::p("SELECT `process`.`pid`, COUNT(`workerqueue`.`pid`) AS `entries` FROM `process`
LEFT JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid` AND NOT `workerqueue`.`done` LEFT JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid` AND NOT `workerqueue`.`done`
@ -905,7 +905,7 @@ class Worker
*/ */
private static function getWaitingJobForPID() private static function getWaitingJobForPID()
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$r = DBA::select('workerqueue', [], ['pid' => getmypid(), 'done' => false]); $r = DBA::select('workerqueue', [], ['pid' => getmypid(), 'done' => false]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
@ -931,7 +931,7 @@ class Worker
} }
$ids = []; $ids = [];
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$condition = ["`priority` = ? AND `pid` = 0 AND NOT `done` AND `next_try` < ?", $priority, DateTimeFormat::utcNow()]; $condition = ["`priority` = ? AND `pid` = 0 AND NOT `done` AND `next_try` < ?", $priority, DateTimeFormat::utcNow()];
$tasks = DBA::select('workerqueue', ['id', 'command', 'parameter'], $condition, ['limit' => $limit, 'order' => ['retrial', 'created']]); $tasks = DBA::select('workerqueue', ['id', 'command', 'parameter'], $condition, ['limit' => $limit, 'order' => ['retrial', 'created']]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
@ -965,7 +965,7 @@ class Worker
$waiting = []; $waiting = [];
$priorities = [self::PRIORITY_CRITICAL, self::PRIORITY_HIGH, self::PRIORITY_MEDIUM, self::PRIORITY_LOW, self::PRIORITY_NEGLIGIBLE]; $priorities = [self::PRIORITY_CRITICAL, self::PRIORITY_HIGH, self::PRIORITY_MEDIUM, self::PRIORITY_LOW, self::PRIORITY_NEGLIGIBLE];
foreach ($priorities as $priority) { foreach ($priorities as $priority) {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
if (DBA::exists('workerqueue', ["`priority` = ? AND `pid` = 0 AND NOT `done` AND `next_try` < ?", $priority, DateTimeFormat::utcNow()])) { if (DBA::exists('workerqueue', ["`priority` = ? AND `pid` = 0 AND NOT `done` AND `next_try` < ?", $priority, DateTimeFormat::utcNow()])) {
$waiting[$priority] = true; $waiting[$priority] = true;
} }
@ -978,7 +978,7 @@ class Worker
$running = []; $running = [];
$running_total = 0; $running_total = 0;
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$processes = DBA::p("SELECT COUNT(DISTINCT(`pid`)) AS `running`, `priority` FROM `workerqueue-view` GROUP BY `priority`"); $processes = DBA::p("SELECT COUNT(DISTINCT(`pid`)) AS `running`, `priority` FROM `workerqueue-view` GROUP BY `priority`");
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
while ($process = DBA::fetch($processes)) { while ($process = DBA::fetch($processes)) {
@ -1056,7 +1056,7 @@ class Worker
// If there is not enough results we check without priority limit // If there is not enough results we check without priority limit
if ($limit > 0) { if ($limit > 0) {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$condition = ["`pid` = 0 AND NOT `done` AND `next_try` < ?", DateTimeFormat::utcNow()]; $condition = ["`pid` = 0 AND NOT `done` AND `next_try` < ?", DateTimeFormat::utcNow()];
$tasks = DBA::select('workerqueue', ['id', 'command', 'parameter'], $condition, ['limit' => $limit, 'order' => ['priority', 'retrial', 'created']]); $tasks = DBA::select('workerqueue', ['id', 'command', 'parameter'], $condition, ['limit' => $limit, 'order' => ['priority', 'retrial', 'created']]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
@ -1090,13 +1090,13 @@ class Worker
$worker[$pid][] = $id; $worker[$pid][] = $id;
} }
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
foreach ($worker as $worker_pid => $worker_ids) { foreach ($worker as $worker_pid => $worker_ids) {
DI::logger()->info('Set queue entry', ['pid' => $worker_pid, 'ids' => $worker_ids]); DI::logger()->info('Set queue entry', ['pid' => $worker_pid, 'ids' => $worker_ids]);
DBA::update( DBA::update(
'workerqueue', 'workerqueue',
['executed' => DateTimeFormat::utcNow(), 'pid' => $worker_pid], ['executed' => DateTimeFormat::utcNow(), 'pid' => $worker_pid],
['id' => $worker_ids, 'done' => false, 'pid' => 0] ['id' => $worker_ids, 'done' => false, 'pid' => 0],
); );
} }
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
@ -1117,7 +1117,7 @@ class Worker
return $waiting; return $waiting;
} }
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
if (!DI::lock()->acquire(self::LOCK_PROCESS)) { if (!DI::lock()->acquire(self::LOCK_PROCESS)) {
return []; return [];
} }
@ -1142,7 +1142,7 @@ class Worker
*/ */
public static function unclaimProcess(Process $process) public static function unclaimProcess(Process $process)
{ {
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
DBA::update('workerqueue', ['executed' => DBA::NULL_DATETIME, 'pid' => 0], ['pid' => $process->pid, 'done' => false]); DBA::update('workerqueue', ['executed' => DBA::NULL_DATETIME, 'pid' => 0], ['pid' => $process->pid, 'done' => false]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);
self::$db_duration_write += (microtime(true) - $stamp); self::$db_duration_write += (microtime(true) - $stamp);
@ -1374,7 +1374,7 @@ class Worker
$new_retrial = $queue['retrial'] + 1; $new_retrial = $queue['retrial'] + 1;
$total = 0; $total = 0;
for ($retrial = 0; $retrial <= $max_level + 1; ++$retrial) { for ($retrial = 0; $retrial <= $max_level + 1; ++$retrial) {
$delay = (($retrial + 3) ** 4) + (rand(1, 30) * ($retrial + 1)); $delay = (($retrial + 3) ** 4) + (random_int(1, 30) * ($retrial + 1));
$total += $delay; $total += $delay;
if (($total < $retrial_time) && ($retrial > $queue['retrial'])) { if (($total < $retrial_time) && ($retrial > $queue['retrial'])) {
$new_retrial = $retrial; $new_retrial = $retrial;
@ -1427,7 +1427,7 @@ class Worker
} }
// Calculate the delay until the next trial // Calculate the delay until the next trial
$delay = (($new_retrial + 2) ** 4) + (rand(1, 30) * ($new_retrial)); $delay = (($new_retrial + 2) ** 4) + (random_int(1, 30) * ($new_retrial));
$next = DateTimeFormat::utc('now + ' . $delay . ' seconds'); $next = DateTimeFormat::utc('now + ' . $delay . ' seconds');
if (($priority < self::PRIORITY_MEDIUM) && ($new_retrial > 3)) { if (($priority < self::PRIORITY_MEDIUM) && ($new_retrial > 3)) {
@ -1440,7 +1440,7 @@ class Worker
DI::logger()->info('Deferred task', ['id' => $id, 'retrial' => $new_retrial, 'created' => $queue['created'], 'next_execution' => $next, 'old_prio' => $queue['priority'], 'new_prio' => $priority]); DI::logger()->info('Deferred task', ['id' => $id, 'retrial' => $new_retrial, 'created' => $queue['created'], 'next_execution' => $next, 'old_prio' => $queue['priority'], 'new_prio' => $priority]);
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
$fields = ['retrial' => $new_retrial, 'next_try' => $next, 'executed' => DBA::NULL_DATETIME, 'pid' => 0, 'priority' => $priority]; $fields = ['retrial' => $new_retrial, 'next_try' => $next, 'executed' => DBA::NULL_DATETIME, 'pid' => 0, 'priority' => $priority];
DBA::update('workerqueue', $fields, ['id' => $id]); DBA::update('workerqueue', $fields, ['id' => $id]);
self::$db_duration += (microtime(true) - $stamp); self::$db_duration += (microtime(true) - $stamp);

View file

@ -259,11 +259,11 @@ abstract class DI
} }
/** /**
* @deprecated 2025.07 Use `DI::loggerManager()` and `DI::logger()` instead * @deprecated 2026.01 Use `DI::loggerManager()` and `DI::logger()` instead
*/ */
public static function workerLogger(): Core\Logger\Type\WorkerLogger public static function workerLogger(): Core\Logger\Type\WorkerLogger
{ {
@trigger_error('`' . __METHOD__ . '()` is deprecated since 2025.07 and will be removed after 5 months, use `DI::logger()` instead.', E_USER_DEPRECATED); @trigger_error('`' . __METHOD__ . '()` is deprecated since 2026.01 and will be removed after 5 months, use `DI::logger()` instead.', E_USER_DEPRECATED);
return self::$dice->create(Core\Logger\Type\WorkerLogger::class); return self::$dice->create(Core\Logger\Type\WorkerLogger::class);
} }
@ -413,6 +413,11 @@ abstract class DI
return self::$dice->create(Network\HTTPClient\Capability\ICanSendHttpRequests::class); return self::$dice->create(Network\HTTPClient\Capability\ICanSendHttpRequests::class);
} }
public static function robotsTxt(): Network\RobotsTxt
{
return self::$dice->create(Network\RobotsTxt::class);
}
// //
// "Repository" namespace // "Repository" namespace
// //
@ -627,7 +632,7 @@ abstract class DI
/** /**
* @internal The EventDispatcher should never called outside of the core, like in addons or themes * @internal The EventDispatcher should never called outside of the core, like in addons or themes
* @deprecated 2025.07 Use constructor injection instead * @deprecated 2026.01 Use constructor injection instead
*/ */
public static function eventDispatcher(): \Psr\EventDispatcher\EventDispatcherInterface public static function eventDispatcher(): \Psr\EventDispatcher\EventDispatcherInterface
{ {

View file

@ -23,16 +23,16 @@ class DBA
/** /**
* Lowest possible date value * Lowest possible date value
*/ */
const NULL_DATE = '0001-01-01'; public const NULL_DATE = '0001-01-01';
/** /**
* Lowest possible datetime value * Lowest possible datetime value
*/ */
const NULL_DATETIME = '0001-01-01 00:00:00'; public const NULL_DATETIME = '0001-01-01 00:00:00';
/** /**
* Lowest possible datetime(6) value * Lowest possible datetime(6) value
*/ */
const NULL_DATETIME6 = '0001-01-01 00:00:00.000000'; public const NULL_DATETIME6 = '0001-01-01 00:00:00.000000';
public static function connect(): bool public static function connect(): bool
{ {
@ -130,11 +130,11 @@ class DBA
*/ */
public static function cleanQuery(string $sql): string public static function cleanQuery(string $sql): string
{ {
$search = ["\t", "\n", "\r", " "]; $search = ["\t", "\n", "\r", " "];
$replace = [' ', ' ', ' ', ' ']; $replace = [' ', ' ', ' ', ' '];
do { do {
$oldsql = $sql; $oldsql = $sql;
$sql = str_replace($search, $replace, $sql); $sql = str_replace($search, $replace, $sql);
} while ($oldsql != $sql); } while ($oldsql != $sql);
return $sql; return $sql;
@ -518,8 +518,8 @@ class DBA
'.', '.',
array_map( array_map(
function (string $identifier) { return '`' . str_replace('`', '``', $identifier) . '`'; }, function (string $identifier) { return '`' . str_replace('`', '``', $identifier) . '`'; },
explode('.', $identifier) explode('.', $identifier),
) ),
); );
} }
@ -571,16 +571,14 @@ class DBA
if (count($condition) < 1) { if (count($condition) < 1) {
return ['1']; return ['1'];
} }
$first_key = array_key_first($condition);
reset($condition);
$first_key = key($condition);
if (is_int($first_key)) { if (is_int($first_key)) {
// Already collapsed // Already collapsed
return $condition; return $condition;
} }
$values = []; $values = [];
$condition_string = ""; $condition_string = "";
foreach ($condition as $field => $value) { foreach ($condition as $field => $value) {
if ($condition_string != "") { if ($condition_string != "") {
@ -594,7 +592,7 @@ class DBA
* In case of mixed types, cast all as string. * In case of mixed types, cast all as string.
* Logic needs to be consistent with DBA::p() data types. * Logic needs to be consistent with DBA::p() data types.
*/ */
$is_int = false; $is_int = false;
$is_alpha = false; $is_alpha = false;
foreach ($value as $single_value) { foreach ($value as $single_value) {
if (is_int($single_value)) { if (is_int($single_value)) {
@ -607,13 +605,13 @@ class DBA
if ($is_int && $is_alpha) { if ($is_int && $is_alpha) {
foreach ($value as &$ref) { foreach ($value as &$ref) {
if (is_int($ref)) { if (is_int($ref)) {
$ref = (string)$ref; $ref = (string) $ref;
} }
} }
unset($ref); //Prevent accidental re-use. unset($ref); //Prevent accidental re-use.
} }
$values = array_merge($values, array_values($value)); $values = array_merge($values, array_values($value));
$placeholders = substr(str_repeat("?, ", count($value)), 0, -2); $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
$condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")"; $condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
} else { } else {
@ -647,7 +645,7 @@ class DBA
} }
$conditionStrings = []; $conditionStrings = [];
$result = []; $result = [];
foreach ($conditions as $key => $condition) { foreach ($conditions as $key => $condition) {
if (!$condition) { if (!$condition) {

View file

@ -31,14 +31,14 @@ use Psr\Log\NullLogger;
*/ */
class Database class Database
{ {
const PDO = 'pdo'; public const PDO = 'pdo';
const MYSQLI = 'mysqli'; public const MYSQLI = 'mysqli';
const INSERT_DEFAULT = 0; public const INSERT_DEFAULT = 0;
const INSERT_UPDATE = 1; public const INSERT_UPDATE = 1;
const INSERT_IGNORE = 2; public const INSERT_IGNORE = 2;
const LOCK_OPTIMIZE = 'database::optimize_tables'; public const LOCK_OPTIMIZE = 'database::optimize_tables';
protected $connected = false; protected $connected = false;
@ -145,9 +145,9 @@ class Database
return false; return false;
} }
$persistent = (bool)$this->config->get('database', 'persistent'); $persistent = (bool) $this->config->get('database', 'persistent');
$this->pdo_emulate_prepares = (bool)$this->config->get('database', 'pdo_emulate_prepares'); $this->pdo_emulate_prepares = (bool) $this->config->get('database', 'pdo_emulate_prepares');
if (!$this->config->get('database', 'disable_pdo') && class_exists('\PDO') && in_array('mysql', PDO::getAvailableDrivers())) { if (!$this->config->get('database', 'disable_pdo') && class_exists('\PDO') && in_array('mysql', PDO::getAvailableDrivers())) {
$this->driver = self::PDO; $this->driver = self::PDO;
@ -340,8 +340,8 @@ class Database
while ($row = $this->fetch($r)) { while ($row = $this->fetch($r)) {
if ((intval($this->config->get('system', 'db_loglimit_index')) > 0)) { if ((intval($this->config->get('system', 'db_loglimit_index')) > 0)) {
$log = (in_array($row['key'], $watchlist) && $log = (in_array($row['key'], $watchlist)
($row['rows'] >= intval($this->config->get('system', 'db_loglimit_index')))); && ($row['rows'] >= intval($this->config->get('system', 'db_loglimit_index'))));
} else { } else {
$log = false; $log = false;
} }
@ -358,12 +358,12 @@ class Database
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
@file_put_contents( @file_put_contents(
$this->config->get('system', 'db_log_index'), $this->config->get('system', 'db_log_index'),
DateTimeFormat::utcNow() . "\t" . DateTimeFormat::utcNow() . "\t"
$row['key'] . "\t" . $row['rows'] . "\t" . $row['Extra'] . "\t" . . $row['key'] . "\t" . $row['rows'] . "\t" . $row['Extra'] . "\t"
basename($backtrace[1]["file"]) . "\t" . . basename($backtrace[1]["file"]) . "\t"
$backtrace[1]["line"] . "\t" . $backtrace[2]["function"] . "\t" . . $backtrace[1]["line"] . "\t" . $backtrace[2]["function"] . "\t"
substr($query, 0, 4000) . "\n", . substr($query, 0, 4000) . "\n",
FILE_APPEND FILE_APPEND,
); );
} }
} }
@ -491,7 +491,7 @@ class Database
foreach ($params as $param) { foreach ($params as $param) {
// Avoid problems with some MySQL servers and boolean values. See issue #3645 // Avoid problems with some MySQL servers and boolean values. See issue #3645
if (is_bool($param)) { if (is_bool($param)) {
$param = (int)$param; $param = (int) $param;
} }
$args[++$i] = $param; $args[++$i] = $param;
} }
@ -542,8 +542,8 @@ class Database
if (count($args) == 0) { if (count($args) == 0) {
if (!$retval = $this->connection->query($this->replaceParameters($sql, $args))) { if (!$retval = $this->connection->query($this->replaceParameters($sql, $args))) {
$errorInfo = $this->connection->errorInfo(); $errorInfo = $this->connection->errorInfo();
$this->error = (string)$errorInfo[2]; $this->error = (string) $errorInfo[2];
$this->errorno = (int)$errorInfo[1]; $this->errorno = (int) $errorInfo[1];
$retval = false; $retval = false;
$is_error = true; $is_error = true;
break; break;
@ -557,8 +557,8 @@ class Database
if (!$stmt) { if (!$stmt) {
$errorInfo = $this->connection->errorInfo(); $errorInfo = $this->connection->errorInfo();
$this->error = (string)$errorInfo[2]; $this->error = (string) $errorInfo[2];
$this->errorno = (int)$errorInfo[1]; $this->errorno = (int) $errorInfo[1];
$retval = false; $retval = false;
$is_error = true; $is_error = true;
break; break;
@ -569,7 +569,7 @@ class Database
if (is_int($args[$param])) { if (is_int($args[$param])) {
$data_type = PDO::PARAM_INT; $data_type = PDO::PARAM_INT;
} elseif ($args[$param] !== null) { } elseif ($args[$param] !== null) {
$args[$param] = (string)$args[$param]; $args[$param] = (string) $args[$param];
} }
$stmt->bindParam($param, $args[$param], $data_type); $stmt->bindParam($param, $args[$param], $data_type);
@ -577,8 +577,8 @@ class Database
if (!$stmt->execute()) { if (!$stmt->execute()) {
$errorInfo = $stmt->errorInfo(); $errorInfo = $stmt->errorInfo();
$this->error = (string)$errorInfo[2]; $this->error = (string) $errorInfo[2];
$this->errorno = (int)$errorInfo[1]; $this->errorno = (int) $errorInfo[1];
$retval = false; $retval = false;
$is_error = true; $is_error = true;
} else { } else {
@ -596,8 +596,8 @@ class Database
if (!$can_be_prepared || (count($args) == 0)) { if (!$can_be_prepared || (count($args) == 0)) {
$retval = $this->connection->query($this->replaceParameters($sql, $args)); $retval = $this->connection->query($this->replaceParameters($sql, $args));
if ($this->connection->errno) { if ($this->connection->errno) {
$this->error = (string)$this->connection->error; $this->error = (string) $this->connection->error;
$this->errorno = (int)$this->connection->errno; $this->errorno = (int) $this->connection->errno;
$retval = false; $retval = false;
$is_error = true; $is_error = true;
} else { } else {
@ -613,8 +613,8 @@ class Database
$stmt = $this->connection->stmt_init(); $stmt = $this->connection->stmt_init();
if (!$stmt->prepare($sql)) { if (!$stmt->prepare($sql)) {
$this->error = (string)$stmt->error; $this->error = (string) $stmt->error;
$this->errorno = (int)$stmt->errno; $this->errorno = (int) $stmt->errno;
$retval = false; $retval = false;
$is_error = true; $is_error = true;
break; break;
@ -631,7 +631,7 @@ class Database
$param_types .= 's'; $param_types .= 's';
} elseif (is_object($args[$param]) && method_exists($args[$param], '__toString')) { } elseif (is_object($args[$param]) && method_exists($args[$param], '__toString')) {
$param_types .= 's'; $param_types .= 's';
$args[$param] = (string)$args[$param]; $args[$param] = (string) $args[$param];
} else { } else {
$param_types .= 'b'; $param_types .= 'b';
} }
@ -644,8 +644,8 @@ class Database
} }
if (!$stmt->execute()) { if (!$stmt->execute()) {
$this->error = (string)$this->connection->error; $this->error = (string) $this->connection->error;
$this->errorno = (int)$this->connection->errno; $this->errorno = (int) $this->connection->errno;
$retval = false; $retval = false;
$is_error = true; $is_error = true;
} else { } else {
@ -711,15 +711,15 @@ class Database
} }
} }
$this->error = (string)$error; $this->error = (string) $error;
$this->errorno = (int)$errorno; $this->errorno = (int) $errorno;
} }
$this->profiler->stopRecording(); $this->profiler->stopRecording();
if ($this->config->get('system', 'db_log')) { if ($this->config->get('system', 'db_log')) {
$stamp2 = microtime(true); $stamp2 = microtime(true);
$duration = (float)($stamp2 - $stamp1); $duration = (float) ($stamp2 - $stamp1);
if (($duration > $this->config->get('system', 'db_loglimit'))) { if (($duration > $this->config->get('system', 'db_loglimit'))) {
$duration = round($duration, 3); $duration = round($duration, 3);
@ -727,11 +727,11 @@ class Database
@file_put_contents( @file_put_contents(
$this->config->get('system', 'db_log'), $this->config->get('system', 'db_log'),
DateTimeFormat::utcNow() . "\t" . $duration . "\t" . DateTimeFormat::utcNow() . "\t" . $duration . "\t"
basename($backtrace[0]['file']) . "\t" . . basename($backtrace[0]['file']) . "\t"
$backtrace[0]['line'] . "\t" . $backtrace[0]['function'] . "\t" . . $backtrace[0]['line'] . "\t" . $backtrace[0]['function'] . "\t"
substr($this->replaceParameters($sql, $args), 0, 4000) . "\n", . substr($this->replaceParameters($sql, $args), 0, 4000) . "\n",
FILE_APPEND FILE_APPEND,
); );
} }
} }
@ -801,6 +801,8 @@ class Database
$this->error = $error; $this->error = $error;
$this->errorno = $errorno; $this->errorno = $errorno;
} elseif (!$retval) {
$this->logger->warning('Database execution was unsuccessful', ['sql' => $this->replaceParameters($sql, $params), 'timeout' => $timeout]);
} }
$this->profiler->stopRecording(); $this->profiler->stopRecording();
@ -829,9 +831,7 @@ class Database
if (empty($condition)) { if (empty($condition)) {
return DBStructure::existsTable($table); return DBStructure::existsTable($table);
} }
$first_key = array_key_first($condition);
reset($condition);
$first_key = key($condition);
if (!is_int($first_key)) { if (!is_int($first_key)) {
$fields = [$first_key]; $fields = [$first_key];
} }
@ -1039,6 +1039,10 @@ class Database
return $result; return $result;
} }
if ($this->affectedRows() === 0) {
$this->logger->info('affectedRows is 0.', ['table' => $table, 'fields' => $param, 'sql' => $this->replaceParameters($sql, $param)]);
}
return $this->affectedRows() != 0; return $this->affectedRows() != 0;
} }
@ -1088,7 +1092,7 @@ class Database
$id = $this->connection->insert_id; $id = $this->connection->insert_id;
break; break;
} }
return (int)$id; return (int) $id;
} }
/** /**
@ -1482,6 +1486,19 @@ class Database
return false; return false;
} }
$sql = $this->getSQL($table, $fields, $condition, $params);
DBA::buildCondition($condition);
$result = $this->p($sql, $condition);
if ($this->driver == self::PDO && !empty($result)) {
$this->currentTable = $table;
}
return $result;
}
public function getSQL(string $table, array $fields = [], array $condition = [], array $params = []): string
{
if (count($fields) > 0) { if (count($fields) > 0) {
$fields = $this->escapeFields($fields, $params); $fields = $this->escapeFields($fields, $params);
$select_string = implode(', ', $fields); $select_string = implode(', ', $fields);
@ -1495,15 +1512,7 @@ class Database
$param_string = DBA::buildParameter($params); $param_string = DBA::buildParameter($params);
$sql = "SELECT " . $select_string . " FROM " . $table_string . $condition_string . $param_string; return "SELECT " . $select_string . " FROM " . $table_string . $condition_string . $param_string;
$result = $this->p($sql, $condition);
if ($this->driver == self::PDO && !empty($result)) {
$this->currentTable = $table;
}
return $result;
} }
/** /**
@ -1551,7 +1560,7 @@ class Database
$this->logger->notice('Invalid count.', ['table' => $table, 'row' => $row, 'expression' => $expression, 'condition' => $condition_string, 'callstack' => System::callstack()]); $this->logger->notice('Invalid count.', ['table' => $table, 'row' => $row, 'expression' => $expression, 'condition' => $condition_string, 'callstack' => System::callstack()]);
return 0; return 0;
} else { } else {
return (int)$row['count']; return (int) $row['count'];
} }
} }
@ -1638,13 +1647,13 @@ class Database
continue; continue;
} }
if ((substr($types[$field], 0, 7) == 'tinyint') || (substr($types[$field], 0, 8) == 'smallint') || if ((substr($types[$field], 0, 7) == 'tinyint') || (substr($types[$field], 0, 8) == 'smallint')
(substr($types[$field], 0, 9) == 'mediumint') || (substr($types[$field], 0, 3) == 'int') || || (substr($types[$field], 0, 9) == 'mediumint') || (substr($types[$field], 0, 3) == 'int')
(substr($types[$field], 0, 6) == 'bigint') || (substr($types[$field], 0, 7) == 'boolean')) { || (substr($types[$field], 0, 6) == 'bigint') || (substr($types[$field], 0, 7) == 'boolean')) {
$fields[$field] = (int)$content; $fields[$field] = (int) $content;
} }
if ((substr($types[$field], 0, 5) == 'float') || (substr($types[$field], 0, 6) == 'double')) { if ((substr($types[$field], 0, 5) == 'float') || (substr($types[$field], 0, 6) == 'double')) {
$fields[$field] = (float)$content; $fields[$field] = (float) $content;
} }
} }
@ -1884,7 +1893,7 @@ class Database
if (is_bool($value)) { if (is_bool($value)) {
$value = ($value ? 'true' : 'false'); $value = ($value ? 'true' : 'false');
} elseif (is_float($value) || is_integer($value)) { } elseif (is_float($value) || is_integer($value)) {
$value = (string)$value; $value = (string) $value;
} else { } else {
$value = "'" . $this->escape($value) . "'"; $value = "'" . $this->escape($value) . "'";
} }

View file

@ -70,7 +70,7 @@ class DbaDefinition
// 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]) || (!isset($definition[$table]['fields'][$field]['not null']) && array_key_exists($field, $data))) {
// Limit the length of varchar, varbinary, char and binary fields // Limit the length of varchar, varbinary, char and binary fields
if (is_string($data[$field]) && preg_match("/char\((\d*)\)/", $definition[$table]['fields'][$field]['type'], $result)) { if (is_string($data[$field]) && preg_match("/char\((\d*)\)/", $definition[$table]['fields'][$field]['type'], $result)) {
if ($charset == 'latin1') { if ($charset == 'latin1') {

View file

@ -35,9 +35,9 @@ use GuzzleHttp\Psr7\Uri;
class PostUpdate class PostUpdate
{ {
// Needed for the helper function to read from the legacy term table // Needed for the helper function to read from the legacy term table
const OBJECT_TYPE_POST = 1; public const OBJECT_TYPE_POST = 1;
const VERSION = 1550; public const VERSION = 1550;
/** /**
* Calls the post update functions * Calls the post update functions
@ -185,7 +185,7 @@ class PostUpdate
Protocol::DIASPORA, Protocol::DIASPORA,
Protocol::OSTATUS, Protocol::OSTATUS,
Protocol::ACTIVITYPUB, Protocol::ACTIVITYPUB,
0 0,
); );
while ($contact = DBA::fetch($contacts)) { while ($contact = DBA::fetch($contacts)) {
@ -348,7 +348,7 @@ class PostUpdate
Tag::MENTION, Tag::MENTION,
Tag::EXCLUSIVE_MENTION, Tag::EXCLUSIVE_MENTION,
Tag::IMPLICIT_MENTION, Tag::IMPLICIT_MENTION,
$id $id,
); );
if (DBA::errorNo() != 0) { if (DBA::errorNo() != 0) {
@ -357,7 +357,7 @@ class PostUpdate
} }
while ($term = DBA::fetch($terms)) { while ($term = DBA::fetch($terms)) {
if (($term['type'] == Tag::MENTION) && !empty($term['url']) && !strstr($term['body'], $term['url'])) { if (($term['type'] == Tag::MENTION) && !empty($term['url']) && !strstr($term['body'], (string) $term['url'])) {
$condition = ['nurl' => Strings::normaliseLink($term['url']), 'uid' => 0, 'deleted' => false]; $condition = ['nurl' => Strings::normaliseLink($term['url']), 'uid' => 0, 'deleted' => false];
$contact = DBA::selectFirst('contact', ['url', 'alias'], $condition, ['order' => ['id']]); $contact = DBA::selectFirst('contact', ['url', 'alias'], $condition, ['order' => ['id']]);
if (!DBA::isResult($contact)) { if (!DBA::isResult($contact)) {
@ -366,7 +366,7 @@ class PostUpdate
$contact = DBA::selectFirst('contact', ['url', 'alias'], $condition, ['order' => ['id']]); $contact = DBA::selectFirst('contact', ['url', 'alias'], $condition, ['order' => ['id']]);
} }
if (DBA::isResult($contact) && (!strstr($term['body'], $contact['url']) && (empty($contact['alias']) || !strstr($term['body'], $contact['alias'])))) { if (DBA::isResult($contact) && (!strstr($term['body'], (string) $contact['url']) && (empty($contact['alias']) || !strstr($term['body'], (string) $contact['alias'])))) {
$term['type'] = Tag::IMPLICIT_MENTION; $term['type'] = Tag::IMPLICIT_MENTION;
} }
} }
@ -507,7 +507,7 @@ class PostUpdate
'term', 'term',
['oid'], ['oid'],
["`type` IN (?, ?) AND `oid` >= ?", Category::CATEGORY, Category::FILE, $id], ["`type` IN (?, ?) AND `oid` >= ?", Category::CATEGORY, Category::FILE, $id],
['order' => ['oid'], 'limit' => 1000, 'group_by' => ['oid']] ['order' => ['oid'], 'limit' => 1000, 'group_by' => ['oid']],
); );
if (DBA::errorNo() != 0) { if (DBA::errorNo() != 0) {
@ -651,7 +651,7 @@ class PostUpdate
DBA::update( DBA::update(
'contact', 'contact',
['gsid' => GServer::getRealID($contact['baseurl'], true), 'baseurl' => GServer::cleanURL($contact['baseurl'])], ['gsid' => GServer::getRealID($contact['baseurl'], true), 'baseurl' => GServer::cleanURL($contact['baseurl'])],
['id' => $contact['id']] ['id' => $contact['id']],
); );
++$rows; ++$rows;
@ -706,7 +706,7 @@ class PostUpdate
DBA::update( DBA::update(
'apcontact', 'apcontact',
['gsid' => GServer::getRealID($apcontact['baseurl'], true), 'baseurl' => GServer::cleanURL($apcontact['baseurl'])], ['gsid' => GServer::getRealID($apcontact['baseurl'], true), 'baseurl' => GServer::cleanURL($apcontact['baseurl'])],
['url' => $apcontact['url']] ['url' => $apcontact['url']],
); );
++$rows; ++$rows;
@ -1079,7 +1079,7 @@ class PostUpdate
WHERE NOT `source` IS NULL AND `conversation`.`protocol` = ? AND `uri-id` > ? LIMIT ?", WHERE NOT `source` IS NULL AND `conversation`.`protocol` = ? AND `uri-id` > ? LIMIT ?",
Conversation::PARCEL_ACTIVITYPUB, Conversation::PARCEL_ACTIVITYPUB,
$id, $id,
1000 1000,
); );
if (DBA::errorNo() != 0) { if (DBA::errorNo() != 0) {
@ -1239,12 +1239,12 @@ class PostUpdate
$parts = parse_url($contact['url']); $parts = parse_url($contact['url']);
unset($parts['path']); unset($parts['path']);
$server = (string)Uri::fromParts($parts); $server = (string) Uri::fromParts($parts);
DBA::update( DBA::update(
'contact', 'contact',
['gsid' => GServer::getRealID($server, true), 'baseurl' => GServer::cleanURL($server)], ['gsid' => GServer::getRealID($server, true), 'baseurl' => GServer::cleanURL($server)],
['id' => $contact['id']] ['id' => $contact['id']],
); );
++$rows; ++$rows;
@ -1333,10 +1333,10 @@ class PostUpdate
return true; return true;
} }
$id = (int)(DI::keyValue()->get('post_update_version_1544_id') ?? 0); $id = (int) (DI::keyValue()->get('post_update_version_1544_id') ?? 0);
if ($id == 0) { if ($id == 0) {
$post = Post::selectFirstPost(['uri-id'], [], ['order' => ['uri-id' => true]]); $post = Post::selectFirstPost(['uri-id'], [], ['order' => ['uri-id' => true]]);
$id = (int)($post['uri-id'] ?? 0); $id = (int) ($post['uri-id'] ?? 0);
} }
DI::logger()->info('Start', ['uri-id' => $id]); DI::logger()->info('Start', ['uri-id' => $id]);
@ -1398,10 +1398,10 @@ class PostUpdate
} }
DBA::close($engagements); DBA::close($engagements);
$id = (int)(DI::keyValue()->get('post_update_version_1550_id') ?? 0); $id = (int) (DI::keyValue()->get('post_update_version_1550_id') ?? 0);
if ($id == 0) { if ($id == 0) {
$post = Post::selectFirstPost(['uri-id'], [], ['order' => ['uri-id' => true]]); $post = Post::selectFirstPost(['uri-id'], [], ['order' => ['uri-id' => true]]);
$id = (int)($post['uri-id'] ?? 0); $id = (int) ($post['uri-id'] ?? 0);
} }
DI::logger()->info('Start', ['uri-id' => $id]); DI::logger()->info('Start', ['uri-id' => $id]);

View file

@ -33,12 +33,11 @@ class Notification extends BaseFactory
/** /**
* @param NotificationEntity $Notification * @param NotificationEntity $Notification
* @param bool $display_quotes Display quoted posts
* *
* @return MstdnNotification * @return MstdnNotification
* @throws UnexpectedNotificationTypeException * @throws UnexpectedNotificationTypeException
*/ */
public function createFromNotification(NotificationEntity $Notification, bool $display_quotes): MstdnNotification public function createFromNotification(NotificationEntity $Notification): MstdnNotification
{ {
$type = self::getType($Notification); $type = self::getType($Notification);
@ -50,7 +49,7 @@ class Notification extends BaseFactory
if ($Notification->targetUriId) { if ($Notification->targetUriId) {
try { try {
$status = $this->mstdnStatusFactory->createFromUriId($Notification->targetUriId, $Notification->uid, $display_quotes); $status = $this->mstdnStatusFactory->createFromUriId($Notification->targetUriId, $Notification->uid);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$status = null; $status = null;
} }

View file

@ -87,7 +87,6 @@ class Status extends BaseFactory
/** /**
* @param int $uriId Uri-ID of the item * @param int $uriId Uri-ID of the item
* @param int $uid Item user * @param int $uid Item user
* @param bool $display_quote Display quoted posts
* @param bool $reblog Check for reblogged post * @param bool $reblog Check for reblogged post
* @param bool $in_reply_status Add an "in_reply_status" element * @param bool $in_reply_status Add an "in_reply_status" element
* *
@ -95,7 +94,7 @@ class Status extends BaseFactory
* @throws InternalServerErrorException * @throws InternalServerErrorException
* @throws ImagickException|NotFoundException * @throws ImagickException|NotFoundException
*/ */
public function createFromUriId(int $uriId, int $uid = 0, bool $display_quote = false, bool $reblog = true, bool $in_reply_status = true): \Friendica\Object\Api\Mastodon\Status public function createFromUriId(int $uriId, int $uid = 0, bool $reblog = true, bool $in_reply_status = true): \Friendica\Object\Api\Mastodon\Status
{ {
$fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'author-gsid', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id', $fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', 'author-gsid', 'causer-uri-id', 'post-reason', 'starred', 'app', 'title', 'body', 'raw-body', 'content-warning', 'question-id',
'created', 'edited', 'commented', 'received', 'changed', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id', 'created', 'edited', 'commented', 'received', 'changed', 'network', 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity', 'featured', 'has-media', 'quote-uri-id',
@ -261,56 +260,12 @@ class Status extends BaseFactory
$poll = null; $poll = null;
} }
if ($display_quote) { $quote = self::createQuote($item, $uid);
$quote = self::createQuote($item, $uid);
$item['body'] = BBCode::removeSharedData($item['body']); $item['body'] = BBCode::removeSharedData($item['body']);
if (!is_null($item['raw-body'])) { if (!is_null($item['raw-body'])) {
$item['raw-body'] = BBCode::removeSharedData($item['raw-body']); $item['raw-body'] = BBCode::removeSharedData($item['raw-body']);
}
} else {
// We can always safely add attached activities. Real quotes are added to the body via "addSharedPost".
if (empty($item['quote-uri-id'])) {
$quote = self::createQuote($item, $uid);
} else {
$quote = [];
}
$shared = $this->contentItem->getSharedPost($item, ['uri-id']);
if (!empty($shared)) {
$shared_uri_id = $shared['post']['uri-id'];
foreach ($this->mstdnMentionFactory->createFromUriId($shared_uri_id)->getArrayCopy() as $mention) {
if (!in_array($mention, $mentions)) {
$mentions[] = $mention;
}
}
foreach ($this->mstdnTagFactory->createFromUriId($shared_uri_id) as $tag) {
if (!in_array($tag, $tags)) {
$tags[] = $tag;
}
}
foreach ($this->mstdnAttachmentFactory->createFromUriId($shared_uri_id) as $attachment) {
if (!in_array($attachment, $attachments)) {
$attachments[] = $attachment;
}
}
if (empty($card->toArray())) {
$card = $this->mstdnCardFactory->createFromUriId($shared_uri_id);
}
}
if (!is_null($item['raw-body'])) {
$item['raw-body'] = $this->contentItem->addSharedPost($item, $item['raw-body']);
$item['raw-body'] = Post\Media::addHTMLLinkToBody($uriId, $item['raw-body']);
} else {
$item['body'] = $this->contentItem->addSharedPost($item);
$item['body'] = Post\Media::addHTMLLinkToBody($uriId, $item['body']);
}
} }
$emojis = null; $emojis = null;
@ -330,7 +285,7 @@ class Status extends BaseFactory
if ($is_reshare) { if ($is_reshare) {
try { try {
$reshare = $this->createFromUriId($uriId, $uid, $display_quote, false, false)->toArray(); $reshare = $this->createFromUriId($uriId, $uid, false, false)->toArray();
} catch (\Exception $exception) { } catch (\Exception $exception) {
DI::logger()->info('Reshare not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]); DI::logger()->info('Reshare not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]);
$reshare = []; $reshare = [];
@ -341,7 +296,7 @@ class Status extends BaseFactory
if ($in_reply_status && ($item['gravity'] == Item::GRAVITY_COMMENT)) { if ($in_reply_status && ($item['gravity'] == Item::GRAVITY_COMMENT)) {
try { try {
$in_reply = $this->createFromUriId($item['thr-parent-id'], $uid, $display_quote, false, false)->toArray(); $in_reply = $this->createFromUriId($item['thr-parent-id'], $uid, false, false)->toArray();
} catch (\Exception $exception) { } catch (\Exception $exception) {
DI::logger()->info('Reply post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]); DI::logger()->info('Reply post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]);
$in_reply = []; $in_reply = [];
@ -382,7 +337,7 @@ class Status extends BaseFactory
if (!empty($quote_id) && ($quote_id != $item['uri-id'])) { if (!empty($quote_id) && ($quote_id != $item['uri-id'])) {
try { try {
$quoted_status = $this->createFromUriId($quote_id, $uid, false, false, false)->toArray(); $quoted_status = $this->createFromUriId($quote_id, $uid, false, false)->toArray();
$quote = [ $quote = [
'state' => 'accepted', 'state' => 'accepted',
'quoted_status' => $quoted_status, 'quoted_status' => $quoted_status,

View file

@ -15,7 +15,6 @@ use Friendica\Content\Conversation as ConversationContent;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Content\Text\HTML; use Friendica\Content\Text\HTML;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Core\Worker; use Friendica\Core\Worker;
use Friendica\Database\Database; use Friendica\Database\Database;
@ -46,15 +45,15 @@ use Friendica\Worker\UpdateContact;
*/ */
class Contact class Contact
{ {
const DEFAULT_AVATAR_PHOTO = '/images/person-300.jpg'; public const DEFAULT_AVATAR_PHOTO = '/images/person-300.jpg';
const DEFAULT_AVATAR_THUMB = '/images/person-80.jpg'; public const DEFAULT_AVATAR_THUMB = '/images/person-80.jpg';
const DEFAULT_AVATAR_MICRO = '/images/person-48.jpg'; public const DEFAULT_AVATAR_MICRO = '/images/person-48.jpg';
/** /**
* @} * @}
*/ */
const LOCK_INSERT = 'contact-insert'; public const LOCK_INSERT = 'contact-insert';
/** /**
* Account types * Account types
@ -77,12 +76,12 @@ class Contact
* This will only be assigned to contacts, not to user accounts * This will only be assigned to contacts, not to user accounts
* @{ * @{
*/ */
const TYPE_UNKNOWN = -1; public const TYPE_UNKNOWN = -1;
const TYPE_PERSON = User::ACCOUNT_TYPE_PERSON; public const TYPE_PERSON = User::ACCOUNT_TYPE_PERSON;
const TYPE_ORGANISATION = User::ACCOUNT_TYPE_ORGANISATION; public const TYPE_ORGANISATION = User::ACCOUNT_TYPE_ORGANISATION;
const TYPE_NEWS = User::ACCOUNT_TYPE_NEWS; public const TYPE_NEWS = User::ACCOUNT_TYPE_NEWS;
const TYPE_COMMUNITY = User::ACCOUNT_TYPE_COMMUNITY; public const TYPE_COMMUNITY = User::ACCOUNT_TYPE_COMMUNITY;
const TYPE_RELAY = User::ACCOUNT_TYPE_RELAY; public const TYPE_RELAY = User::ACCOUNT_TYPE_RELAY;
/** /**
* @} * @}
*/ */
@ -93,11 +92,11 @@ class Contact
* Relationship types * Relationship types
* @{ * @{
*/ */
const NOTHING = 0; // There is no relationship between the contact and the user public const NOTHING = 0; // There is no relationship between the contact and the user
const FOLLOWER = 1; // The contact is following this user (the contact is the subscriber) public const FOLLOWER = 1; // The contact is following this user (the contact is the subscriber)
const SHARING = 2; // The contact shares their content with this user (the user is the subscriber) public const SHARING = 2; // The contact shares their content with this user (the user is the subscriber)
const FRIEND = 3; // There is a mutual relationship between the contact and the user public const FRIEND = 3; // There is a mutual relationship between the contact and the user
const SELF = 4; // This is the user theirself public const SELF = 4; // This is the user theirself
/** /**
* @} * @}
*/ */
@ -187,14 +186,14 @@ class Contact
'failure_update', 'failed', 'term-date', 'last-item', 'last-discovery', 'local-data', 'failure_update', 'failed', 'term-date', 'last-item', 'last-discovery', 'local-data',
'readonly', 'contact-type', 'manually-approve', 'archive', 'unsearchable', 'sensitive', 'readonly', 'contact-type', 'manually-approve', 'archive', 'unsearchable', 'sensitive',
'baseurl', 'gsid', 'bd', 'photo', 'thumb', 'micro', 'name-date', 'uri-date', 'avatar-date', 'baseurl', 'gsid', 'bd', 'photo', 'thumb', 'micro', 'name-date', 'uri-date', 'avatar-date',
'request', 'confirm', 'poco', 'writable', 'forum', 'prv', 'bdyear' 'request', 'confirm', 'poco', 'writable', 'forum', 'prv', 'bdyear',
]; ];
$contact = self::selectFirst($fields, ['id' => $cid]); $contact = self::selectFirst($fields, ['id' => $cid]);
if (empty($contact)) { if (empty($contact)) {
return false; return false;
} }
$contact['uid'] = 0; $contact['uid'] = 0;
return (bool)self::insert($contact); return (bool) self::insert($contact);
} }
/** /**
@ -824,7 +823,7 @@ class Contact
$user = DBA::selectFirst( $user = DBA::selectFirst(
'user', 'user',
['uid', 'username', 'nickname', 'pubkey', 'prvkey'], ['uid', 'username', 'nickname', 'pubkey', 'prvkey'],
['uid' => $uid, 'account_removed' => false, 'account_expired' => false] ['uid' => $uid, 'account_removed' => false, 'account_expired' => false],
); );
if (!DBA::isResult($user)) { if (!DBA::isResult($user)) {
return false; return false;
@ -852,14 +851,14 @@ class Contact
'uri-date' => DateTimeFormat::utcNow(), 'uri-date' => DateTimeFormat::utcNow(),
'avatar-date' => DateTimeFormat::utcNow(), 'avatar-date' => DateTimeFormat::utcNow(),
'baseurl' => DI::baseUrl(), 'baseurl' => DI::baseUrl(),
'closeness' => 0 'closeness' => 0,
]; ];
$return = true; $return = true;
// Only create the entry if it doesn't exist yet // Only create the entry if it doesn't exist yet
if (!DBA::exists('contact', ['uid' => $uid, 'self' => true])) { if (!DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
$return = (bool)self::insert($contact); $return = (bool) self::insert($contact);
} }
// Create the public contact // Create the public contact
@ -887,7 +886,7 @@ class Contact
$fields = [ $fields = [
'id', 'uri-id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve', 'id', 'uri-id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve',
'xmpp', 'matrix', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable', 'xmpp', 'matrix', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable',
'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network', 'baseurl', 'gsid' 'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network', 'baseurl', 'gsid',
]; ];
$self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]); $self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
if (!DBA::isResult($self)) { if (!DBA::isResult($self)) {
@ -902,7 +901,7 @@ class Contact
$fields = [ $fields = [
'name', 'photo', 'thumb', 'about', 'address', 'locality', 'region', 'name', 'photo', 'thumb', 'about', 'address', 'locality', 'region',
'country-name', 'pub_keywords', 'xmpp', 'matrix', 'net-publish' 'country-name', 'pub_keywords', 'xmpp', 'matrix', 'net-publish',
]; ];
$profile = DBA::selectFirst('profile', $fields, ['uid' => $uid]); $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid]);
if (!DBA::isResult($profile)) { if (!DBA::isResult($profile)) {
@ -987,7 +986,7 @@ class Contact
// Update the profile // Update the profile
$fields = [ $fields = [
'photo' => User::getAvatarUrl($user), 'photo' => User::getAvatarUrl($user),
'thumb' => User::getAvatarUrl($user, Proxy::SIZE_THUMB) 'thumb' => User::getAvatarUrl($user, Proxy::SIZE_THUMB),
]; ];
DBA::update('profile', $fields, ['uid' => $uid]); DBA::update('profile', $fields, ['uid' => $uid]);
@ -1423,7 +1422,7 @@ class Contact
$fields = [ $fields = [
'name', 'nick', 'url', 'addr', 'alias', 'avatar', 'header', 'contact-type', 'name', 'nick', 'url', 'addr', 'alias', 'avatar', 'header', 'contact-type',
'keywords', 'location', 'about', 'unsearchable', 'batch', 'notify', 'poll', 'keywords', 'location', 'about', 'unsearchable', 'batch', 'notify', 'poll',
'request', 'confirm', 'poco', 'subscribe', 'network', 'baseurl', 'gsid' 'request', 'confirm', 'poco', 'subscribe', 'network', 'baseurl', 'gsid',
]; ];
$personal_contact = DBA::selectFirst('contact', $fields, ["`addr` = ? AND `uid` != 0", $url]); $personal_contact = DBA::selectFirst('contact', $fields, ["`addr` = ? AND `uid` != 0", $url]);
@ -1506,17 +1505,13 @@ class Contact
if ($data['network'] == Protocol::DIASPORA) { if ($data['network'] == Protocol::DIASPORA) {
try { try {
DI::dsprContact()->updateFromProbeArray($data); DI::dsprContact()->updateFromProbeArray($data);
} catch (NotFoundException $e) { } catch (NotFoundException|\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['url' => $url, 'data' => $data]);
} catch (\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['url' => $url, 'data' => $data]); DI::logger()->notice($e->getMessage(), ['url' => $url, 'data' => $data]);
} }
} elseif (!empty($data['networks'][Protocol::DIASPORA])) { } elseif (!empty($data['networks'][Protocol::DIASPORA])) {
try { try {
DI::dsprContact()->updateFromProbeArray($data['networks'][Protocol::DIASPORA]); DI::dsprContact()->updateFromProbeArray($data['networks'][Protocol::DIASPORA]);
} catch (NotFoundException $e) { } catch (NotFoundException|\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['url' => $url, 'data' => $data['networks'][Protocol::DIASPORA]]);
} catch (\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['url' => $url, 'data' => $data['networks'][Protocol::DIASPORA]]); DI::logger()->notice($e->getMessage(), ['url' => $url, 'data' => $data['networks'][Protocol::DIASPORA]]);
} }
} }
@ -1625,29 +1620,29 @@ class Contact
/** /**
* Returns posts from a given contact url * Returns posts from a given contact url
* *
* @param string $contact_url Contact URL * @param string $contact_url Contact URL
* @param int $uid User ID * @param int $uid User ID
* @param bool $only_media Only display media content * @param bool $only_media Only display media content
* @param string $last_created Newest creation date, used for paging * @param array $request Request variables
* @return string posts in HTML * @return string posts in HTML
* @throws Exception * @throws Exception
*/ */
public static function getPostsFromUrl(string $contact_url, int $uid, bool $only_media = false, ?string $last_created = null): string public static function getPostsFromUrl(string $contact_url, int $uid, bool $only_media = false, array $request = []): string
{ {
return self::getPostsFromId(self::getIdForURL($contact_url), $uid, $only_media, $last_created); return self::getPostsFromId(self::getIdForURL($contact_url), $uid, $only_media, $request);
} }
/** /**
* Returns posts from a given contact id * Returns posts from a given contact id
* *
* @param int $cid Contact ID * @param int $cid Contact ID
* @param int $uid User ID * @param int $uid User ID
* @param bool $only_media Only display media content * @param bool $only_media Only display media content
* @param string $last_created Newest creation date, used for paging * @param array $request Request variables
* @return string posts in HTML * @return string posts in HTML
* @throws Exception * @throws Exception
*/ */
public static function getPostsFromId(int $cid, int $uid, bool $only_media = false, ?string $last_created = null): string public static function getPostsFromId(int $cid, int $uid, bool $only_media = false, array $request = []): string
{ {
$contact = DBA::selectFirst('contact', ['contact-type', 'network', 'name', 'nick'], ['id' => $cid]); $contact = DBA::selectFirst('contact', ['contact-type', 'network', 'name', 'nick'], ['id' => $cid]);
if (!DBA::isResult($contact)) { if (!DBA::isResult($contact)) {
@ -1668,14 +1663,14 @@ class Contact
$condition = DBA::mergeConditions($condition, ["`$contact_field` = ? AND `gravity` IN (?, ?)", $cid, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]); $condition = DBA::mergeConditions($condition, ["`$contact_field` = ? AND `gravity` IN (?, ?)", $cid, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]);
if (!empty($last_created)) { if (!empty($request['last_created'])) {
$condition = DBA::mergeConditions($condition, ["`created` < ?", $last_created]); $condition = DBA::mergeConditions($condition, ["`created` < ?", $request['last_created']]);
} }
if ($only_media) { if ($only_media) {
$condition = DBA::mergeConditions($condition, [ $condition = DBA::mergeConditions($condition, [
"`uri-id` IN (SELECT `uri-id` FROM `post-media` WHERE `type` IN (?, ?, ?, ?))", "`uri-id` IN (SELECT `uri-id` FROM `post-media` WHERE `type` IN (?, ?, ?, ?))",
Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO, Post\Media::HLS Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO, Post\Media::HLS,
]); ]);
} }
@ -1688,21 +1683,13 @@ class Contact
$pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage); $pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage);
$params = ['order' => ['created' => true], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]]; $params = ['order' => ['created' => true], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
if (DI::pConfig()->get($uid, 'system', 'infinite_scroll', true)) {
$tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
$o = Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]);
} else {
$o = '';
}
$fields = array_merge(Item::DISPLAY_FIELDLIST, ['featured']); $fields = array_merge(Item::DISPLAY_FIELDLIST, ['featured']);
$items = Post::toArray(Post::selectForUser($uid, $fields, $condition, $params)); $items = Post::toArray(Post::selectForUser($uid, $fields, $condition, $params));
$o .= DI::conversation()->render($items, ConversationContent::MODE_CONTACT_POSTS); $o = DI::conversation()->render($items, ConversationContent::MODE_CONTACT_POSTS, isset($request['mode']) && ($request['mode'] == 'raw'));
if (DI::pConfig()->get($uid, 'system', 'infinite_scroll', true)) { if (DI::pConfig()->get($uid, 'system', 'infinite_scroll', true)) {
$o .= HTML::scrollLoader(); $o .= HTML::scrollLoader($request);
} else { } else {
$o .= $pager->renderMinimal(count($items)); $o .= $pager->renderMinimal(count($items));
} }
@ -1713,13 +1700,14 @@ class Contact
/** /**
* Returns threads from a given contact id * Returns threads from a given contact id
* *
* @param int $cid Contact ID * @param int $cid Contact ID
* @param int $update Update mode * @param int $update Update mode
* @param int $parent Item parent ID for the update mode * @param int $parent Item parent ID for the update mode
* @param array $request Request variables
* @return string posts in HTML * @return string posts in HTML
* @throws Exception * @throws Exception
*/ */
public static function getThreadsFromId(int $cid, int $uid, int $update = 0, int $parent = 0, string $last_created = ''): string public static function getThreadsFromId(int $cid, int $uid, int $update = 0, int $parent = 0, array $request = []): string
{ {
$contact = DBA::selectFirst('contact', ['contact-type', 'network', 'name', 'nick'], ['id' => $cid]); $contact = DBA::selectFirst('contact', ['contact-type', 'network', 'name', 'nick'], ['id' => $cid]);
if (!DBA::isResult($contact)) { if (!DBA::isResult($contact)) {
@ -1738,8 +1726,8 @@ class Contact
if (!empty($parent)) { if (!empty($parent)) {
$condition = DBA::mergeConditions($condition, ['parent' => $parent]); $condition = DBA::mergeConditions($condition, ['parent' => $parent]);
} elseif (!empty($last_created)) { } elseif (isset($request['last_created'])) {
$condition = DBA::mergeConditions($condition, ["`created` < ?", $last_created]); $condition = DBA::mergeConditions($condition, ["`created` < ?", $request['last_created']]);
} }
$contact_field = ((($contact["contact-type"] == self::TYPE_COMMUNITY) || ($contact['network'] == Protocol::MAIL)) ? 'owner-id' : 'author-id'); $contact_field = ((($contact["contact-type"] == self::TYPE_COMMUNITY) || ($contact['network'] == Protocol::MAIL)) ? 'owner-id' : 'author-id');
@ -1752,18 +1740,11 @@ class Contact
$pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage); $pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage);
if (DI::pConfig()->get($uid, 'system', 'infinite_scroll', true)) {
$tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
$o = Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]);
} else {
$o = '';
}
$condition1 = DBA::mergeConditions($condition, ["`$contact_field` = ? AND `gravity` = ?", $cid, Item::GRAVITY_PARENT]); $condition1 = DBA::mergeConditions($condition, ["`$contact_field` = ? AND `gravity` = ?", $cid, Item::GRAVITY_PARENT]);
$condition2 = DBA::mergeConditions($condition, [ $condition2 = DBA::mergeConditions($condition, [
"`author-id` = ? AND `gravity` = ? AND `vid` = ? AND `protocol` != ? AND `thr-parent-id` = `parent-uri-id`", "`author-id` = ? AND `gravity` = ? AND `vid` = ? AND `protocol` != ? AND `thr-parent-id` = `parent-uri-id`",
$cid, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), Conversation::PARCEL_DIASPORA $cid, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), Conversation::PARCEL_DIASPORA,
]); ]);
$sql1 = "SELECT `uri-id`, `created` FROM `post-thread-user-view` WHERE " . array_shift($condition1); $sql1 = "SELECT `uri-id`, `created` FROM `post-thread-user-view` WHERE " . array_shift($condition1);
@ -1776,17 +1757,19 @@ class Contact
$union = array_merge($union, [$pager->getStart(), $pager->getItemsPerPage()]); $union = array_merge($union, [$pager->getStart(), $pager->getItemsPerPage()]);
$items = Post::toArray(DBA::p($sql, $union)); $items = Post::toArray(DBA::p($sql, $union));
if (empty($last_created) && ($pager->getStart() == 0)) { $raw = isset($request['mode']) && ($request['mode'] == 'raw');
if (!$raw && !$update && ($pager->getStart() == 0)) {
$fields = ['uri-id', 'thr-parent-id', 'gravity', 'author-id', 'created']; $fields = ['uri-id', 'thr-parent-id', 'gravity', 'author-id', 'created'];
$pinned = Post\Collection::selectToArrayForContact($cid, Post\Collection::FEATURED, $fields); $pinned = Post\Collection::selectToArrayForContact($cid, Post\Collection::FEATURED, $fields);
$items = array_merge($items, $pinned); $items = array_merge($items, $pinned);
} }
$o .= DI::conversation()->render($items, ConversationContent::MODE_CONTACTS, $update, false, 'pinned_created', $uid); $o = DI::conversation()->render($items, ConversationContent::MODE_CONTACTS, $update || $raw, false, 'pinned_created', $uid);
if (!$update) { if (!$update) {
if (DI::pConfig()->get($uid, 'system', 'infinite_scroll', true)) { if (DI::pConfig()->get($uid, 'system', 'infinite_scroll', true)) {
$o .= HTML::scrollLoader(); $o .= HTML::scrollLoader($request);
} else { } else {
$o .= $pager->renderMinimal(count($items)); $o .= $pager->renderMinimal(count($items));
} }
@ -2288,7 +2271,7 @@ class Contact
{ {
$condition = [ $condition = [
"`nurl` = ? AND ((`uid` = ? AND `network` IN (?, ?)) OR `uid` = ?)", "`nurl` = ? AND ((`uid` = ? AND `network` IN (?, ?)) OR `uid` = ?)",
Strings::normaliseLink($url), $uid, Protocol::FEED, Protocol::MAIL, 0 Strings::normaliseLink($url), $uid, Protocol::FEED, Protocol::MAIL, 0,
]; ];
$contact = self::selectFirst(['id', 'updated'], $condition, ['order' => ['uid' => true]]); $contact = self::selectFirst(['id', 'updated'], $condition, ['order' => ['uid' => true]]);
return self::getAvatarUrlForId($contact['id'] ?? 0, $size, $contact['updated'] ?? ''); return self::getAvatarUrlForId($contact['id'] ?? 0, $size, $contact['updated'] ?? '');
@ -2363,7 +2346,7 @@ class Contact
$contact = DBA::selectFirst( $contact = DBA::selectFirst(
'contact', 'contact',
['uid', 'avatar', 'photo', 'thumb', 'micro', 'blurhash', 'xmpp', 'addr', 'nurl', 'url', 'network', 'uri-id'], ['uid', 'avatar', 'photo', 'thumb', 'micro', 'blurhash', 'xmpp', 'addr', 'nurl', 'url', 'network', 'uri-id'],
['id' => $cid, 'self' => false] ['id' => $cid, 'self' => false],
); );
if (!DBA::isResult($contact)) { if (!DBA::isResult($contact)) {
return; return;
@ -2445,7 +2428,7 @@ class Contact
'avatar-date' => DateTimeFormat::utcNow(), 'avatar-date' => DateTimeFormat::utcNow(),
'photo' => $avatar, 'photo' => $avatar,
'thumb' => self::getDefaultAvatar($contact, Proxy::SIZE_THUMB), 'thumb' => self::getDefaultAvatar($contact, Proxy::SIZE_THUMB),
'micro' => self::getDefaultAvatar($contact, Proxy::SIZE_MICRO) 'micro' => self::getDefaultAvatar($contact, Proxy::SIZE_MICRO),
]; ];
DI::logger()->debug('Use default avatar', ['id' => $cid, 'uid' => $uid]); DI::logger()->debug('Use default avatar', ['id' => $cid, 'uid' => $uid]);
} }
@ -2517,7 +2500,7 @@ class Contact
$personal_contacts = DBA::select( $personal_contacts = DBA::select(
'contact', 'contact',
['id', 'uid'], ['id', 'uid'],
["`nurl` = ? AND `id` != ? AND NOT `self`", $contact['nurl'], $cid] ["`nurl` = ? AND `id` != ? AND NOT `self`", $contact['nurl'], $cid],
); );
while ($personal_contact = DBA::fetch($personal_contacts)) { while ($personal_contact = DBA::fetch($personal_contacts)) {
$cids[] = $personal_contact['id']; $cids[] = $personal_contact['id'];
@ -2714,9 +2697,9 @@ class Contact
return true; return true;
} }
$stamp = (float)microtime(true); $stamp = (float) microtime(true);
self::updateFromProbe($id); self::updateFromProbe($id);
DI::logger()->debug('Contact data is updated.', ['duration' => round((float)microtime(true) - $stamp, 3), 'id' => $id, 'url' => $contact['url']]); DI::logger()->debug('Contact data is updated.', ['duration' => round((float) microtime(true) - $stamp, 3), 'id' => $id, 'url' => $contact['url']]);
return true; return true;
} }
@ -2732,7 +2715,7 @@ class Contact
if (!empty($id)) { if (!empty($id)) {
return self::updateByIdIfNeeded($id); return self::updateByIdIfNeeded($id);
} }
return (bool)self::getIdForURL($url); return (bool) self::getIdForURL($url);
} }
/** /**
@ -2764,17 +2747,13 @@ class Contact
if ($data['network'] == Protocol::DIASPORA) { if ($data['network'] == Protocol::DIASPORA) {
try { try {
DI::dsprContact()->updateFromProbeArray($data); DI::dsprContact()->updateFromProbeArray($data);
} catch (NotFoundException $e) { } catch (NotFoundException|\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
} catch (\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]); DI::logger()->notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
} }
} elseif (!empty($data['networks'][Protocol::DIASPORA])) { } elseif (!empty($data['networks'][Protocol::DIASPORA])) {
try { try {
DI::dsprContact()->updateFromProbeArray($data['networks'][Protocol::DIASPORA]); DI::dsprContact()->updateFromProbeArray($data['networks'][Protocol::DIASPORA]);
} catch (NotFoundException $e) { } catch (NotFoundException|\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
} catch (\InvalidArgumentException $e) {
DI::logger()->notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]); DI::logger()->notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
} }
} }
@ -2845,7 +2824,7 @@ class Contact
'uid', 'uri-id', 'avatar', 'header', 'name', 'nick', 'location', 'keywords', 'about', 'subscribe', 'uid', 'uri-id', 'avatar', 'header', 'name', 'nick', 'location', 'keywords', 'about', 'subscribe',
'manually-approve', 'unsearchable', 'url', 'addr', 'batch', 'notify', 'poll', 'request', 'confirm', 'poco', 'manually-approve', 'unsearchable', 'url', 'addr', 'batch', 'notify', 'poll', 'request', 'confirm', 'poco',
'network', 'alias', 'baseurl', 'gsid', 'forum', 'prv', 'contact-type', 'pubkey', 'last-item', 'xmpp', 'matrix', 'network', 'alias', 'baseurl', 'gsid', 'forum', 'prv', 'contact-type', 'pubkey', 'last-item', 'xmpp', 'matrix',
'created', 'last-update' 'created', 'last-update',
]; ];
/** @var array<string,mixed> */ /** @var array<string,mixed> */
@ -2919,8 +2898,8 @@ class Contact
// We must not try to update relay contacts via probe. They are no real contacts. // We must not try to update relay contacts via probe. They are no real contacts.
// See Relay::updateContact() for more details. // See Relay::updateContact() for more details.
// We check after the probing to be able to correct falsely detected contact types. // We check after the probing to be able to correct falsely detected contact types.
if (($contact['contact-type'] == self::TYPE_RELAY) && Strings::compareLink($contact['url'], $contact['baseurl'] ?? '') && if (($contact['contact-type'] == self::TYPE_RELAY) && Strings::compareLink($contact['url'], $contact['baseurl'] ?? '')
(!Strings::compareLink($ret['url'], $contact['url']) || in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM])) && (!Strings::compareLink($ret['url'], $contact['url']) || in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM]))
) { ) {
if (GServer::reachable($contact)) { if (GServer::reachable($contact)) {
self::updateContact($id, $uid, $uriid, $contact['url'], ['failed' => false, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $success_next_update, 'success_update' => $updated, 'unsearchable' => true]); self::updateContact($id, $uid, $uriid, $contact['url'], ['failed' => false, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $success_next_update, 'success_update' => $updated, 'unsearchable' => true]);
@ -2955,8 +2934,8 @@ class Contact
$ret['prv'] = false; $ret['prv'] = false;
$ret['contact-type'] = $ret['account-type']; $ret['contact-type'] = $ret['account-type'];
if (($ret['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY) && isset($ret['manually-approve'])) { if (($ret['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY) && isset($ret['manually-approve'])) {
$ret['forum'] = (bool)!$ret['manually-approve']; $ret['forum'] = (bool) !$ret['manually-approve'];
$ret['prv'] = (bool)!$ret['forum']; $ret['prv'] = (bool) !$ret['forum'];
} }
} }
@ -3239,7 +3218,7 @@ class Contact
$pending = false; $pending = false;
if (($protocol == Protocol::ACTIVITYPUB) && isset($ret['manually-approve'])) { if (($protocol == Protocol::ACTIVITYPUB) && isset($ret['manually-approve'])) {
$pending = (bool)$ret['manually-approve']; $pending = (bool) $ret['manually-approve'];
} }
$writeable = in_array($protocol, [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB]); $writeable = in_array($protocol, [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB]);
@ -3374,8 +3353,8 @@ class Contact
// Contact is blocked at user-level // Contact is blocked at user-level
if ( if (
!empty($contact['id']) && !empty($importer['id']) && !empty($contact['id']) && !empty($importer['id'])
Contact\User::isBlocked($contact['id'], $importer['id']) && Contact\User::isBlocked($contact['id'], $importer['id'])
) { ) {
return false; return false;
} }
@ -3388,7 +3367,7 @@ class Contact
) { ) {
self::update( self::update(
['rel' => self::FRIEND, 'writable' => true, 'pending' => false], ['rel' => self::FRIEND, 'writable' => true, 'pending' => false],
['id' => $contact['id'], 'uid' => $importer['uid']] ['id' => $contact['id'], 'uid' => $importer['uid']],
); );
} }
@ -3450,7 +3429,7 @@ class Contact
$intro = DI::introFactory()->createNew( $intro = DI::introFactory()->createNew(
$importer['uid'], $importer['uid'],
$contact_record['id'], $contact_record['id'],
$note $note,
); );
DI::intro()->save($intro); DI::intro()->save($intro);
} }
@ -3567,7 +3546,7 @@ class Contact
AND NOT `contact`.`deleted`', AND NOT `contact`.`deleted`',
DBA::NULL_DATE, DBA::NULL_DATE,
self::SHARING, self::SHARING,
self::FRIEND self::FRIEND,
]; ];
$contacts = DBA::select('contact', ['id', 'uid', 'name', 'url', 'bd'], $condition); $contacts = DBA::select('contact', ['id', 'uid', 'name', 'url', 'bd'], $condition);
@ -3582,7 +3561,7 @@ class Contact
DBA::update( DBA::update(
'contact', 'contact',
['bdyear' => substr($nextbd, 0, 4), 'bd' => $nextbd], ['bdyear' => substr($nextbd, 0, 4), 'bd' => $nextbd],
['id' => $contact['id']] ['id' => $contact['id']],
); );
} }
} }
@ -3794,7 +3773,7 @@ class Contact
'failed' => false, 'failed' => false,
'deleted' => false, 'deleted' => false,
'unsearchable' => false, 'unsearchable' => false,
'uid' => $uid 'uid' => $uid,
]; ];
if (!$show_blocked) { if (!$show_blocked) {
@ -3824,7 +3803,7 @@ class Contact
$condition = DBA::mergeConditions( $condition = DBA::mergeConditions(
$condition, $condition,
["(`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?)", $search, $search, $search] ["(`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?)", $search, $search, $search],
); );
return DBA::selectToArray('account-user-view', [], $condition, $params); return DBA::selectToArray('account-user-view', [], $condition, $params);

View file

@ -829,27 +829,23 @@ class Relation
DI::logger()->debug('Calculation - start', ['uid' => $uid, 'cid' => $contact_id, 'days' => $days]); DI::logger()->debug('Calculation - start', ['uid' => $uid, 'cid' => $contact_id, 'days' => $days]);
$follow = Verb::getID(Activity::FOLLOW); $follow = Verb::getID(Activity::FOLLOW);
$view = Verb::getID(Activity::VIEW);
$read = Verb::getID(Activity::READ);
DBA::update('contact-relation', ['score' => 0, 'relation-score' => 0, 'thread-score' => 0, 'relation-thread-score' => 0], ['relation-cid' => $contact_id]); DBA::update('contact-relation', ['score' => 0, 'relation-score' => 0, 'thread-score' => 0, 'relation-thread-score' => 0], ['relation-cid' => $contact_id]);
$total = DBA::fetchFirst( $total = DBA::fetchFirst(
"SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)", "SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ?",
$contact_id, $contact_id,
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
DI::logger()->debug('Calculate relation-score', ['uid' => $uid, 'total' => $total['activity']]); DI::logger()->debug('Calculate relation-score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p( $interactions = DBA::p(
"SELECT `post`.`author-id`, count(*) AS `activity`, EXISTS(SELECT `pid` FROM `account-user-view` WHERE `pid` = `post`.`author-id` AND `uid` = ? AND `rel` IN (?, ?)) AS `follows` "SELECT `post`.`author-id`, count(*) AS `activity`, EXISTS(SELECT `pid` FROM `account-user-view` WHERE `pid` = `post`.`author-id` AND `uid` = ? AND `rel` IN (?, ?)) AS `follows`
FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`", FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ? GROUP BY `post`.`author-id`",
$uid, $uid,
Contact::SHARING, Contact::SHARING,
Contact::FRIEND, Contact::FRIEND,
@ -857,9 +853,7 @@ class Relation
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
while ($interaction = DBA::fetch($interactions)) { while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535); $score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
@ -868,21 +862,19 @@ class Relation
DBA::close($interactions); DBA::close($interactions);
$total = DBA::fetchFirst( $total = DBA::fetchFirst(
"SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)", "SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ?",
$contact_id, $contact_id,
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
DI::logger()->debug('Calculate relation-thread-score', ['uid' => $uid, 'total' => $total['activity']]); DI::logger()->debug('Calculate relation-thread-score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p( $interactions = DBA::p(
"SELECT `post`.`author-id`, count(*) AS `activity`, EXISTS(SELECT `pid` FROM `account-user-view` WHERE `pid` = `post`.`author-id` AND `uid` = ? AND `rel` IN (?, ?)) AS `follows` "SELECT `post`.`author-id`, count(*) AS `activity`, EXISTS(SELECT `pid` FROM `account-user-view` WHERE `pid` = `post`.`author-id` AND `uid` = ? AND `rel` IN (?, ?)) AS `follows`
FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`", FROM `post-user` INNER JOIN `post` ON `post`.`uri-id` = `post-user`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ? GROUP BY `post`.`author-id`",
$uid, $uid,
Contact::SHARING, Contact::SHARING,
Contact::FRIEND, Contact::FRIEND,
@ -890,9 +882,7 @@ class Relation
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
while ($interaction = DBA::fetch($interactions)) { while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535); $score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
@ -901,27 +891,23 @@ class Relation
DBA::close($interactions); DBA::close($interactions);
$total = DBA::fetchFirst( $total = DBA::fetchFirst(
"SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)", "SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ?",
$contact_id, $contact_id,
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
DI::logger()->debug('Calculate score', ['uid' => $uid, 'total' => $total['activity']]); DI::logger()->debug('Calculate score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p( $interactions = DBA::p(
"SELECT `post`.`author-id`, count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`", "SELECT `post`.`author-id`, count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`thr-parent-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ? GROUP BY `post`.`author-id`",
$contact_id, $contact_id,
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
while ($interaction = DBA::fetch($interactions)) { while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535); $score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);
@ -930,27 +916,23 @@ class Relation
DBA::close($interactions); DBA::close($interactions);
$total = DBA::fetchFirst( $total = DBA::fetchFirst(
"SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?)", "SELECT count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ?",
$contact_id, $contact_id,
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
DI::logger()->debug('Calculate thread-score', ['uid' => $uid, 'total' => $total['activity']]); DI::logger()->debug('Calculate thread-score', ['uid' => $uid, 'total' => $total['activity']]);
$interactions = DBA::p( $interactions = DBA::p(
"SELECT `post`.`author-id`, count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND NOT `post`.`vid` IN (?, ?, ?) GROUP BY `post`.`author-id`", "SELECT `post`.`author-id`, count(*) AS `activity` FROM `post-user` INNER JOIN `post` ON `post-user`.`uri-id` = `post`.`parent-uri-id` WHERE `post-user`.`author-id` = ? AND `post-user`.`received` >= ? AND `post-user`.`uid` = ? AND `post`.`author-id` != ? AND `post`.`vid` != ? GROUP BY `post`.`author-id`",
$contact_id, $contact_id,
DateTimeFormat::utc('now - ' . $days . ' day'), DateTimeFormat::utc('now - ' . $days . ' day'),
$uid, $uid,
$contact_id, $contact_id,
$follow, $follow
$view,
$read
); );
while ($interaction = DBA::fetch($interactions)) { while ($interaction = DBA::fetch($interactions)) {
$score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535); $score = min((int)(($interaction['activity'] / $total['activity']) * 65535), 65535);

View file

@ -23,6 +23,7 @@ use Friendica\Util\Map;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\Temporal; use Friendica\Util\Temporal;
use Friendica\Util\XML; use Friendica\Util\XML;
use IntlDateFormatter;
/** /**
* functions for interacting with the event database table * functions for interacting with the event database table
@ -37,12 +38,10 @@ class Event
$uriid = $event['uri-id'] ?? $uriid; $uriid = $event['uri-id'] ?? $uriid;
$bd_format = DI::l10n()->t('l F d, Y \@ g:i A \G\M\TP (e)'); // Friday October 29, 2021 @ 9:15 AM GMT-04:00 (America/New_York) $event_start = DI::l10n()->formatDateTime($event['start'], IntlDateFormatter::FULL, IntlDateFormatter::LONG);
$event_start = DI::l10n()->getDay(DateTimeFormat::local($event['start'], $bd_format));
if (!empty($event['finish'])) { if (!empty($event['finish'])) {
$event_end = DI::l10n()->getDay(DateTimeFormat::local($event['finish'], $bd_format)); $event_end = DI::l10n()->formatDateTime($event['finish'], IntlDateFormatter::FULL, IntlDateFormatter::LONG);
} else { } else {
$event_end = ''; $event_end = '';
} }
@ -459,7 +458,7 @@ class Event
'dtstart_label' => DI::l10n()->t('Starts:'), 'dtstart_label' => DI::l10n()->t('Starts:'),
'dtend_label' => DI::l10n()->t('Finishes:'), 'dtend_label' => DI::l10n()->t('Finishes:'),
'location_label' => DI::l10n()->t('Location:') 'location_label' => DI::l10n()->t('Location:'),
]; ];
} }
@ -542,7 +541,7 @@ class Event
AND `event`.`uid` = ? AND `event`.`uid` = ?
$sql_perms", $sql_perms",
$event_id, $event_id,
$owner_uid $owner_uid,
)); ));
if (empty($events)) { if (empty($events)) {
throw new NotFoundException(DI::l10n()->t('Event not found.')); throw new NotFoundException(DI::l10n()->t('Event not found.'));
@ -607,7 +606,7 @@ class Event
$owner_uid, $owner_uid,
$start, $start,
$start, $start,
$finish $finish,
)); ));
$events = self::removeDuplicates($events); $events = self::removeDuplicates($events);
@ -636,8 +635,7 @@ class Event
$start = DateTimeFormat::local($event['start'], 'c'); $start = DateTimeFormat::local($event['start'], 'c');
$j = DateTimeFormat::local($event['start'], 'j'); $j = DateTimeFormat::local($event['start'], 'j');
$day = DateTimeFormat::local($event['start'], $fmt); $day = DI::l10n()->fullDate($event['start']);
$day = DI::l10n()->getDay($day);
if ($event['nofinish']) { if ($event['nofinish']) {
$end = null; $end = null;
@ -658,7 +656,7 @@ class Event
$title = strip_tags(BBCode::convertForUriId($event['uri-id'], $event['summary'])); $title = strip_tags(BBCode::convertForUriId($event['uri-id'], $event['summary']));
if (!$title) { if (!$title) {
list($title, $_trash) = explode("<br", BBCode::convertForUriId($event['uri-id'], Strings::escapeHtml($event['desc'])), BBCode::TWITTER_API); [$title, $_trash] = explode("<br", BBCode::convertForUriId($event['uri-id'], Strings::escapeHtml($event['desc'])), BBCode::TWITTER_API);
} }
$event['author-link'] = Contact::magicLink($event['author-link']); $event['author-link'] = Contact::magicLink($event['author-link']);
@ -710,11 +708,11 @@ class Event
$time_format = "%H:%M:%S"; $time_format = "%H:%M:%S";
$date_format = "%Y-%m-%d"; $date_format = "%Y-%m-%d";
$o .= '"' . $event['summary'] . '", "' . strftime($date_format, $tmp1) . $o .= '"' . $event['summary'] . '", "' . strftime($date_format, $tmp1)
'", "' . strftime($time_format, $tmp1) . '", "' . $event['desc'] . . '", "' . strftime($time_format, $tmp1) . '", "' . $event['desc']
'", "' . strftime($date_format, $tmp2) . . '", "' . strftime($date_format, $tmp2)
'", "' . strftime($time_format, $tmp2) . . '", "' . strftime($time_format, $tmp2)
'", "' . $event['location'] . '"' . PHP_EOL; . '", "' . $event['location'] . '"' . PHP_EOL;
} }
break; break;
@ -882,30 +880,26 @@ class Event
$same_date = false; $same_date = false;
$finish = false; $finish = false;
// Set the different time formats.
$dformat = DI::l10n()->t('l F d, Y \@ g:i A'); // Friday January 18, 2011 @ 8:01 AM.
$dformat_short = DI::l10n()->t('D g:i A'); // Fri 8:01 AM.
$tformat = DI::l10n()->t('g:i A'); // 8:01 AM.
$tzformat = DI::l10n()->t('e'); // Atlantic/Azores.
// Convert the time to different formats. // Convert the time to different formats.
$dtstart_dt = DI::l10n()->getDay(DateTimeFormat::local($item['event-start'], $dformat)); $dtstart_dt = DI::l10n()->fullDateTime($item['event-start']);
$dtstart_title = DateTimeFormat::utc($item['event-start'], DateTimeFormat::ATOM); $dtstart_title = DateTimeFormat::utc($item['event-start'], DateTimeFormat::ATOM);
// Format: Jan till Dec. // Format: Jan till Dec.
$month_short = DI::l10n()->getDayShort(DateTimeFormat::local($item['event-start'], 'M')); $month_short = DI::l10n()->formatDateTimeByPattern($item['event-start'], 'MMM');
// Format: 1 till 31. // Format: 1 till 31.
$date_short = DateTimeFormat::local($item['event-start'], 'j'); $date_short = DI::l10n()->formatDateTimeByPattern($item['event-start'], 'd');
$start_time = DateTimeFormat::local($item['event-start'], $tformat); $start_time = DI::l10n()->formatDateTime($item['event-start'], IntlDateFormatter::NONE, IntlDateFormatter::SHORT);
$start_short = DI::l10n()->getDayShort(DateTimeFormat::local($item['event-start'], $dformat_short)); $start_day = DI::l10n()->formatDateTimeByPattern($item['event-start'], 'eeee');
$timezone = DateTimeFormat::local($item['event-start'], $tzformat); $start_short = $start_day . ' ' . $start_time;
$timezone = DI::l10n()->formatDateTimeByPattern($item['event-start'], 'vvvv');
// If the option 'nofinisch' isn't set, we need to format the finish date/time. // If the option 'nofinisch' isn't set, we need to format the finish date/time.
if (!$item['event-nofinish']) { if (!$item['event-nofinish']) {
$finish = true; $finish = true;
$dtend_dt = DI::l10n()->getDay(DateTimeFormat::local($item['event-finish'], $dformat)); $dtend_dt = DI::l10n()->fullDateTime($item['event-finish']);
$dtend_title = DateTimeFormat::utc($item['event-finish'], DateTimeFormat::ATOM); $dtend_title = DateTimeFormat::utc($item['event-finish'], DateTimeFormat::ATOM);
$end_short = DI::l10n()->getDayShort(DateTimeFormat::utc($item['event-finish'], $dformat_short)); $end_time = DI::l10n()->formatDateTime($item['event-finish'], IntlDateFormatter::NONE, IntlDateFormatter::SHORT);
$end_time = DateTimeFormat::local($item['event-finish'], $tformat); $end_day = DI::l10n()->formatDateTimeByPattern($item['event-finish'], 'eeee');
$end_short = $end_day . ' ' . $end_time;
// Check if start and finish time is at the same day. // Check if start and finish time is at the same day.
if (substr($dtstart_title, 0, 10) === substr($dtend_title, 0, 10)) { if (substr($dtstart_title, 0, 10) === substr($dtend_title, 0, 10)) {
$same_date = true; $same_date = true;
@ -923,7 +917,7 @@ class Event
'id' => $item['author-id'], 'id' => $item['author-id'],
'network' => $item['author-network'], 'network' => $item['author-network'],
'url' => $item['author-link'], 'url' => $item['author-link'],
'alias' => $item['author-alias'] 'alias' => $item['author-alias'],
]; ];
$profile_link = Contact::magicLinkByContact($author); $profile_link = Contact::magicLinkByContact($author);
@ -1027,7 +1021,7 @@ class Event
'uid' => $contact['uid'], 'uid' => $contact['uid'],
'cid' => $contact['id'], 'cid' => $contact['id'],
'start' => DateTimeFormat::utc($birthday), 'start' => DateTimeFormat::utc($birthday),
'type' => 'birthday' 'type' => 'birthday',
]; ];
if (DBA::exists('event', $condition)) { if (DBA::exists('event', $condition)) {
return false; return false;

View file

@ -29,6 +29,7 @@ use Friendica\Util\Network;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\XML; use Friendica\Util\XML;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Network\RobotsTxt;
use Friendica\Worker\UpdateGServer; use Friendica\Worker\UpdateGServer;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
@ -39,44 +40,46 @@ use Psr\Http\Message\UriInterface;
class GServer class GServer
{ {
// Directory types // Directory types
const DT_NONE = 0; public const DT_NONE = 0;
const DT_POCO = 1; public const DT_POCO = 1;
const DT_MASTODON = 2; public const DT_MASTODON = 2;
// Methods to detect server types // Methods to detect server types
// Non endpoint specific methods // Non endpoint specific methods
const DETECT_MANUAL = 0; public const DETECT_MANUAL = 0;
const DETECT_HEADER = 1; public const DETECT_HEADER = 1;
const DETECT_BODY = 2; public const DETECT_BODY = 2;
const DETECT_HOST_META = 3; public const DETECT_HOST_META = 3;
const DETECT_CONTACTS = 4; public const DETECT_CONTACTS = 4;
const DETECT_AP_ACTOR = 5; public const DETECT_AP_ACTOR = 5;
const DETECT_AP_COLLECTION = 6; public const DETECT_AP_COLLECTION = 6;
const DETECT_UNSPECIFIC = [self::DETECT_MANUAL, self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_HOST_META, self::DETECT_CONTACTS, self::DETECT_AP_ACTOR]; public const DETECT_UNSPECIFIC = [self::DETECT_MANUAL, self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_HOST_META, self::DETECT_CONTACTS, self::DETECT_AP_ACTOR];
// Implementation specific endpoints // Implementation specific endpoints
// @todo Possibly add Lemmy detection via the endpoint /api/v3/site // @todo Possibly add Lemmy detection via the endpoint /api/v3/site
const DETECT_FRIENDIKA = 10; public const DETECT_FRIENDIKA = 10;
const DETECT_FRIENDICA = 11; public const DETECT_FRIENDICA = 11;
const DETECT_STATUSNET = 12; public const DETECT_STATUSNET = 12;
const DETECT_GNUSOCIAL = 13; public const DETECT_GNUSOCIAL = 13;
const DETECT_CONFIG_JSON = 14; // Statusnet, GNU Social, Older Hubzilla/Redmatrix public const DETECT_CONFIG_JSON = 14; // Statusnet, GNU Social, Older Hubzilla/Redmatrix
const DETECT_SITEINFO_JSON = 15; // Newer Hubzilla public const DETECT_SITEINFO_JSON = 15; // Newer Hubzilla
const DETECT_MASTODON_API = 16; public const DETECT_MASTODON_API = 16;
const DETECT_STATUS_PHP = 17; // Nextcloud public const DETECT_STATUS_PHP = 17; // Nextcloud
const DETECT_V1_CONFIG = 18; public const DETECT_V1_CONFIG = 18;
const DETECT_SYSTEM_ACTOR = 20; // Mistpark, Osada, Roadhouse, Zap public const DETECT_SYSTEM_ACTOR = 20; // Mistpark, Osada, Roadhouse, Zap
const DETECT_THREADS = 21; public const DETECT_THREADS = 21;
// Standardized endpoints // Standardized endpoints
const DETECT_STATISTICS_JSON = 100; public const DETECT_STATISTICS_JSON = 100;
const DETECT_NODEINFO_10 = 101; // Nodeinfo Version 1.0 public const DETECT_NODEINFO_10 = 101; // Nodeinfo Version 1.0
const DETECT_NODEINFO_20 = 102; // Nodeinfo Version 2.0 public const DETECT_NODEINFO_20 = 102; // Nodeinfo Version 2.0
const DETECT_NODEINFO2_10 = 103; // Nodeinfo2 Version 1.0 public const DETECT_NODEINFO2_10 = 103; // Nodeinfo2 Version 1.0
const DETECT_NODEINFO_21 = 104; // Nodeinfo Version 2.1 public const DETECT_NODEINFO_21 = 104; // Nodeinfo Version 2.1
const DETECT_NODEINFO_22 = 105; // Nodeinfo Version 2.2 public const DETECT_NODEINFO_22 = 105; // Nodeinfo Version 2.2
private static RobotsTxt $robotsTxt;
/** /**
* Check for the existence of a server and adds it in the background if not existant * Check for the existence of a server and adds it in the background if not existant
@ -191,10 +194,10 @@ class GServer
*/ */
private static function isDefunct(array $gserver): bool private static function isDefunct(array $gserver): bool
{ {
return ($gserver['failed'] || in_array($gserver['network'], Protocol::FEDERATED)) && return ($gserver['failed'] || in_array($gserver['network'], Protocol::FEDERATED))
($gserver['last_contact'] >= $gserver['created']) && && ($gserver['last_contact'] >= $gserver['created'])
($gserver['last_contact'] < $gserver['last_failure']) && && ($gserver['last_contact'] < $gserver['last_failure'])
($gserver['last_contact'] < DateTimeFormat::utc('now - 90 days')); && ($gserver['last_contact'] < DateTimeFormat::utc('now - 90 days'));
} }
/** /**
@ -253,9 +256,9 @@ class GServer
} elseif (!empty($contact['baseurl'])) { } elseif (!empty($contact['baseurl'])) {
$server = $contact['baseurl']; $server = $contact['baseurl'];
} elseif ($contact['network'] == Protocol::DIASPORA) { } elseif ($contact['network'] == Protocol::DIASPORA) {
$parts = (array)parse_url($contact['url']); $parts = (array) parse_url($contact['url']);
unset($parts['path']); unset($parts['path']);
$server = (string)Uri::fromParts($parts); $server = (string) Uri::fromParts($parts);
} else { } else {
return true; return true;
} }
@ -479,7 +482,7 @@ class GServer
self::update( self::update(
['url' => $url, 'failed' => true, 'blocked' => Network::isUrlBlocked($url), 'last_failure' => DateTimeFormat::utcNow(), ['url' => $url, 'failed' => true, 'blocked' => Network::isUrlBlocked($url), 'last_failure' => DateTimeFormat::utcNow(),
'next_contact' => $next_update, 'network' => Protocol::PHANTOM, 'detection-method' => null], 'next_contact' => $next_update, 'network' => Protocol::PHANTOM, 'detection-method' => null],
['nurl' => $nurl] ['nurl' => $nurl],
); );
DI::logger()->info('Set failed status for existing server', ['url' => $url]); DI::logger()->info('Set failed status for existing server', ['url' => $url]);
if (self::isDefunct($gserver)) { if (self::isDefunct($gserver)) {
@ -518,7 +521,7 @@ class GServer
public static function cleanURL(string $dirtyUrl): string public static function cleanURL(string $dirtyUrl): string
{ {
try { try {
return (string)self::cleanUri(new Uri($dirtyUrl)); return (string) self::cleanUri(new Uri($dirtyUrl));
} catch (\Throwable $e) { } catch (\Throwable $e) {
DI::logger()->warning('Invalid URL', ['dirtyUrl' => $dirtyUrl]); DI::logger()->warning('Invalid URL', ['dirtyUrl' => $dirtyUrl]);
return ''; return '';
@ -543,8 +546,8 @@ class GServer
preg_replace( preg_replace(
'#(?:^|/)index\.php#', '#(?:^|/)index\.php#',
'', '',
rtrim($dirtyUri->getPath(), '/') rtrim($dirtyUri->getPath(), '/'),
) ),
); );
} }
@ -591,8 +594,8 @@ class GServer
if (!Strings::compareLink($url, $valid_url)) { if (!Strings::compareLink($url, $valid_url)) {
// We only follow redirects when the path stays the same or the target url has no path. // We only follow redirects when the path stays the same or the target url has no path.
// Some systems have got redirects on their landing page to a single account page. This check handles it. // Some systems have got redirects on their landing page to a single account page. This check handles it.
if (((parse_url($url, PHP_URL_HOST) != parse_url($valid_url, PHP_URL_HOST)) && (parse_url($url, PHP_URL_PATH) == parse_url($valid_url, PHP_URL_PATH))) || if (((parse_url($url, PHP_URL_HOST) != parse_url($valid_url, PHP_URL_HOST)) && (parse_url($url, PHP_URL_PATH) == parse_url($valid_url, PHP_URL_PATH)))
(((parse_url($url, PHP_URL_HOST) != parse_url($valid_url, PHP_URL_HOST)) || (parse_url($url, PHP_URL_PATH) != parse_url($valid_url, PHP_URL_PATH))) && empty(parse_url($valid_url, PHP_URL_PATH)))) { || (((parse_url($url, PHP_URL_HOST) != parse_url($valid_url, PHP_URL_HOST)) || (parse_url($url, PHP_URL_PATH) != parse_url($valid_url, PHP_URL_PATH))) && empty(parse_url($valid_url, PHP_URL_PATH)))) {
DI::logger()->debug('Found redirect. Mark old entry as failure', ['old' => $url, 'new' => $valid_url]); DI::logger()->debug('Found redirect. Mark old entry as failure', ['old' => $url, 'new' => $valid_url]);
self::setFailureByUrl($url); self::setFailureByUrl($url);
$target_id = self::getID($valid_url, true); $target_id = self::getID($valid_url, true);
@ -606,12 +609,12 @@ class GServer
return false; return false;
} }
if ((parse_url($url, PHP_URL_HOST) != parse_url($valid_url, PHP_URL_HOST)) && (parse_url($url, PHP_URL_PATH) != parse_url($valid_url, PHP_URL_PATH)) && if ((parse_url($url, PHP_URL_HOST) != parse_url($valid_url, PHP_URL_HOST)) && (parse_url($url, PHP_URL_PATH) != parse_url($valid_url, PHP_URL_PATH))
(parse_url($url, PHP_URL_PATH) == '')) { && (parse_url($url, PHP_URL_PATH) == '')) {
DI::logger()->debug('Found redirect. Mark old entry as failure and redirect to the basepath.', ['old' => $url, 'new' => $valid_url]); DI::logger()->debug('Found redirect. Mark old entry as failure and redirect to the basepath.', ['old' => $url, 'new' => $valid_url]);
$parts = (array)parse_url($valid_url); $parts = (array) parse_url($valid_url);
unset($parts['path']); unset($parts['path']);
$valid_url = (string)Uri::fromParts($parts); $valid_url = (string) Uri::fromParts($parts);
self::setFailureByUrl($url); self::setFailureByUrl($url);
if (!self::getID($valid_url, true) && !Network::isUrlBlocked($valid_url)) { if (!self::getID($valid_url, true) && !Network::isUrlBlocked($valid_url)) {
@ -622,14 +625,17 @@ class GServer
DI::logger()->debug('Found redirect, but ignore it.', ['old' => $url, 'new' => $valid_url]); DI::logger()->debug('Found redirect, but ignore it.', ['old' => $url, 'new' => $valid_url]);
} }
if ((parse_url($url, PHP_URL_HOST) == parse_url($valid_url, PHP_URL_HOST)) && if ((parse_url($url, PHP_URL_HOST) == parse_url($valid_url, PHP_URL_HOST))
(parse_url($url, PHP_URL_PATH) == parse_url($valid_url, PHP_URL_PATH)) && && (parse_url($url, PHP_URL_PATH) == parse_url($valid_url, PHP_URL_PATH))
(parse_url($url, PHP_URL_SCHEME) != parse_url($valid_url, PHP_URL_SCHEME))) { && (parse_url($url, PHP_URL_SCHEME) != parse_url($valid_url, PHP_URL_SCHEME))) {
$url = $valid_url; $url = $valid_url;
} }
$in_webroot = empty(parse_url($url, PHP_URL_PATH)); $in_webroot = empty(parse_url($url, PHP_URL_PATH));
self::$robotsTxt = DI::robotsTxt();
self::$robotsTxt->load($url);
// When a nodeinfo is present, we don't need to dig further // When a nodeinfo is present, we don't need to dig further
$curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if ($curlResult->isTimeout()) { if ($curlResult->isTimeout()) {
@ -642,6 +648,10 @@ class GServer
} else { } else {
$serverdata = self::parseNodeinfo($url, $curlResult); $serverdata = self::parseNodeinfo($url, $curlResult);
if (!empty($serverdata)) {
$serverdata = self::clearStatisticalData($serverdata);
}
if (empty($serverdata) || !in_array($serverdata['detection-method'], [self::DETECT_NODEINFO_20, self::DETECT_NODEINFO_21, self::DETECT_NODEINFO_22])) { if (empty($serverdata) || !in_array($serverdata['detection-method'], [self::DETECT_NODEINFO_20, self::DETECT_NODEINFO_21, self::DETECT_NODEINFO_22])) {
$curlResult = DI::httpClient()->get($url . '/.well-known/x-nodeinfo2', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/.well-known/x-nodeinfo2', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
$serverdata = self::parseNodeinfo2($curlResult) ?: $serverdata; $serverdata = self::parseNodeinfo2($curlResult) ?: $serverdata;
@ -658,7 +668,7 @@ class GServer
// When there is no Nodeinfo, then use some protocol specific endpoints // When there is no Nodeinfo, then use some protocol specific endpoints
if ($serverdata['network'] == Protocol::PHANTOM) { if ($serverdata['network'] == Protocol::PHANTOM) {
if ($in_webroot) { if ($in_webroot && self::$robotsTxt->isAllowed('/')) {
// Fetch the landing page, possibly it reveals some data // Fetch the landing page, possibly it reveals some data
$accept = 'application/activity+json,application/ld+json,application/json,*/*;q=0.9'; $accept = 'application/activity+json,application/ld+json,application/json,*/*;q=0.9';
$curlResult = DI::httpClient()->get($url, $accept, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url, $accept, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
@ -693,17 +703,17 @@ class GServer
return false; return false;
} }
if (in_array($url, ['https://www.threads.net', 'https://threads.net'])) {
$serverdata['detection-method'] = self::DETECT_THREADS;
$serverdata['network'] = Protocol::ACTIVITYPUB;
$serverdata['platform'] = 'threads';
}
if (($serverdata['network'] == Protocol::PHANTOM) || in_array($serverdata['detection-method'], self::DETECT_UNSPECIFIC)) { if (($serverdata['network'] == Protocol::PHANTOM) || in_array($serverdata['detection-method'], self::DETECT_UNSPECIFIC)) {
$serverdata = self::detectMastodonAlikes($url, $serverdata); $serverdata = self::detectMastodonAlikes($url, $serverdata);
} }
} }
if (($serverdata['network'] === Protocol::PHANTOM) && in_array($url, ['https://www.threads.net', 'https://threads.net'])) {
$serverdata['detection-method'] = self::DETECT_THREADS;
$serverdata['network'] = Protocol::ACTIVITYPUB;
$serverdata['platform'] = 'threads';
}
// All following checks are done for systems that always have got a "host-meta" endpoint. // All following checks are done for systems that always have got a "host-meta" endpoint.
// With this check we don't have to waste time and resources for dead systems. // With this check we don't have to waste time and resources for dead systems.
// Also this hopefully prevents us from receiving abuse messages. // Also this hopefully prevents us from receiving abuse messages.
@ -779,8 +789,8 @@ class GServer
// When a server is new, then there is no gserver entry yet. // When a server is new, then there is no gserver entry yet.
// But in "detectNetworkViaContacts" it could happen that a contact is updated, // But in "detectNetworkViaContacts" it could happen that a contact is updated,
// and this can call this function here as well. // and this can call this function here as well.
if (self::getID($url, true) && (in_array($serverdata['network'], [Protocol::PHANTOM, Protocol::FEED]) || if (self::getID($url, true) && (in_array($serverdata['network'], [Protocol::PHANTOM, Protocol::FEED])
in_array($serverdata['detection-method'], [self::DETECT_MANUAL, self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_HOST_META]))) { || in_array($serverdata['detection-method'], [self::DETECT_MANUAL, self::DETECT_HEADER, self::DETECT_BODY, self::DETECT_HOST_META]))) {
$serverdata = self::detectNetworkViaContacts($url, $serverdata); $serverdata = self::detectNetworkViaContacts($url, $serverdata);
} }
@ -812,12 +822,7 @@ class GServer
$serverdata = self::fetchWeeklyUsage($url, $serverdata); $serverdata = self::fetchWeeklyUsage($url, $serverdata);
} }
$serverdata['registered-users'] = $serverdata['registered-users'] ?? 0; $serverdata['registered-users'] ??= 0;
// Numbers above a reasonable value (10 millions) are ignored
if ($serverdata['registered-users'] > 10000000) {
$serverdata['registered-users'] = 0;
}
// On an active server there has to be at least a single user // On an active server there has to be at least a single user
if (!in_array($serverdata['network'], [Protocol::PHANTOM, Protocol::FEED]) && ($serverdata['registered-users'] <= 0)) { if (!in_array($serverdata['network'], [Protocol::PHANTOM, Protocol::FEED]) && ($serverdata['registered-users'] <= 0)) {
@ -862,6 +867,53 @@ class GServer
return $ret; return $ret;
} }
/**
* Clear statistical data from nodeinfo when we are not allowed to access it or when the data seems to be faked.
*
* GotoSocial is known for having an option to provide fake statistical data, so we apply some checks for this platform in particular.
*
* @param array $serverdata
* @return array
*/
private static function clearStatisticalData(array $serverdata): array
{
// We don't clear the data when we are allowed to access the nodeinfo
$clear = !self::$robotsTxt->isAllowed('/.well-known/nodeinfo');
// Numbers above a reasonable value (10 millions) are a sign for faked data, so we clear the data in this case as well.
if (($serverdata['registered-users'] ?? 0) > 10000000) {
$clear = true;
}
// All the following checks are only applied for GoToSocial, so we can skip them for other platforms.
if (!$clear && $serverdata['platform'] !== 'gotosocial') {
return $serverdata;
}
// When the robots.txt couldn't be loaded, we clear the data as well.
if (!self::$robotsTxt->isLoaded()) {
$clear = true;
}
// We clear the data when there are more than 10.000 registered users, since this is an indicator for faked data.
if (($serverdata['registered-users'] ?? 0) > 10000) {
$clear = true;
}
if (!$clear) {
return $serverdata;
}
$serverdata['registered-users'] = null;
$serverdata['active-week-users'] = null;
$serverdata['active-month-users'] = null;
$serverdata['active-halfyear-users'] = null;
$serverdata['local-posts'] = null;
$serverdata['local-comments'] = null;
return $serverdata;
}
/** /**
* Count the number of known contacts from this server * Count the number of known contacts from this server
*/ */
@ -905,6 +957,10 @@ class GServer
*/ */
private static function discoverRelay(string $server_url) private static function discoverRelay(string $server_url)
{ {
if (!self::$robotsTxt->isAllowed('/.well-known/x-social-relay')) {
return;
}
DI::logger()->info('Discover relay data', ['server' => $server_url]); DI::logger()->info('Discover relay data', ['server' => $server_url]);
$curlResult = DI::httpClient()->get($server_url . '/.well-known/x-social-relay', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($server_url . '/.well-known/x-social-relay', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
@ -918,7 +974,7 @@ class GServer
} }
// Sanitize incoming data, see https://github.com/friendica/friendica/issues/8565 // Sanitize incoming data, see https://github.com/friendica/friendica/issues/8565
$data['subscribe'] = (bool)($data['subscribe'] ?? false); $data['subscribe'] = (bool) ($data['subscribe'] ?? false);
if (!$data['subscribe'] || empty($data['scope']) || !in_array(strtolower($data['scope']), ['all', 'tags'])) { if (!$data['subscribe'] || empty($data['scope']) || !in_array(strtolower($data['scope']), ['all', 'tags'])) {
$data['scope'] = ''; $data['scope'] = '';
@ -1002,6 +1058,10 @@ class GServer
*/ */
private static function fetchStatistics(string $url, array $serverdata): array private static function fetchStatistics(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/statistics.json')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/statistics.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/statistics.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
@ -1181,7 +1241,7 @@ class GServer
$server = [ $server = [
'detection-method' => self::DETECT_NODEINFO_10, 'detection-method' => self::DETECT_NODEINFO_10,
'register_policy' => Register::CLOSED 'register_policy' => Register::CLOSED,
]; ];
if (!empty($nodeinfo['openRegistrations'])) { if (!empty($nodeinfo['openRegistrations'])) {
@ -1412,7 +1472,7 @@ class GServer
$server = [ $server = [
'detection-method' => self::DETECT_NODEINFO2_10, 'detection-method' => self::DETECT_NODEINFO2_10,
'register_policy' => Register::CLOSED 'register_policy' => Register::CLOSED,
]; ];
if (!empty($nodeinfo['openRegistrations'])) { if (!empty($nodeinfo['openRegistrations'])) {
@ -1504,6 +1564,10 @@ class GServer
*/ */
private static function fetchSiteinfo(string $url, array $serverdata): array private static function fetchSiteinfo(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/siteinfo.json')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/siteinfo.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/siteinfo.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
@ -1684,6 +1748,10 @@ class GServer
*/ */
private static function getNomadVersion(string $url): string private static function getNomadVersion(string $url): string
{ {
if (!self::$robotsTxt->isAllowed('/api/z/1.0/version')) {
return '';
}
$curlResult = DI::httpClient()->get($url . '/api/z/1.0/version', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/api/z/1.0/version', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return ''; return '';
@ -1777,6 +1845,10 @@ class GServer
*/ */
private static function validHostMeta(string $url): bool private static function validHostMeta(string $url): bool
{ {
if (!self::$robotsTxt->isAllowed(Probe::HOST_META)) {
return false;
}
$xrd_timeout = DI::config()->get('system', 'xrd_timeout'); $xrd_timeout = DI::config()->get('system', 'xrd_timeout');
$curlResult = DI::httpClient()->get($url . Probe::HOST_META, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . Probe::HOST_META, HttpClientAccept::XRD_XML, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
@ -1866,6 +1938,10 @@ class GServer
{ {
$serverdata['poco'] = ''; $serverdata['poco'] = '';
if (!self::$robotsTxt->isAllowed('/poco')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/poco', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/poco', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
@ -1895,6 +1971,10 @@ class GServer
*/ */
public static function checkMastodonDirectory(string $url, array $serverdata): array public static function checkMastodonDirectory(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/api/v1/directory')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/api/v1/directory?limit=1', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/api/v1/directory?limit=1', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
return $serverdata; return $serverdata;
@ -1927,6 +2007,10 @@ class GServer
*/ */
private static function detectPeertube(string $url, array $serverdata): array private static function detectPeertube(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/api/v1/config')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/api/v1/config', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/api/v1/config', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
@ -1975,6 +2059,10 @@ class GServer
*/ */
private static function detectNextcloud(string $url, array $serverdata, bool $validHostMeta): array private static function detectNextcloud(string $url, array $serverdata, bool $validHostMeta): array
{ {
if (!self::$robotsTxt->isAllowed('/status.php')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/status.php', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/status.php', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
@ -2011,6 +2099,10 @@ class GServer
*/ */
private static function fetchWeeklyUsage(string $url, array $serverdata): array private static function fetchWeeklyUsage(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/api/v1/instance/activity')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/api/v1/instance/activity', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/api/v1/instance/activity', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
@ -2051,6 +2143,10 @@ class GServer
*/ */
private static function detectMastodonAlikes(string $url, array $serverdata): array private static function detectMastodonAlikes(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/api/v1/instance')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/api/v1/instance', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/api/v1/instance', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
@ -2128,6 +2224,10 @@ class GServer
*/ */
private static function detectHubzilla(string $url, array $serverdata): array private static function detectHubzilla(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/api/statusnet/config.json')) {
return $serverdata;
}
$curlResult = DI::httpClient()->get($url . '/api/statusnet/config.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/api/statusnet/config.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) { if (!$curlResult->isSuccess() || ($curlResult->getBodyString() == '')) {
return $serverdata; return $serverdata;
@ -2223,44 +2323,48 @@ class GServer
private static function detectGNUSocial(string $url, array $serverdata): array private static function detectGNUSocial(string $url, array $serverdata): array
{ {
// Test for GNU Social // Test for GNU Social
$curlResult = DI::httpClient()->get($url . '/api/gnusocial/version.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); if (self::$robotsTxt->isAllowed('/api/gnusocial/version.json')) {
if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}') && $curlResult = DI::httpClient()->get($url . '/api/gnusocial/version.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) { if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}')
$serverdata['platform'] = 'gnusocial'; && ($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) {
// Remove junk that some GNU Social servers return $serverdata['platform'] = 'gnusocial';
$serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBodyString()); // Remove junk that some GNU Social servers return
$serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']); $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBodyString());
$serverdata['version'] = trim($serverdata['version'], '"'); $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
$serverdata['network'] = Protocol::OSTATUS; $serverdata['version'] = trim($serverdata['version'], '"');
$serverdata['network'] = Protocol::OSTATUS;
if (in_array($serverdata['detection-method'], self::DETECT_UNSPECIFIC)) { if (in_array($serverdata['detection-method'], self::DETECT_UNSPECIFIC)) {
$serverdata['detection-method'] = self::DETECT_GNUSOCIAL; $serverdata['detection-method'] = self::DETECT_GNUSOCIAL;
}
return $serverdata;
} }
return $serverdata;
} }
// Test for Statusnet // Test for Statusnet
$curlResult = DI::httpClient()->get($url . '/api/statusnet/version.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); if (self::$robotsTxt->isAllowed('/api/statusnet/version.json')) {
if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}') && $curlResult = DI::httpClient()->get($url . '/api/statusnet/version.json', HttpClientAccept::JSON, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) { if ($curlResult->isSuccess() && ($curlResult->getBodyString() != '{"error":"not implemented"}')
&& ($curlResult->getBodyString() != '') && (strlen($curlResult->getBodyString()) < 30)) {
// Remove junk that some GNU Social servers return // Remove junk that some GNU Social servers return
$serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBodyString()); $serverdata['version'] = str_replace(chr(239) . chr(187) . chr(191), '', $curlResult->getBodyString());
$serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']); $serverdata['version'] = str_replace(["\r", "\n", "\t"], '', $serverdata['version']);
$serverdata['version'] = trim($serverdata['version'], '"'); $serverdata['version'] = trim($serverdata['version'], '"');
if (!empty($serverdata['version']) && strtolower(substr($serverdata['version'], 0, 7)) == 'pleroma') { if (!empty($serverdata['version']) && strtolower(substr($serverdata['version'], 0, 7)) == 'pleroma') {
$serverdata['platform'] = 'pleroma'; $serverdata['platform'] = 'pleroma';
$serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version'])); $serverdata['version'] = trim(str_ireplace('pleroma', '', $serverdata['version']));
$serverdata['network'] = Protocol::ACTIVITYPUB; $serverdata['network'] = Protocol::ACTIVITYPUB;
} else { } else {
$serverdata['platform'] = 'statusnet'; $serverdata['platform'] = 'statusnet';
$serverdata['network'] = Protocol::OSTATUS; $serverdata['network'] = Protocol::OSTATUS;
} }
if (in_array($serverdata['detection-method'], self::DETECT_UNSPECIFIC)) { if (in_array($serverdata['detection-method'], self::DETECT_UNSPECIFIC)) {
$serverdata['detection-method'] = self::DETECT_STATUSNET; $serverdata['detection-method'] = self::DETECT_STATUSNET;
}
} }
} }
@ -2277,6 +2381,10 @@ class GServer
*/ */
private static function detectFriendica(string $url, array $serverdata): array private static function detectFriendica(string $url, array $serverdata): array
{ {
if (!self::$robotsTxt->isAllowed('/friendica/json')) {
return $serverdata;
}
// There is a bug in some versions of Friendica that will return an ActivityStream actor when the content type "application/json" is requested. // There is a bug in some versions of Friendica that will return an ActivityStream actor when the content type "application/json" is requested.
// Because of this me must not use ACCEPT_JSON here. // Because of this me must not use ACCEPT_JSON here.
$curlResult = DI::httpClient()->get($url . '/friendica/json', HttpClientAccept::DEFAULT, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]); $curlResult = DI::httpClient()->get($url . '/friendica/json', HttpClientAccept::DEFAULT, [HttpClientOptions::REQUEST => HttpClientRequest::SERVERINFO]);
@ -2565,7 +2673,7 @@ class GServer
'gserver', 'gserver',
['id', 'url', 'nurl', 'network', 'poco', 'directory-type'], ['id', 'url', 'nurl', 'network', 'poco', 'directory-type'],
["NOT `blocked` AND NOT `failed` AND `directory-type` != ? AND `last_poco_query` < ?", GServer::DT_NONE, $last_update], ["NOT `blocked` AND NOT `failed` AND `directory-type` != ? AND `last_poco_query` < ?", GServer::DT_NONE, $last_update],
['order' => ['RAND()']] ['order' => ['RAND()']],
); );
while ($gserver = DBA::fetch($gservers)) { while ($gserver = DBA::fetch($gservers)) {

View file

@ -38,52 +38,52 @@ use Friendica\Util\Map;
use Friendica\Util\Network; use Friendica\Util\Network;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
use Friendica\Util\Strings; use Friendica\Util\Strings;
use Friendica\Util\Temporal;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
class Item class Item
{ {
// Posting types, inspired by https://www.w3.org/TR/activitystreams-vocabulary/#object-types // Posting types, inspired by https://www.w3.org/TR/activitystreams-vocabulary/#object-types
const PT_ARTICLE = 0; public const PT_ARTICLE = 0;
const PT_NOTE = 1; public const PT_NOTE = 1;
const PT_PAGE = 2; public const PT_PAGE = 2;
const PT_IMAGE = 16; public const PT_IMAGE = 16;
const PT_AUDIO = 17; public const PT_AUDIO = 17;
const PT_VIDEO = 18; public const PT_VIDEO = 18;
const PT_DOCUMENT = 19; public const PT_DOCUMENT = 19;
const PT_EVENT = 32; public const PT_EVENT = 32;
const PT_POLL = 33; public const PT_POLL = 33;
const PT_PERSONAL_NOTE = 128; public const PT_PERSONAL_NOTE = 128;
// Posting reasons (Why had a post been stored for a user?) // Posting reasons (Why had a post been stored for a user?)
const PR_NONE = 0; public const PR_NONE = 0;
const PR_TAG = 64; public const PR_TAG = 64;
const PR_TO = 65; public const PR_TO = 65;
const PR_CC = 66; public const PR_CC = 66;
const PR_BTO = 67; public const PR_BTO = 67;
const PR_BCC = 68; public const PR_BCC = 68;
const PR_FOLLOWER = 69; public const PR_FOLLOWER = 69;
const PR_ANNOUNCEMENT = 70; public const PR_ANNOUNCEMENT = 70;
const PR_COMMENT = 71; public const PR_COMMENT = 71;
const PR_STORED = 72; public const PR_STORED = 72;
const PR_GLOBAL = 73; public const PR_GLOBAL = 73;
const PR_RELAY = 74; public const PR_RELAY = 74;
const PR_FETCHED = 75; public const PR_FETCHED = 75;
const PR_COMPLETION = 76; public const PR_COMPLETION = 76;
const PR_DIRECT = 77; public const PR_DIRECT = 77;
const PR_ACTIVITY = 78; public const PR_ACTIVITY = 78;
const PR_DISTRIBUTE = 79; public const PR_DISTRIBUTE = 79;
const PR_PUSHED = 80; public const PR_PUSHED = 80;
const PR_LOCAL = 81; public const PR_LOCAL = 81;
const PR_AUDIENCE = 82; public const PR_AUDIENCE = 82;
public const PR_CHANNEL = 83;
// system.accept_only_sharer setting values // system.accept_only_sharer setting values
const COMPLETION_NONE = 1; public const COMPLETION_NONE = 1;
const COMPLETION_COMMENT = 0; public const COMPLETION_COMMENT = 0;
const COMPLETION_LIKE = 2; public const COMPLETION_LIKE = 2;
// Field list that is used to display the items // Field list that is used to display the items
const DISPLAY_FIELDLIST = [ public const DISPLAY_FIELDLIST = [
'uid', 'id', 'parent', 'guid', 'network', 'protocol', 'gravity', 'uid', 'id', 'parent', 'guid', 'network', 'protocol', 'gravity',
'uri-id', 'uri', 'thr-parent-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'conversation', 'uri-id', 'uri', 'thr-parent-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'conversation',
'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink', 'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink',
@ -100,11 +100,11 @@ class Item
'event-nofinish', 'event-ignore', 'event-id', 'event-nofinish', 'event-ignore', 'event-id',
'question-id', 'question-multiple', 'question-voters', 'question-end-time', 'question-id', 'question-multiple', 'question-voters', 'question-end-time',
'has-categories', 'has-media', 'has-categories', 'has-media',
'delivery_queue_count', 'delivery_queue_done', 'delivery_queue_failed' 'delivery_queue_count', 'delivery_queue_done', 'delivery_queue_failed',
]; ];
// Field list that is used to deliver items via the protocols // Field list that is used to deliver items via the protocols
const DELIVER_FIELDLIST = [ public const DELIVER_FIELDLIST = [
'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', 'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid',
'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', 'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target',
'private', 'title', 'content-warning', 'body', 'raw-body', 'language', 'location', 'coord', 'app', 'sensitive', 'private', 'title', 'content-warning', 'body', 'raw-body', 'language', 'location', 'coord', 'app', 'sensitive',
@ -115,11 +115,11 @@ class Item
'thr-parent-id', 'parent-uri-id', 'quote-uri', 'quote-uri-id', 'postopts', 'pubmail', 'thr-parent-id', 'parent-uri-id', 'quote-uri', 'quote-uri-id', 'postopts', 'pubmail',
'event-created', 'event-edited', 'event-start', 'event-finish', 'event-created', 'event-edited', 'event-start', 'event-finish',
'event-summary', 'event-desc', 'event-location', 'event-type', 'event-summary', 'event-desc', 'event-location', 'event-type',
'event-nofinish', 'event-ignore', 'event-id' 'event-nofinish', 'event-ignore', 'event-id',
]; ];
// All fields in the item table // All fields in the item table
const ITEM_FIELDLIST = [ public const ITEM_FIELDLIST = [
'id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', 'id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent',
'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'conversation', 'vid', 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'conversation', 'vid',
'quote-uri', 'quote-uri-id', 'contact-id', 'wall', 'gravity', 'extid', 'psid', 'quote-uri', 'quote-uri-id', 'contact-id', 'wall', 'gravity', 'extid', 'psid',
@ -131,34 +131,34 @@ class Item
'title', 'content-warning', 'body', 'language', 'location', 'coord', 'app', 'title', 'content-warning', 'body', 'language', 'location', 'coord', 'app',
'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target', 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target',
'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network',
'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id' 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id',
]; ];
// List of all verbs that don't need additional content data. // List of all verbs that don't need additional content data.
// Never reorder or remove entries from this list. Just add new ones at the end, if needed. // Never reorder or remove entries from this list. Just add new ones at the end, if needed.
const ACTIVITIES = [ public const ACTIVITIES = [
Activity::LIKE, Activity::DISLIKE, Activity::LIKE, Activity::DISLIKE,
Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE, Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE,
Activity::FOLLOW, Activity::FOLLOW,
Activity::ANNOUNCE Activity::ANNOUNCE,
]; ];
// Privacy levels // Privacy levels
const PUBLIC = 0; public const PUBLIC = 0;
const PRIVATE = 1; public const PRIVATE = 1;
const UNLISTED = 2; public const UNLISTED = 2;
// Item weight for query ordering // Item weight for query ordering
const GRAVITY_PARENT = 0; public const GRAVITY_PARENT = 0;
const GRAVITY_ACTIVITY = 3; public const GRAVITY_ACTIVITY = 3;
const GRAVITY_COMMENT = 6; public const GRAVITY_COMMENT = 6;
const GRAVITY_UNKNOWN = 9; public const GRAVITY_UNKNOWN = 9;
// Restrictions // Restrictions
const CANT_REPLY = 1; public const CANT_REPLY = 1;
const CANT_LIKE = 2; public const CANT_LIKE = 2;
const CANT_ANNOUNCE = 4; public const CANT_ANNOUNCE = 4;
const CANT_QUOTE = 8; public const CANT_QUOTE = 8;
/** /**
* Update existing item entries * Update existing item entries
@ -208,8 +208,8 @@ class Item
// We only need to call the line by line update for specific fields // We only need to call the line by line update for specific fields
if ( if (
empty($fields['body']) && empty($fields['file']) && empty($fields['body']) && empty($fields['file'])
empty($fields['attach']) && empty($fields['edited']) && empty($fields['attach']) && empty($fields['edited'])
) { ) {
return $rows; return $rows;
} }
@ -284,7 +284,7 @@ class Item
} }
} }
Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::POST, (int)$post['uri-id'], (int)$post['uid']); Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::POST, (int) $post['uri-id'], (int) $post['uid']);
} }
return $rows; return $rows;
@ -352,7 +352,7 @@ class Item
$fields = [ $fields = [
'id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri-id', 'origin', 'id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri-id', 'origin',
'thr-parent-id', 'deleted', 'resource-id', 'event-id', 'vid', 'body', 'thr-parent-id', 'deleted', 'resource-id', 'event-id', 'vid', 'body',
'verb', 'object-type', 'object', 'target', 'contact-id', 'psid', 'gravity' 'verb', 'object-type', 'object', 'target', 'contact-id', 'psid', 'gravity',
]; ];
$item = Post::selectFirst($fields, ['id' => $item_id]); $item = Post::selectFirst($fields, ['id' => $item_id]);
if (!DBA::isResult($item)) { if (!DBA::isResult($item)) {
@ -402,7 +402,7 @@ class Item
Post\DeliveryData::delete($item['uri-id']); Post\DeliveryData::delete($item['uri-id']);
// If it's the parent of a comment thread, kill all the kids // If it's the parent of a comment thread, kill all the kids
if ($item['gravity'] == self::GRAVITY_PARENT) { if ($item['gravity'] == self::GRAVITY_PARENT && !is_null($item['parent'])) {
self::markForDeletion(['parent' => $item['parent'], 'deleted' => false], $priority); self::markForDeletion(['parent' => $item['parent'], 'deleted' => false], $priority);
} }
@ -429,7 +429,7 @@ class Item
// send the notification upstream/downstream // send the notification upstream/downstream
if ($priority) { if ($priority) {
Worker::add(['priority' => $priority, 'dont_fork' => true], 'Notifier', Delivery::DELETION, (int)$item['uri-id'], (int)$item['uid']); Worker::add(['priority' => $priority, 'dont_fork' => true], 'Notifier', Delivery::DELETION, (int) $item['uri-id'], (int) $item['uid']);
} }
} elseif ($item['uid'] != 0) { } elseif ($item['uid'] != 0) {
Post\User::update($item['uri-id'], $item['uid'], ['hidden' => true]); Post\User::update($item['uri-id'], $item['uid'], ['hidden' => true]);
@ -576,7 +576,7 @@ class Item
$condition = [ $condition = [
'verb' => Activity::FOLLOW, 'uid' => $item['uid'], 'verb' => Activity::FOLLOW, 'uid' => $item['uid'],
'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id'] 'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id'],
]; ];
if (Post::exists($condition)) { if (Post::exists($condition)) {
// It happens that we receive multiple follow requests by the same author - we only store one. // It happens that we receive multiple follow requests by the same author - we only store one.
@ -630,7 +630,8 @@ class Item
$priority = Worker::PRIORITY_HIGH; $priority = Worker::PRIORITY_HIGH;
$copy_permissions = false; $copy_permissions = false;
$defined_permissions = isset($item['allow_cid']) && isset($item['allow_gid']) && isset($item['deny_cid']) && isset($item['deny_gid']) && isset($item['private']);
// If it is a posting where users should get notifications, then define it as wall posting // If it is a posting where users should get notifications, then define it as wall posting
if ($notify) { if ($notify) {
@ -642,8 +643,12 @@ class Item
} }
// Mastodon style API visibility // Mastodon style API visibility
$copy_permissions = ($item['visibility'] ?? 'private') == 'private'; if (isset($item['visibility'])) {
unset($item['visibility']); $copy_permissions = $item['visibility'] === 'private';
unset($item['visibility']);
} else {
$copy_permissions = !$defined_permissions;
}
} else { } else {
$item['network'] = trim(($item['network'] ?? '') ?: Protocol::PHANTOM); $item['network'] = trim(($item['network'] ?? '') ?: Protocol::PHANTOM);
} }
@ -677,8 +682,6 @@ class Item
$item['post-type'] = empty($item['title']) ? self::PT_NOTE : self::PT_ARTICLE; $item['post-type'] = empty($item['title']) ? self::PT_NOTE : self::PT_ARTICLE;
} }
$defined_permissions = isset($item['allow_cid']) && isset($item['allow_gid']) && isset($item['deny_cid']) && isset($item['deny_gid']) && isset($item['private']);
$uid = intval($item['uid']); $uid = intval($item['uid']);
// Communities aren't working with the Diaspora protocol // Communities aren't working with the Diaspora protocol
@ -700,8 +703,8 @@ class Item
$item['contact-id'] = self::contactId($item); $item['contact-id'] = self::contactId($item);
if ( if (
!empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY]) && !empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY])
empty($item['origin']) && DI::contentItem()->isTooOld($item['created'], $item['uid']) && empty($item['origin']) && DI::contentItem()->isTooOld($item['created'], $item['uid'])
) { ) {
DI::logger()->info('Item is too old', ['item' => $item]); DI::logger()->info('Item is too old', ['item' => $item]);
return 0; return 0;
@ -793,7 +796,7 @@ class Item
]; ];
$hook_data = $eventDispatcher->dispatch( $hook_data = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL, $hook_data) new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_LOCAL, $hook_data),
)->getArray(); )->getArray();
/** @var array<string,mixed> */ /** @var array<string,mixed> */
@ -806,7 +809,7 @@ class Item
} elseif (!$notify) { } elseif (!$notify) {
/** @var array<string,mixed> */ /** @var array<string,mixed> */
$item = $eventDispatcher->dispatch( $item = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_REMOTE, $item) new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_REMOTE, $item),
)->getArray(); )->getArray();
} }
@ -826,8 +829,8 @@ class Item
$item['allow_cid'], $item['allow_cid'],
$item['allow_gid'], $item['allow_gid'],
$item['deny_cid'], $item['deny_cid'],
$item['deny_gid'] $item['deny_gid'],
) ),
)->id; )->id;
if (!empty($item['extid'])) { if (!empty($item['extid'])) {
@ -899,37 +902,7 @@ class Item
} }
} }
if (empty($item['event-id'])) { $item = self::storeEvent($item);
if (array_key_exists('event-id', $item)) {
unset($item['event-id']);
}
$ev = Event::fromBBCode($item['body']);
if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
DI::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'] ?? Conversation::PARCEL_UNKNOWN;
$ev['direction'] = $item['direction'] ?? Conversation::UNKNOWN;
$ev['source'] = $item['source'] ?? '';
$event = DBA::selectFirst('event', ['id'], ['uri' => $item['uri'], 'uid' => $item['uid']]);
if (DBA::isResult($event)) {
$ev['id'] = $event['id'];
}
$event_id = Event::store($ev);
$item = Event::getItemArrayForImportedId($event_id, $item);
DI::logger()->info('Event was stored', ['id' => $event_id]);
}
}
if (empty($item['causer-id'])) { if (empty($item['causer-id'])) {
unset($item['causer-id']); unset($item['causer-id']);
@ -956,12 +929,16 @@ class Item
$inserted = Post::insert($item['uri-id'], $item); $inserted = Post::insert($item['uri-id'], $item);
if ($item['gravity'] == self::GRAVITY_PARENT) { if ($item['gravity'] == self::GRAVITY_PARENT) {
Post\Thread::insert($item['uri-id'], $item); if (!Post\Thread::insert($item['uri-id'], $item) && !Post\Thread::exists($item['uri-id'])) {
DI::logger()->error('Post-Thread entry was not inserted', ['uri-id' => $item['uri-id']]);
}
} }
// The content of activities normally doesn't matter - except for likes from Misskey // The content of activities normally doesn't matter - except for likes from Misskey
if (!in_array($item['verb'], self::ACTIVITIES) || in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE]) && !empty($item['body']) && (mb_strlen($item['body']) == 1)) { if (!in_array($item['verb'], self::ACTIVITIES) || in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE]) && !empty($item['body']) && (mb_strlen($item['body']) == 1)) {
Post\Content::insert($item['uri-id'], $item); if (!Post\Content::insert($item['uri-id'], $item) && !Post\Content::exists($item['uri-id'])) {
DI::logger()->error('Post-Content entry was not inserted', ['uri-id' => $item['uri-id']]);
}
} }
$item['parent'] = $parent_id; $item['parent'] = $parent_id;
@ -1008,7 +985,9 @@ class Item
if ($item['gravity'] == self::GRAVITY_PARENT) { if ($item['gravity'] == self::GRAVITY_PARENT) {
$item['post-user-id'] = $post_user_id; $item['post-user-id'] = $post_user_id;
Post\ThreadUser::insert($item['uri-id'], $item['uid'], $item); if (!Post\ThreadUser::insert($item['uri-id'], $item['uid'], $item)) {
DI::logger()->error('Post-Thread-User entry was not inserted', ['uri-id' => $item['uri-id']]);
}
} }
DI::logger()->notice('created item', ['post-id' => $post_user_id, 'uid' => $item['uid'], 'network' => $item['network'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]); DI::logger()->notice('created item', ['post-id' => $post_user_id, 'uid' => $item['uid'], 'network' => $item['network'], 'uri-id' => $item['uri-id'], 'guid' => $item['guid']]);
@ -1016,6 +995,44 @@ class Item
return self::handleCreatedItem($orig_item, $post_user_id, $uid, $notify, $copy_permissions, $parent_origin, $priority, $notify_type, $inserted, $source); return self::handleCreatedItem($orig_item, $post_user_id, $uid, $notify, $copy_permissions, $parent_origin, $priority, $notify_type, $inserted, $source);
} }
private static function storeEvent(array $item): array
{
if (!empty($item['event-id'])) {
return $item;
}
if (array_key_exists('event-id', $item)) {
unset($item['event-id']);
}
$ev = Event::fromBBCode($item['body']);
if ((!empty($ev['desc']) || !empty($ev['summary'])) && !empty($ev['start'])) {
DI::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'] ?? Conversation::PARCEL_UNKNOWN;
$ev['direction'] = $item['direction'] ?? Conversation::UNKNOWN;
$ev['source'] = $item['source'] ?? '';
$event = DBA::selectFirst('event', ['id'], ['uri' => $item['uri'], 'uid' => $item['uid']]);
if (DBA::isResult($event)) {
$ev['id'] = $event['id'];
}
$event_id = Event::store($ev);
$item = Event::getItemArrayForImportedId($event_id, $item);
DI::logger()->info('Event was stored', ['id' => $event_id]);
}
return $item;
}
private static function handleCreatedItem(array $orig_item, int $post_user_id, int $uid, int $notify, bool $copy_permissions, $parent_origin, int $priority, string $notify_type, bool $inserted, $source): int private static function handleCreatedItem(array $orig_item, int $post_user_id, int $uid, int $notify, bool $copy_permissions, $parent_origin, int $priority, string $notify_type, bool $inserted, $source): int
{ {
$posted_item = Post::selectFirst(array_merge(self::ITEM_FIELDLIST, ['author-contact-type']), ['post-user-id' => $post_user_id]); $posted_item = Post::selectFirst(array_merge(self::ITEM_FIELDLIST, ['author-contact-type']), ['post-user-id' => $post_user_id]);
@ -1029,10 +1046,10 @@ class Item
DI::logger()->debug('Handle created item', ['id' => $post_user_id, 'uri-id' => $posted_item['uri-id'], 'uid' => $posted_item['uid']]); DI::logger()->debug('Handle created item', ['id' => $post_user_id, 'uri-id' => $posted_item['uri-id'], 'uid' => $posted_item['uid']]);
if ($posted_item['origin'] && $posted_item['gravity'] === self::GRAVITY_PARENT) { if ($posted_item['origin'] && $posted_item['gravity'] === self::GRAVITY_PARENT) {
$posts = (int)(DI::keyValue()->get('nodeinfo_local_posts') ?? 0); $posts = (int) (DI::keyValue()->get('nodeinfo_local_posts') ?? 0);
DI::keyValue()->set('nodeinfo_local_posts', $posts + 1); DI::keyValue()->set('nodeinfo_local_posts', $posts + 1);
} elseif ($posted_item['origin'] && $posted_item['gravity'] == self::GRAVITY_COMMENT) { } elseif ($posted_item['origin'] && $posted_item['gravity'] == self::GRAVITY_COMMENT) {
$comments = (int)(DI::keyValue()->get('nodeinfo_local_comments') ?? 0); $comments = (int) (DI::keyValue()->get('nodeinfo_local_comments') ?? 0);
DI::keyValue()->set('nodeinfo_local_comments', $comments + 1); DI::keyValue()->set('nodeinfo_local_comments', $comments + 1);
} }
@ -1074,7 +1091,7 @@ class Item
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
$posted_item = $eventDispatcher->dispatch( $posted_item = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_REMOTE_END, $posted_item) new ArrayFilterEvent(ArrayFilterEvent::INSERT_POST_REMOTE_END, $posted_item),
)->getArray(); )->getArray();
} }
@ -1111,8 +1128,8 @@ class Item
} }
// Don't relay participation messages // Don't relay participation messages
if (($posted_item['verb'] == Activity::FOLLOW) && if (($posted_item['verb'] == Activity::FOLLOW)
(!$posted_item['origin'] || ($posted_item['author-id'] != Contact::getPublicIdByUserId($uid))) && (!$posted_item['origin'] || ($posted_item['author-id'] != Contact::getPublicIdByUserId($uid)))
) { ) {
DI::logger()->info('Participation messages will not be relayed', ['item' => $posted_item['id'], 'uri' => $posted_item['uri'], 'verb' => $posted_item['verb']]); DI::logger()->info('Participation messages will not be relayed', ['item' => $posted_item['id'], 'uri' => $posted_item['uri'], 'verb' => $posted_item['verb']]);
$transmit = false; $transmit = false;
@ -1126,15 +1143,15 @@ class Item
if ($transmit) { if ($transmit) {
ActivityPub\Transmitter::storeReceiversForItem($posted_item); ActivityPub\Transmitter::storeReceiversForItem($posted_item);
Worker::add(['priority' => $priority, 'dont_fork' => true], 'Notifier', $notify_type, (int)$posted_item['uri-id'], (int)$posted_item['uid']); Worker::add(['priority' => $priority, 'dont_fork' => true], 'Notifier', $notify_type, (int) $posted_item['uri-id'], (int) $posted_item['uid']);
} }
if ($inserted) { if ($inserted) {
if ($posted_item['gravity'] === self::GRAVITY_PARENT) { if ($posted_item['gravity'] === self::GRAVITY_PARENT) {
$posts = (int)(DI::keyValue()->get('nodeinfo_total_posts') ?? 0); $posts = (int) (DI::keyValue()->get('nodeinfo_total_posts') ?? 0);
DI::keyValue()->set('nodeinfo_total_posts', $posts + 1); DI::keyValue()->set('nodeinfo_total_posts', $posts + 1);
} elseif ($posted_item['gravity'] == self::GRAVITY_COMMENT) { } elseif ($posted_item['gravity'] == self::GRAVITY_COMMENT) {
$comments = (int)(DI::keyValue()->get('nodeinfo_total_comments') ?? 0); $comments = (int) (DI::keyValue()->get('nodeinfo_total_comments') ?? 0);
DI::keyValue()->set('nodeinfo_total_comments', $comments + 1); DI::keyValue()->set('nodeinfo_total_comments', $comments + 1);
} }
@ -1161,6 +1178,9 @@ class Item
self::reshareChannelPost($engagement_uri_id); self::reshareChannelPost($engagement_uri_id);
} }
if ($posted_item['origin'] && in_array($posted_item['verb'], [Activity::LIKE, Activity::DISLIKE, Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE, Activity::ANNOUNCE, Activity::POST])) {
DI::contentItem()->setViewed($posted_item['thr-parent-id'], $posted_item['uid']);
}
} }
if ($posted_item['uid'] !== 0 && ($posted_item['origin'] || !in_array($posted_item['network'], Protocol::FEDERATED))) { if ($posted_item['uid'] !== 0 && ($posted_item['origin'] || !in_array($posted_item['network'], Protocol::FEDERATED))) {
@ -1196,13 +1216,13 @@ class Item
DI::logger()->debug('Post accepted for channels', ['uri-id' => $uri_id, 'uid' => $uid, 'network' => $item['network']]); DI::logger()->debug('Post accepted for channels', ['uri-id' => $uri_id, 'uid' => $uid, 'network' => $item['network']]);
if (($item['gravity'] === self::GRAVITY_ACTIVITY) && ($item['verb'] === Activity::ANNOUNCE) && ($item['parent-uri-id'] === $item['thr-parent-id'])) { if (($item['gravity'] === self::GRAVITY_ACTIVITY) && ($item['verb'] === Activity::ANNOUNCE) && ($item['parent-uri-id'] === $item['thr-parent-id'])) {
DI::ChannelPost()->add($engagement, $uid, $item['author-id']); DI::ChannelPost()->add($engagement, self::GRAVITY_PARENT, $uid, $item['author-id']);
DI::SystemChannelPost()->add($engagement, self::GRAVITY_PARENT, $uid, $item['network'], $item['author-id']); DI::SystemChannelPost()->add($engagement, self::GRAVITY_PARENT, $uid, $item['network'], $item['author-id']);
} else { } else {
if ($item['origin']) { if ($item['origin']) {
DI::ChannelPost()->add($engagement, $uid, $item['author-id']); DI::ChannelPost()->add($engagement, $item['gravity'], $uid, $item['author-id']);
} elseif ($item['gravity'] === self::GRAVITY_PARENT) { } else {
DI::ChannelPost()->add($engagement, $uid); DI::ChannelPost()->add($engagement, $item['gravity'], $uid);
} }
DI::SystemChannelPost()->add($engagement, $item['gravity'], $uid, $item['network']); DI::SystemChannelPost()->add($engagement, $item['gravity'], $uid, $item['network']);
} }
@ -1240,7 +1260,7 @@ class Item
foreach (DI::userDefinedChannel()->getMatchingChannelUsers($engagement['searchtext'], $language, $tags, $engagement['media-type'], $item['owner-id'], $reshare_id) as $uid) { foreach (DI::userDefinedChannel()->getMatchingChannelUsers($engagement['searchtext'], $language, $tags, $engagement['media-type'], $item['owner-id'], $reshare_id) as $uid) {
$condition = [ $condition = [
'verb' => Activity::ANNOUNCE, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY, 'verb' => Activity::ANNOUNCE, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY,
'author-id' => Contact::getPublicIdByUserId($uid), 'uid' => $uid, 'thr-parent-id' => $uri_id 'author-id' => Contact::getPublicIdByUserId($uid), 'uid' => $uid, 'thr-parent-id' => $uri_id,
]; ];
if (!Post::exists($condition)) { if (!Post::exists($condition)) {
DI::logger()->debug('Reshare post', ['uid' => $uid, 'uri-id' => $uri_id]); DI::logger()->debug('Reshare post', ['uid' => $uid, 'uri-id' => $uri_id]);
@ -1315,7 +1335,7 @@ class Item
$parent = Post::selectFirst( $parent = Post::selectFirst(
['id', 'causer-id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-reason'], ['id', 'causer-id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-reason'],
['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']] ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']],
); );
if (!DBA::isResult($parent)) { if (!DBA::isResult($parent)) {
DI::logger()->error('Parent not found', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); DI::logger()->error('Parent not found', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]);
@ -1414,7 +1434,7 @@ class Item
$condition = [ $condition = [
'id' => $itemid, 'uid' => 0, 'id' => $itemid, 'uid' => 0,
'network' => array_merge(Protocol::FEDERATED, ['']), 'network' => array_merge(Protocol::FEDERATED, ['']),
'visible' => true, 'deleted' => false, 'private' => [self::PUBLIC, self::UNLISTED] 'visible' => true, 'deleted' => false, 'private' => [self::PUBLIC, self::UNLISTED],
]; ];
$item = Post::selectFirst(array_merge(self::ITEM_FIELDLIST, ['protocol']), $condition); $item = Post::selectFirst(array_merge(self::ITEM_FIELDLIST, ['protocol']), $condition);
if (!DBA::isResult($item)) { if (!DBA::isResult($item)) {
@ -1534,9 +1554,9 @@ class Item
$is_reshare = ($item['gravity'] == self::GRAVITY_ACTIVITY) && ($item['verb'] == Activity::ANNOUNCE); $is_reshare = ($item['gravity'] == self::GRAVITY_ACTIVITY) && ($item['verb'] == Activity::ANNOUNCE);
if (($uid != 0) && (($item['gravity'] == self::GRAVITY_PARENT) || $is_reshare) && if (($uid != 0) && (($item['gravity'] == self::GRAVITY_PARENT) || $is_reshare)
DI::pConfig()->get($uid, 'system', 'accept_only_sharer') == self::COMPLETION_NONE && && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') == self::COMPLETION_NONE
!in_array($item['post-reason'], [self::PR_FOLLOWER, self::PR_TAG, self::PR_TO, self::PR_CC, self::PR_ACTIVITY, self::PR_AUDIENCE]) && !in_array($item['post-reason'], [self::PR_FOLLOWER, self::PR_TAG, self::PR_TO, self::PR_CC, self::PR_ACTIVITY, self::PR_AUDIENCE])
) { ) {
DI::logger()->info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid, 'uri-id' => $uri_id, 'post-reason' => $item['post-reason']]); DI::logger()->info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid, 'uri-id' => $uri_id, 'post-reason' => $item['post-reason']]);
return 0; return 0;
@ -1687,7 +1707,7 @@ class Item
$contact = DBA::selectFirst('contact', [], ['id' => $item['contact-id'], 'self' => false]); $contact = DBA::selectFirst('contact', [], ['id' => $item['contact-id'], 'self' => false]);
if (DBA::isResult($contact)) { if (DBA::isResult($contact)) {
$notify = self::isRemoteSelf($contact, $item); $notify = self::isRemoteSelf($contact, $item);
$item['wall'] = (bool)$notify; $item['wall'] = (bool) $notify;
} }
} }
@ -1899,7 +1919,7 @@ class Item
// Glue it together to be able to make a hash from it // Glue it together to be able to make a hash from it
if (!empty($parsed)) { if (!empty($parsed)) {
$host_id = implode('/', (array)$parsed); $host_id = implode('/', (array) $parsed);
} else { } else {
$host_id = $uri; $host_id = $uri;
} }
@ -1974,13 +1994,13 @@ class Item
if ($arr['private'] != self::PRIVATE) { if ($arr['private'] != self::PRIVATE) {
Contact::update( Contact::update(
['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], ['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']],
['id' => $arr['owner-id']] ['id' => $arr['owner-id']],
); );
if ($arr['owner-id'] != $arr['author-id']) { if ($arr['owner-id'] != $arr['author-id']) {
Contact::update( Contact::update(
['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], ['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']],
['id' => $arr['author-id']] ['id' => $arr['author-id']],
); );
} }
} }
@ -2010,7 +2030,7 @@ class Item
$body = preg_replace( $body = preg_replace(
"/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", "/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", "#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]",
$body $body,
); );
} }
@ -2020,7 +2040,7 @@ class Item
function ($match) { function ($match) {
return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]"); return ("[url=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/url]");
}, },
$body $body,
); );
$body = preg_replace_callback( $body = preg_replace_callback(
@ -2028,7 +2048,7 @@ class Item
function ($match) { function ($match) {
return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]"); return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]");
}, },
$body $body,
); );
$body = preg_replace_callback( $body = preg_replace_callback(
@ -2036,14 +2056,14 @@ class Item
function ($match) { function ($match) {
return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]"); return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]");
}, },
$body $body,
); );
// Repair recursive urls // Repair recursive urls
$body = preg_replace( $body = preg_replace(
"/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", "/&num;\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",
"&num;$2", "&num;$2",
$body $body,
); );
foreach ($tags as $tag) { foreach ($tags as $tag) {
@ -2225,7 +2245,7 @@ class Item
$self = DBA::selectFirst( $self = DBA::selectFirst(
'contact', 'contact',
['id', 'name', 'url', 'thumb'], ['id', 'name', 'url', 'thumb'],
['uid' => $contact['uid'], 'self' => true] ['uid' => $contact['uid'], 'self' => true],
); );
if (!DBA::isResult($self)) { if (!DBA::isResult($self)) {
DI::logger()->error('Self contact not found', ['uid' => $contact['uid']]); DI::logger()->error('Self contact not found', ['uid' => $contact['uid']]);
@ -2272,7 +2292,7 @@ class Item
unset($datarray['private']); unset($datarray['private']);
} }
return (bool)$result; return (bool) $result;
} }
/** /**
@ -2381,8 +2401,8 @@ class Item
private static function hasPermissions(array $obj) private static function hasPermissions(array $obj)
{ {
return !empty($obj['allow_cid']) || !empty($obj['allow_gid']) || return !empty($obj['allow_cid']) || !empty($obj['allow_gid'])
!empty($obj['deny_cid']) || !empty($obj['deny_gid']); || !empty($obj['deny_cid']) || !empty($obj['deny_gid']);
} }
// @TODO $uid is unused parameter // @TODO $uid is unused parameter
@ -2442,7 +2462,7 @@ class Item
$condition = [ $condition = [
"`uid` = ? AND NOT `deleted` AND `gravity` = ?", "`uid` = ? AND NOT `deleted` AND `gravity` = ?",
$uid, self::GRAVITY_PARENT $uid, self::GRAVITY_PARENT,
]; ];
/* /*
@ -2469,16 +2489,16 @@ class Item
return 0; return 0;
} }
$expire_items = (bool)DI::pConfig()->get($uid, 'expire', 'items', true); $expire_items = (bool) DI::pConfig()->get($uid, 'expire', 'items', true);
// Forcing expiring of items - but not notes and marked items // Forcing expiring of items - but not notes and marked items
if ($force) { if ($force) {
$expire_items = true; $expire_items = true;
} }
$expire_notes = (bool)DI::pConfig()->get($uid, 'expire', 'notes', true); $expire_notes = (bool) DI::pConfig()->get($uid, 'expire', 'notes', true);
$expire_starred = (bool)DI::pConfig()->get($uid, 'expire', 'starred', true); $expire_starred = (bool) DI::pConfig()->get($uid, 'expire', 'starred', true);
$expire_photos = (bool)DI::pConfig()->get($uid, 'expire', 'photos', false); $expire_photos = (bool) DI::pConfig()->get($uid, 'expire', 'photos', false);
$expired = 0; $expired = 0;
@ -2520,7 +2540,7 @@ class Item
$condition = [ $condition = [
"`uid` = ? AND `wall` = ? AND NOT `deleted` AND `visible` AND `received` >= ?", "`uid` = ? AND `wall` = ? AND NOT `deleted` AND `visible` AND `received` >= ?",
$uid, $wall, $user['register_date'] $uid, $wall, $user['register_date'],
]; ];
$params = ['order' => ['received' => false]]; $params = ['order' => ['received' => false]];
$thread = Post::selectFirstThread(['received'], $condition, $params); $thread = Post::selectFirstThread(['received'], $condition, $params);
@ -2571,7 +2591,7 @@ class Item
return false; return false;
} }
if (!Post::exists(['uri-id' => $item['parent-uri-id'], 'uid' => $uid])) { if (!in_array($verb, ['view', 'unview']) && !Post::exists(['uri-id' => $item['parent-uri-id'], 'uid' => $uid])) {
$stored = self::storeForUserByUriId($item['parent-uri-id'], $uid, ['post-reason' => Item::PR_ACTIVITY]); $stored = self::storeForUserByUriId($item['parent-uri-id'], $uid, ['post-reason' => Item::PR_ACTIVITY]);
if (($item['parent-uri-id'] == $item['uri-id']) && !empty($stored)) { if (($item['parent-uri-id'] == $item['uri-id']) && !empty($stored)) {
$item = Post::selectFirst(self::ITEM_FIELDLIST, ['id' => $stored]); $item = Post::selectFirst(self::ITEM_FIELDLIST, ['id' => $stored]);
@ -2626,6 +2646,10 @@ class Item
case 'unannounce': case 'unannounce':
$activity = Activity::ANNOUNCE; $activity = Activity::ANNOUNCE;
break; break;
case 'view':
case 'unview':
$activity = Activity::VIEW;
break;
default: default:
DI::logger()->warning('unknown verb', ['verb' => $verb, 'item' => $item_id]); DI::logger()->warning('unknown verb', ['verb' => $verb, 'item' => $item_id]);
return false; return false;
@ -2652,7 +2676,7 @@ class Item
$condition = [ $condition = [
'vid' => $vids, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY, 'vid' => $vids, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY,
'author-id' => $author_id, 'uid' => $uid, 'thr-parent-id' => $uri_id 'author-id' => $author_id, 'uid' => $uid, 'thr-parent-id' => $uri_id,
]; ];
$like_item = Post::selectFirst(['id', 'guid', 'verb'], $condition); $like_item = Post::selectFirst(['id', 'guid', 'verb'], $condition);
@ -2763,7 +2787,7 @@ class Item
$condition = [ $condition = [
"(`private` != ? OR (`private` = ? AND `wall` "(`private` != ? OR (`private` = ? AND `wall`
AND `psid` IN (" . implode(', ', array_fill(0, count($permissionSets), '?')) . ")))", AND `psid` IN (" . implode(', ', array_fill(0, count($permissionSets), '?')) . ")))",
self::PRIVATE, self::PRIVATE self::PRIVATE, self::PRIVATE,
]; ];
$condition = array_merge($condition, $permissionSets->column('id')); $condition = array_merge($condition, $permissionSets->column('id'));
} }
@ -2890,9 +2914,9 @@ class Item
self::update( self::update(
[ [
'rendered-html' => $item['rendered-html'], 'rendered-html' => $item['rendered-html'],
'rendered-hash' => $item['rendered-hash'] 'rendered-hash' => $item['rendered-hash'],
], ],
['id' => $item['id']] ['id' => $item['id']],
); );
} }
} }
@ -2991,6 +3015,9 @@ class Item
$quote_uri_id = $shared_item['uri-id']; $quote_uri_id = $shared_item['uri-id'];
} }
} }
} elseif (!empty($item['quote-uri'])) {
DI::logger()->notice('Quote-uri specified, but it had not been found on the system.', ['uri-id' => $item['uri-id'], 'quote-uri' => $item['quote-uri']]);
$item['body'] .= "\n[hr]\nRE: [url]" . $item['quote-uri'] . '[/url]';
} }
if (!empty($quote_uri_id)) { if (!empty($quote_uri_id)) {
@ -3043,7 +3070,7 @@ class Item
$hook_data = [ $hook_data = [
'item' => $item, 'item' => $item,
'filter_reasons' => $filter_reasons 'filter_reasons' => $filter_reasons,
]; ];
$hook_data = $eventDispatcher->dispatch( $hook_data = $eventDispatcher->dispatch(
@ -3066,7 +3093,7 @@ class Item
'item' => $item, 'item' => $item,
'html' => $s, 'html' => $s,
'preview' => $is_preview, 'preview' => $is_preview,
'filter_reasons' => $filter_reasons 'filter_reasons' => $filter_reasons,
]; ];
$hook_data = $eventDispatcher->dispatch( $hook_data = $eventDispatcher->dispatch(
@ -3249,7 +3276,7 @@ class Item
unset($urlparts['fragment']); unset($urlparts['fragment']);
try { try {
$url = (string)Uri::fromParts((array)$urlparts); $url = (string) Uri::fromParts((array) $urlparts);
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
DI::logger()->notice('Invalid URL', ['$url' => $url, '$urlparts' => $urlparts]); DI::logger()->notice('Invalid URL', ['$url' => $url, '$urlparts' => $urlparts]);
/* See https://github.com/friendica/friendica/issues/12113 /* See https://github.com/friendica/friendica/issues/12113
@ -3270,8 +3297,8 @@ class Item
foreach ([0, 1, 2] as $size) { foreach ([0, 1, 2] as $size) {
if ( if (
preg_match('#/photo/.*-' . $size . '\.#ism', $url) && preg_match('#/photo/.*-' . $size . '\.#ism', $url)
strpos(preg_replace('#(/photo/.*)-[012]\.#ism', '$1-' . $size . '.', $body), $url) && strpos(preg_replace('#(/photo/.*)-[012]\.#ism', '$1-' . $size . '.', $body), $url)
) { ) {
return true; return true;
} }
@ -3443,10 +3470,10 @@ class Item
} }
} }
// @todo Judge between the links to use the one with most information // @todo Judge between the links to use the one with most information
if (!$found && (empty($attachment) || $PostMedia->authorName || if (!$found && (empty($attachment) || $PostMedia->authorName
(!$attachment->name && $PostMedia->name) || || (!$attachment->name && $PostMedia->name)
(!$attachment->description && $PostMedia->description) || || (!$attachment->description && $PostMedia->description)
(!$attachment->preview && $PostMedia->preview))) { || (!$attachment->preview && $PostMedia->preview))) {
$attachment = $PostMedia; $attachment = $PostMedia;
} }
} }
@ -3455,17 +3482,17 @@ class Item
$data = [ $data = [
'after' => '', 'after' => '',
'author_name' => $attachment->authorName ?? '', 'author_name' => $attachment->authorName ?? '',
'author_url' => (string)($attachment->authorUrl ?? ''), 'author_url' => (string) ($attachment->authorUrl ?? ''),
'description' => $attachment->description ?? '', 'description' => $attachment->description ?? '',
'image' => '', 'image' => '',
'preview' => '', 'preview' => '',
'provider_name' => $attachment->publisherName ?? '', 'provider_name' => $attachment->publisherName ?? '',
'provider_url' => (string)($attachment->publisherUrl ?? ''), 'provider_url' => (string) ($attachment->publisherUrl ?? ''),
'text' => '', 'text' => '',
'title' => $attachment->name ?? '', 'title' => $attachment->name ?? '',
'type' => 'link', 'type' => 'link',
'url' => (string)$attachment->url, 'url' => (string) $attachment->url,
'player_url' => (string)$attachment->playerUrl, 'player_url' => (string) $attachment->playerUrl,
'player_width' => $attachment->playerWidth, 'player_width' => $attachment->playerWidth,
'player_height' => $attachment->playerHeight, 'player_height' => $attachment->playerHeight,
'embed_type' => $attachment->embedType, 'embed_type' => $attachment->embedType,
@ -3574,7 +3601,7 @@ class Item
$trailing = ''; $trailing = '';
/** @var PostMedia $PostMedia */ /** @var PostMedia $PostMedia */
foreach ($PostMedias as $PostMedia) { foreach ($PostMedias as $PostMedia) {
if (strpos($item['body'], (string)$PostMedia->url)) { if (strpos($item['body'], (string) $PostMedia->url)) {
continue; continue;
} }
@ -3583,7 +3610,7 @@ class Item
'id' => $item['author-id'], 'id' => $item['author-id'],
'network' => $item['author-network'], 'network' => $item['author-network'],
'url' => $item['author-link'], 'url' => $item['author-link'],
'alias' => $item['author-alias'] 'alias' => $item['author-alias'],
]; ];
$the_url = Contact::magicLinkByContact($author, $PostMedia->url); $the_url = Contact::magicLinkByContact($author, $PostMedia->url);
@ -3644,7 +3671,7 @@ class Item
'id' => $item['question-id'], 'id' => $item['question-id'],
'multiple' => $item['question-multiple'], 'multiple' => $item['question-multiple'],
'voters' => $item['question-voters'], 'voters' => $item['question-voters'],
'endtime' => $item['question-end-time'] 'endtime' => $item['question-end-time'],
]; ];
$options = Post\QuestionOption::getByURIId($item['uri-id']); $options = Post\QuestionOption::getByURIId($item['uri-id']);
@ -3658,11 +3685,11 @@ class Item
} }
if (!empty($question['voters']) && !empty($question['endtime'])) { if (!empty($question['voters']) && !empty($question['endtime'])) {
$summary = DI::l10n()->tt('%d voter. Poll end: %s', '%d voters. Poll end: %s', $question['voters'] ?? 0, Temporal::getRelativeDate($question['endtime'])); $summary = DI::l10n()->tt('%d voter. Poll end: %s', '%d voters. Poll end: %s', $question['voters'] ?? 0, DI::l10n()->relativeDateTime($question['endtime']));
} elseif (!empty($question['voters'])) { } elseif (!empty($question['voters'])) {
$summary = DI::l10n()->tt('%d voter.', '%d voters.', $question['voters'] ?? 0); $summary = DI::l10n()->tt('%d voter.', '%d voters.', $question['voters'] ?? 0);
} elseif (!empty($question['endtime'])) { } elseif (!empty($question['endtime'])) {
$summary = DI::l10n()->t('Poll end: %s', Temporal::getRelativeDate($question['endtime'])); $summary = DI::l10n()->t('Poll end: %s', DI::l10n()->relativeDateTime($question['endtime']));
} else { } else {
$summary = ''; $summary = '';
} }
@ -3843,7 +3870,7 @@ class Item
]; ];
$hook_data = $eventDispatcher->dispatch( $hook_data = $eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::FETCH_ITEM_BY_LINK, $hook_data) new ArrayFilterEvent(ArrayFilterEvent::FETCH_ITEM_BY_LINK, $hook_data),
)->getArray(); )->getArray();
if (isset($hook_data['item_id'])) { if (isset($hook_data['item_id'])) {
@ -3955,7 +3982,7 @@ class Item
public static function incrementInbound(string $network) public static function incrementInbound(string $network)
{ {
$packets = (int)(DI::keyValue()->get('stats_packets_inbound_' . $network) ?? 0); $packets = (int) (DI::keyValue()->get('stats_packets_inbound_' . $network) ?? 0);
if ($packets >= PHP_INT_MAX) { if ($packets >= PHP_INT_MAX) {
$packets = 0; $packets = 0;
} }
@ -3964,7 +3991,7 @@ class Item
public static function incrementOutbound(string $network) public static function incrementOutbound(string $network)
{ {
$packets = (int)(DI::keyValue()->get('stats_packets_outbound_' . $network) ?? 0); $packets = (int) (DI::keyValue()->get('stats_packets_outbound_' . $network) ?? 0);
if ($packets >= PHP_INT_MAX) { if ($packets >= PHP_INT_MAX) {
$packets = 0; $packets = 0;
} }

View file

@ -92,7 +92,7 @@ final class ItemHelper
$item['uid'], $item['uid'],
Protocol::ACTIVITYPUB, Protocol::ACTIVITYPUB,
Protocol::DIASPORA, Protocol::DIASPORA,
Protocol::DFRN Protocol::DFRN,
]; ];
$existing = Post::selectFirst(['id', 'network'], $condition); $existing = Post::selectFirst(['id', 'network'], $condition);
@ -105,7 +105,7 @@ final class ItemHelper
'uid' => $item['uid'], 'uid' => $item['uid'],
'network' => $item['network'], 'network' => $item['network'],
'existing_id' => $existing['id'], 'existing_id' => $existing['id'],
'existing_network' => $existing['network'] 'existing_network' => $existing['network'],
]); ]);
} }
@ -134,7 +134,7 @@ final class ItemHelper
$condition = [ $condition = [
'uri-id' => $item['uri-id'], 'uid' => $item['uid'], 'uri-id' => $item['uri-id'], 'uid' => $item['uid'],
'network' => [$item['network'], Protocol::DFRN] 'network' => [$item['network'], Protocol::DFRN],
]; ];
if (Post::exists($condition)) { if (Post::exists($condition)) {
$this->logger->notice('duplicated item with the same uri found.', $condition); $this->logger->notice('duplicated item with the same uri found.', $condition);
@ -214,15 +214,15 @@ final class ItemHelper
} }
// We haven't invented time travel by now. // We haven't invented time travel by now.
if ($item['edited'] > $item['received'] ) { if ($item['edited'] > $item['received']) {
$item['edited'] = $item['received'] ; $item['edited'] = $item['received'] ;
} }
if ($item['changed'] > $item['received'] ) { if ($item['changed'] > $item['received']) {
$item['changed'] = $item['received'] ; $item['changed'] = $item['received'] ;
} }
if ($item['commented'] > $item['received'] ) { if ($item['commented'] > $item['received']) {
$item['commented'] = $item['received'] ; $item['commented'] = $item['received'] ;
} }
@ -236,13 +236,13 @@ final class ItemHelper
$default = [ $default = [
'url' => $item['author-link'], 'name' => $item['author-name'], 'url' => $item['author-link'], 'name' => $item['author-name'],
'photo' => $item['author-avatar'], 'network' => $item['network'] 'photo' => $item['author-avatar'], 'network' => $item['network'],
]; ];
$item['author-id'] = ($item['author-id'] ?? 0) ?: Contact::getIdForURL($item['author-link'], 0, null, $default); $item['author-id'] = ($item['author-id'] ?? 0) ?: Contact::getIdForURL($item['author-link'], 0, null, $default);
$default = [ $default = [
'url' => $item['owner-link'], 'name' => $item['owner-name'], 'url' => $item['owner-link'], 'name' => $item['owner-name'],
'photo' => $item['owner-avatar'], 'network' => $item['network'] 'photo' => $item['owner-avatar'], 'network' => $item['network'],
]; ];
$item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default); $item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default);
@ -264,9 +264,12 @@ final class ItemHelper
'uid', 'uri', 'parent-uri', 'id', 'deleted', 'uid', 'uri', 'parent-uri', 'id', 'deleted',
'uri-id', 'parent-uri-id', 'restrictions', 'verb', 'uri-id', 'parent-uri-id', 'restrictions', 'verb',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'wall', 'private', 'origin', 'author-id' 'wall', 'private', 'origin', 'author-id',
]; ];
$condition = ['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => $item['uid']];
$uids = $item['verb'] === Activity::VIEW ? [0, $item['uid']] : $item['uid'];
$condition = ['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => $uids];
$params = ['order' => ['id' => false]]; $params = ['order' => ['id' => false]];
$parent = Post::selectFirst($fields, $condition, $params); $parent = Post::selectFirst($fields, $condition, $params);
@ -298,7 +301,7 @@ final class ItemHelper
$condition = [ $condition = [
'uri-id' => $parent['parent-uri-id'], 'uri-id' => $parent['parent-uri-id'],
'parent-uri-id' => $parent['parent-uri-id'], 'parent-uri-id' => $parent['parent-uri-id'],
'uid' => $parent['uid'] 'uid' => $parent['uid'],
]; ];
$params = ['order' => ['id' => false]]; $params = ['order' => ['id' => false]];
$toplevel_parent = Post::selectFirst($fields, $condition, $params); $toplevel_parent = Post::selectFirst($fields, $condition, $params);
@ -354,7 +357,7 @@ final class ItemHelper
} }
// If its a post that originated here then tag the thread as "mention" // If its a post that originated here then tag the thread as "mention"
if ($item['origin'] && $item['uid']) { if ($item['origin'] && $item['uid'] && $item['verb'] !== Activity::VIEW) {
$this->database->update('post-thread-user', ['mention' => true], ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); $this->database->update('post-thread-user', ['mention' => true], ['uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]);
$this->logger->info('tagged thread as mention', ['parent' => $parent_id, 'parent-uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]); $this->logger->info('tagged thread as mention', ['parent' => $parent_id, 'parent-uri-id' => $item['parent-uri-id'], 'uid' => $item['uid']]);
} }
@ -365,7 +368,7 @@ final class ItemHelper
return $item; return $item;
} }
private function hasRestrictions(array $item, int $author_id, int $restrictions = null): bool private function hasRestrictions(array $item, int $author_id, ?int $restrictions = null): bool
{ {
if (empty($restrictions) || ($author_id == $item['author-id'])) { if (empty($restrictions) || ($author_id == $item['author-id'])) {
return false; return false;
@ -393,6 +396,16 @@ final class ItemHelper
return true; return true;
} }
if ($item['uid'] != 0 && Contact\User::isBlocked($item['author-id'], $item['uid'])) {
$this->logger->debug('Author is blocked by the user, post is ignored.', ['author' => $item['author-id'], 'uid' => $item['uid']]);
return true;
}
if ($item['uid'] != 0 && Contact\User::isIsBlocked($author_id, $item['uid'])) {
$this->logger->debug('User is blocked by the author, post is ignored.', ['author' => $author_id, 'uid' => $item['uid']]);
return true;
}
return false; return false;
} }

View file

@ -908,7 +908,7 @@ class Photo
$fields = [ $fields = [
'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny,
'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false) 'accessible' => (bool)DI::pConfig()->get($uid, 'system', 'accessible-photos', false)
]; ];
$condition = ['resource-id' => $image_rid, 'uid' => $uid]; $condition = ['resource-id' => $image_rid, 'uid' => $uid];

View file

@ -7,7 +7,7 @@
namespace Friendica\Model\Post; namespace Friendica\Model\Post;
use \BadMethodCallException; use BadMethodCallException;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
@ -75,6 +75,16 @@ class Content
return DBA::delete('post-content', $conditions); return DBA::delete('post-content', $conditions);
} }
/**
* Check if a post-content entry exists for the given URI ID
*
* @param integer $uri_id
* @return boolean exists?
*/
public static function exists(int $uri_id): bool
{
return DBA::exists('post-content', ['uri-id' => $uri_id]);
}
/** /**
* Search posts for given content * Search posts for given content

View file

@ -83,25 +83,25 @@ class Counts
$counts = []; $counts = [];
$activity_verbs = [ $activity_verbs = [
Activity::LIKE, Verb::getID(Activity::LIKE),
Activity::DISLIKE, Verb::getID(Activity::DISLIKE),
Activity::ATTEND, Verb::getID(Activity::ATTEND),
Activity::ATTENDMAYBE, Verb::getID(Activity::ATTENDMAYBE),
Activity::ATTENDNO, Verb::getID(Activity::ATTENDNO),
Activity::ANNOUNCE, Verb::getID(Activity::ANNOUNCE),
Activity::VIEW, Verb::getID(Activity::VIEW),
Activity::READ, Verb::getID(Activity::READ),
]; ];
$verbs = array_merge($activity_verbs, [Activity::EMOJIREACT, Activity::POST]); $verbs = array_merge($activity_verbs, [Verb::getID(Activity::EMOJIREACT), Verb::getID(Activity::POST)]);
$condition = DBA::mergeConditions($condition, ['verb' => $verbs]); $condition = DBA::mergeConditions($condition, ['vid' => $verbs]);
$countquery = DBA::select('post-counts-view', [], $condition); $countquery = DBA::select('post-counts-view', [], $condition);
while ($count = DBA::fetch($countquery)) { while ($count = DBA::fetch($countquery)) {
if (!empty($count['reaction'])) { if (!empty($count['reaction'])) {
$count['verb'] = Activity::EMOJIREACT; $count['verb'] = Activity::EMOJIREACT;
$count['vid'] = Verb::getID($count['verb']); $count['vid'] = Verb::getID($count['verb']);
} elseif (in_array($count['verb'], $activity_verbs)) { } elseif (in_array($count['vid'], $activity_verbs)) {
$count['reaction'] = $count['verb']; $count['reaction'] = $count['verb'];
} }
$counts[] = $count; $counts[] = $count;

View file

@ -108,11 +108,12 @@ class Engagement
'network' => $parent['network'], 'network' => $parent['network'],
'restricted' => !in_array($item['network'], Protocol::FEDERATED) || ($parent['private'] != Item::PUBLIC), 'restricted' => !in_array($item['network'], Protocol::FEDERATED) || ($parent['private'] != Item::PUBLIC),
'comments' => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_COMMENT]), 'comments' => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_COMMENT]),
'views' => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_ACTIVITY, 'vid' => [Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)]]),
'activities' => DBA::count('post', [ 'activities' => DBA::count('post', [
"`parent-uri-id` = ? AND `gravity` = ? AND NOT `vid` IN (?, ?, ?)", "`parent-uri-id` = ? AND `gravity` = ? AND NOT `vid` IN (?, ?, ?)",
$item['parent-uri-id'], Item::GRAVITY_ACTIVITY, $item['parent-uri-id'], Item::GRAVITY_ACTIVITY,
Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ) Verb::getID(Activity::FOLLOW), Verb::getID(Activity::VIEW), Verb::getID(Activity::READ)
]) ]),
]; ];
if (!$store && ($engagement['comments'] == 0) && ($engagement['activities'] == 0)) { if (!$store && ($engagement['comments'] == 0) && ($engagement['activities'] == 0)) {
DI::logger()->debug('No media, follower, subscribed tags, comments or activities. Engagement not stored', ['fields' => $engagement]); DI::logger()->debug('No media, follower, subscribed tags, comments or activities. Engagement not stored', ['fields' => $engagement]);
@ -379,7 +380,7 @@ class Engagement
DI::logger()->notice('Cleared expired engagements', ['limit' => $limit, 'rows' => DBA::affectedRows()]); DI::logger()->notice('Cleared expired engagements', ['limit' => $limit, 'rows' => DBA::affectedRows()]);
} }
private static function getCreationDateLimit(bool $forDeletion): string public static function getCreationDateLimit(bool $forDeletion): string
{ {
$posts = DI::config()->get('channel', 'engagement_post_limit'); $posts = DI::config()->get('channel', 'engagement_post_limit');
if (!empty($posts)) { if (!empty($posts)) {

View file

@ -44,22 +44,22 @@ use GuzzleHttp\Psr7\Uri;
*/ */
class Media class Media
{ {
const UNKNOWN = 0; public const UNKNOWN = 0;
const IMAGE = 1; public const IMAGE = 1;
const VIDEO = 2; public const VIDEO = 2;
const AUDIO = 3; public const AUDIO = 3;
const TEXT = 4; public const TEXT = 4;
const APPLICATION = 5; public const APPLICATION = 5;
const TORRENT = 16; public const TORRENT = 16;
const HTML = 17; public const HTML = 17;
const XML = 18; public const XML = 18;
const PLAIN = 19; public const PLAIN = 19;
const ACTIVITY = 20; public const ACTIVITY = 20;
const ACCOUNT = 21; public const ACCOUNT = 21;
const HLS = 22; public const HLS = 22;
const JSON = 23; public const JSON = 23;
const LD = 24; public const LD = 24;
const DOCUMENT = 128; public const DOCUMENT = 128;
/** /**
* Insert a post-media record * Insert a post-media record
@ -105,15 +105,22 @@ class Media
$stored = $media; $stored = $media;
$media = self::fetchAdditionalData($media); $media = self::fetchAdditionalData($media);
$exif = $media['exif'] ?? null;
$media = self::unsetEmptyFields($media); $media = self::unsetEmptyFields($media);
$media = DI::dbaDefinition()->truncateFieldsForTable('post-media', $media); $media = DI::dbaDefinition()->truncateFieldsForTable('post-media', $media);
if (array_diff_assoc($media, $stored)) { if (array_diff_assoc($media, $stored)) {
$result = DBA::insert('post-media', $media, Database::INSERT_UPDATE); $result = DBA::insert('post-media', $media, Database::INSERT_UPDATE);
DI::logger()->info('Updated media', ['result' => $result, 'media' => $media]); $id = $media['id'] ?? DBA::lastInsertId();
DI::logger()->info('Updated media', ['result' => $result, 'id' => $id, 'media' => $media]);
} else { } else {
$id = null;
DI::logger()->info('Nothing to update', ['media' => $media]); DI::logger()->info('Nothing to update', ['media' => $media]);
} }
if (isset($id) && isset($exif)) {
MediaExif::insert($id, $media['uri-id'], $exif);
}
return $result; return $result;
} }
@ -166,11 +173,11 @@ class Media
'url' => $href, 'url' => $href,
'size' => $length, 'size' => $length,
'mimetype' => $type, 'mimetype' => $type,
'description' => $title 'description' => $title,
]); ]);
return '[attach]href="' . $media['url'] . '" length="' . $media['size'] . return '[attach]href="' . $media['url'] . '" length="' . $media['size']
'" type="' . $media['mimetype'] . '" title="' . $media['description'] . '"[/attach]'; . '" type="' . $media['mimetype'] . '" title="' . $media['description'] . '"[/attach]';
} }
private static function setModified(array $media, string $lastModified): array private static function setModified(array $media, string $lastModified): array
@ -183,8 +190,8 @@ class Media
return $media; return $media;
} }
$media['modified'] = DateTimeFormat::utc($lastModified); $media['modified'] = DateTimeFormat::utc($lastModified);
$media['published'] = $media['published'] ?? $media['modified']; $media['published'] ??= $media['modified'];
return $media; return $media;
} }
@ -228,7 +235,7 @@ class Media
$media['mimetype'] = $curlResult->getContentType(); $media['mimetype'] = $curlResult->getContentType();
} }
if (empty($media['size']) && $is_head) { if (empty($media['size']) && $is_head) {
$media['size'] = (int)($curlResult->getHeader('Content-Length')[0] ?? strlen($curlResult->getBodyString() ?? '')); $media['size'] = (int) ($curlResult->getHeader('Content-Length')[0] ?? strlen($curlResult->getBodyString() ?? ''));
} }
$media = self::setModified($media, $curlResult->getHeader('Last-Modified')[0] ?? ''); $media = self::setModified($media, $curlResult->getHeader('Last-Modified')[0] ?? '');
} else { } else {
@ -253,6 +260,7 @@ class Media
$media['width'] = $imagedata[0]; $media['width'] = $imagedata[0];
$media['height'] = $imagedata[1]; $media['height'] = $imagedata[1];
$media['blurhash'] = $imagedata['blurhash'] ?? null; $media['blurhash'] = $imagedata['blurhash'] ?? null;
$media['exif'] = $imagedata['exif'] ?? null;
if (!empty($imagedata['description']) && empty($media['description'])) { if (!empty($imagedata['description']) && empty($media['description'])) {
$media['description'] = $imagedata['description']; $media['description'] = $imagedata['description'];
DI::logger()->debug('Detected text for image', $media); DI::logger()->debug('Detected text for image', $media);
@ -365,8 +373,8 @@ class Media
} }
if ( if (
!empty($item['plink']) && Strings::compareLink($item['plink'], $media['url']) && !empty($item['plink']) && Strings::compareLink($item['plink'], $media['url'])
parse_url($item['plink'], PHP_URL_HOST) != parse_url($item['uri'], PHP_URL_HOST) && parse_url($item['plink'], PHP_URL_HOST) != parse_url($item['uri'], PHP_URL_HOST)
) { ) {
DI::logger()->debug('Not a link to an activity', ['uri-id' => $media['uri-id'], 'url' => $media['url'], 'plink' => $item['plink'], 'uri' => $item['uri']]); DI::logger()->debug('Not a link to an activity', ['uri-id' => $media['uri-id'], 'url' => $media['url'], 'plink' => $item['plink'], 'uri' => $item['uri']]);
$media['type'] = $media['type'] == self::ACTIVITY ? self::JSON : $media['type']; $media['type'] = $media['type'] == self::ACTIVITY ? self::JSON : $media['type'];
@ -786,7 +794,7 @@ class Media
foreach (explode("\n", $curlResult->getBodyString() ?? '') as $line) { foreach (explode("\n", $curlResult->getBodyString() ?? '') as $line) {
if (strpos(trim($line), '#EXT-X-STREAM-INF') === 0) { if (strpos(trim($line), '#EXT-X-STREAM-INF') === 0) {
if (preg_match('/RESOLUTION=([\d]+)x([\d]+)/', $line, $matches)) { if (preg_match('/RESOLUTION=([\d]+)x([\d]+)/', $line, $matches)) {
$resolutions[$matches[1]] = [(int)$matches[1], (int)$matches[2]]; $resolutions[$matches[1]] = [(int) $matches[1], (int) $matches[2]];
} }
} }
} }
@ -885,7 +893,7 @@ class Media
'type' => self::IMAGE, 'type' => self::IMAGE,
'url' => $image, 'url' => $image,
'preview' => $picture[2], 'preview' => $picture[2],
'description' => $picture[3] 'description' => $picture[3],
]; ];
} elseif (self::isLinkToPhoto($picture[1], $picture[2])) { } elseif (self::isLinkToPhoto($picture[1], $picture[2])) {
$body = str_replace($picture[0], '', $body); $body = str_replace($picture[0], '', $body);
@ -895,7 +903,7 @@ class Media
'type' => self::IMAGE, 'type' => self::IMAGE,
'url' => $picture[1], 'url' => $picture[1],
'preview' => $picture[2], 'preview' => $picture[2],
'description' => $picture[3] 'description' => $picture[3],
]; ];
} elseif ($removepicturelinks) { } elseif ($removepicturelinks) {
$body = str_replace($picture[0], '', $body); $body = str_replace($picture[0], '', $body);
@ -905,7 +913,7 @@ class Media
'type' => self::UNKNOWN, 'type' => self::UNKNOWN,
'url' => $picture[1], 'url' => $picture[1],
'preview' => $picture[2], 'preview' => $picture[2],
'description' => $picture[3] 'description' => $picture[3],
]; ];
} }
} }
@ -930,7 +938,7 @@ class Media
'type' => self::IMAGE, 'type' => self::IMAGE,
'url' => $image, 'url' => $image,
'preview' => $picture[2], 'preview' => $picture[2],
'description' => null 'description' => null,
]; ];
} elseif (self::isLinkToPhoto($picture[1], $picture[2])) { } elseif (self::isLinkToPhoto($picture[1], $picture[2])) {
$body = str_replace($picture[0], '', $body); $body = str_replace($picture[0], '', $body);
@ -940,7 +948,7 @@ class Media
'type' => self::IMAGE, 'type' => self::IMAGE,
'url' => $picture[1], 'url' => $picture[1],
'preview' => $picture[2], 'preview' => $picture[2],
'description' => null 'description' => null,
]; ];
} elseif ($removepicturelinks) { } elseif ($removepicturelinks) {
$body = str_replace($picture[0], '', $body); $body = str_replace($picture[0], '', $body);
@ -950,7 +958,7 @@ class Media
'type' => self::UNKNOWN, 'type' => self::UNKNOWN,
'url' => $picture[1], 'url' => $picture[1],
'preview' => $picture[2], 'preview' => $picture[2],
'description' => null 'description' => null,
]; ];
} }
} }
@ -1354,7 +1362,7 @@ class Media
'src' => $links[0]['preview'], 'src' => $links[0]['preview'],
'height' => $links[0]['preview-height'], 'height' => $links[0]['preview-height'],
'width' => $links[0]['preview-width'], 'width' => $links[0]['preview-width'],
]] ]],
]; ];
$body .= "\n" . PageInfo::getFooterFromData($data); $body .= "\n" . PageInfo::getFooterFromData($data);
@ -1375,7 +1383,7 @@ class Media
return $body; return $body;
} }
if (strpos($body, $links[0]['url'])) { if (strpos($body, (string) $links[0]['url'])) {
return $body; return $body;
} }
@ -1416,9 +1424,9 @@ class Media
*/ */
public static function getPreviewUrlForId(int $id, string $size = ''): string public static function getPreviewUrlForId(int $id, string $size = ''): string
{ {
return DI::baseUrl() . '/photo/preview/' . return DI::baseUrl() . '/photo/preview/'
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') . . (Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '')
$id; . $id;
} }
/** /**
@ -1430,9 +1438,9 @@ class Media
*/ */
public static function getUrlForId(int $id, string $size = ''): string public static function getUrlForId(int $id, string $size = ''): string
{ {
return DI::baseUrl() . '/photo/media/' . return DI::baseUrl() . '/photo/media/'
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') . . (Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '')
$id; . $id;
} }
/** /**

View file

@ -0,0 +1,265 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Model\Post;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Photo;
use Friendica\Util\DateTimeFormat;
/**
* Class MediaExif
*
* This Model class handles exif media interactions.
*/
class MediaExif
{
/**
* Insert a post-media-exif record
*
* @param int $media_id Id of the related media entry
* @param int $uri_id Uri-Id of the related post
* @param array $data Array with Exif data
* @return bool
*/
public static function insert(int $media_id, int $uri_id, array $data): bool
{
$exif = self::translate($data);
$row = [
'media-id' => $media_id,
'uri-id' => $uri_id,
'raw-data' => json_encode($data),
'FocalLength' => self::getFocalLength($exif),
'ExposureTime' => self::getExposureTime($exif),
'LensSpecification' => self::getLensSpecification($exif),
'ApertureFNumber' => $data['COMPUTED']['ApertureFNumber'] ?? null,
'FocusDistance' => $data['COMPUTED']['FocusDistance'] ?? null,
'CCDWidth' => $data['COMPUTED']['CCDWidth'] ?? null,
'ISOSpeedRatings' => $exif['ISOSpeedRatings'] ?? null,
'DateTime' => self::getDateTime($exif),
'DateTimeOriginal' => self::getDateTimeOriginal($exif),
'DateTimeDigitized' => self::getDateTimeDigitized($exif),
'BodySerialNumber' => $exif['BodySerialNumber'] ?? null,
'Orientation' => $exif['Orientation'] ?? null,
'Artist' => $exif['Artist'] ?? null,
'Copyright' => $data['COMPUTED']['Copyright'] ?? null,
'ExpandFilm' => $exif['ExpandFilm'] ?? null,
'ExpandLens' => $exif['ExpandLens'] ?? null,
'HostComputer' => $exif['HostComputer'] ?? null,
'ImageDescription' => $exif['ImageDescription'] ?? null,
"ImageUniqueID" => $exif['ImageUniqueID'] ?? null,
"LensMake" => $exif['LensMake'] ?? null,
"LensModel" => $exif['LensModel'] ?? null,
'Make' => $exif['Make'] ?? null,
'MakerNote' => $exif['MakerNote'] ?? null,
'Model' => $exif['Model'] ?? null,
'OwnerName' => $exif['OwnerName'] ?? null,
'Software' => $exif['Software'] ?? null,
"UserComment" => $exif['UserComment'] ?? null,
];
if (isset($exif['GPSLatitude']) && isset($exif['GPSLatitudeRef']) && isset($exif['GPSLongitude']) && isset($exif['GPSLongitudeRef'])) {
$row['coord'] = Photo::getGps($exif['GPSLatitude'], $exif['GPSLatitudeRef']) . ' ' . Photo::getGps($exif['GPSLongitude'], $exif['GPSLongitudeRef']);
}
$row = DI::dbaDefinition()->truncateFieldsForTable('post-media-exif', $row);
$result = DBA::insert('post-media-exif', $row, Database::INSERT_UPDATE);
DI::logger()->info('Updated media exif', ['result' => $result, 'row' => $row]);
return $result;
}
/**
* Get the lens specification from exif data
*
* @param array $exif
* @return string|null
*/
private static function getLensSpecification(array $exif): ?string
{
if (!isset($exif['LensSpecification']) || !is_array($exif['LensSpecification']) || count($exif['LensSpecification']) != 4) {
return null;
}
$vals = [];
foreach ($exif['LensSpecification'] as $val) {
$parts = explode('/', $val);
if (count($parts) == 2 && is_numeric($parts[0]) && is_numeric($parts[1]) && $parts[1] != 0) {
$vals[] = (float) $parts[0] / (float) $parts[1];
} else {
$vals[] = null;
}
}
$LensSpecification = null;
if (count($vals) == 4 && !in_array(null, $vals)) {
if (isset($vals[0]) && $vals[0] == $vals[1]) {
$LensSpecification = round($vals[0]) . 'mm';
} elseif (isset($vals[0]) && isset($vals[1])) {
$LensSpecification = round($vals[0]) . '-' . round($vals[1]) . 'mm';
}
if (isset($vals[2]) && $vals[2] == $vals[3]) {
$LensSpecification .= ' f/' . round($vals[2], 1);
} elseif (isset($vals[2]) && isset($vals[3])) {
$LensSpecification .= ' f/' . round($vals[2], 1) . '-' . round($vals[3], 1);
}
}
return $LensSpecification;
}
/**
* Get the focal length from exif data
*
* @param array $exif
* @return string|null
*/
private static function getFocalLength(array $exif): ?string
{
if (!isset($exif['FocalLength'])) {
return null;
}
$parts = explode('/', $exif['FocalLength']);
if (count($parts) == 2 && is_numeric($parts[0]) && is_numeric($parts[1]) && $parts[1] != 0) {
return round((float) $parts[0] / (float) $parts[1], 1) . ' mm';
}
return null;
}
/**
* Get the exposure time from exif data
*
* @param array $exif
* @return string|null
*/
private static function getExposureTime(array $exif): ?string
{
if (!isset($exif['ExposureTime'])) {
return null;
}
$parts = explode('/', $exif['ExposureTime']);
if (count($parts) == 2 && is_numeric($parts[0]) && is_numeric($parts[1]) && $parts[1] != 0) {
$value = (float) $parts[0] / (float) $parts[1];
if ($value >= 1) {
return (string) round($value, 0);
} elseif ($value > 0) {
return '1/' . round(1 / $value);
}
}
return null;
}
/**
* Get the DateTime from exif data
*
* @param array $exif
* @return string|null
*/
private static function getDateTime(array $exif): ?string
{
$dateTime = $exif['DateTime'] ?? null;
if ($dateTime && isset($exif['OffsetTime'])) {
$dateTime .= ' ' . $exif['OffsetTime'];
}
return $dateTime ? DateTimeFormat::utc($dateTime) : null;
}
/**
* Get the DateTimeOriginal from exif data
*
* @param array $exif
* @return string|null
*/
private static function getDateTimeOriginal(array $exif): ?string
{
$dateTime = $exif['DateTimeOriginal'] ?? null;
if ($dateTime && isset($exif['OffsetTimeOriginal'])) {
$dateTime .= ' ' . $exif['OffsetTimeOriginal'];
}
return $dateTime ? DateTimeFormat::utc($dateTime) : null;
}
/**
* Get the DateTimeDigitized from exif data
*
* @param array $exif
* @return string|null
*/
private static function getDateTimeDigitized(array $exif): ?string
{
$dateTime = $exif['DateTimeDigitized'] ?? null;
if ($dateTime && isset($exif['OffsetTimeDigitized'])) {
$dateTime .= ' ' . $exif['OffsetTimeDigitized'];
}
return $dateTime ? DateTimeFormat::utc($dateTime) : null;
}
/**
* Translation for all tags that aren't known by "exif_read_data"
* @see https://exiv2.org/tags.html
* @param array $data
* @return array
*/
public static function translate(array $data): array
{
$translation = [
'UndefinedTag:0x4746' => 'Rating',
'UndefinedTag:0x8830' => 'SensitivityType',
'UndefinedTag:0x8831' => 'StandardOutputSensitivity',
'UndefinedTag:0x8832' => 'RecommendedExposureIndex',
'UndefinedTag:0x9010' => 'OffsetTime',
'UndefinedTag:0x9011' => 'OffsetTimeOriginal',
'UndefinedTag:0x9012' => 'OffsetTimeDigitized',
'UndefinedTag:0x9402' => 'Pressure',
'UndefinedTag:0xA431' => 'BodySerialNumber',
'UndefinedTag:0xA432' => 'LensSpecification',
'UndefinedTag:0xA433' => 'LensMake',
'UndefinedTag:0xA434' => 'LensModel',
'UndefinedTag:0xA460' => 'CompositeImage',
'UndefinedTag:0xAFC1' => 'ExpandLens',
'UndefinedTag:0xAFC2' => 'ExpandFilm',
'UndefinedTag:0xC4A5' => 'PrintImageMatching',
];
$exif = [];
foreach ($data as $key => $value) {
if (isset($translation[$key])) {
$exif[$translation[$key]] = $value;
} else {
$exif[$key] = $value;
}
}
$translation = [
'InternalSerialNumber' => 'BodySerialNumber',
'Author' => 'Artist',
];
foreach ($translation as $key => $value) {
if (!isset($exif[$value]) && isset($exif[$key])) {
$exif[$value] = $exif[$key];
}
unset($exif[$key]);
}
if (isset($data['COMPUTED']['UserComment'])) {
$exif['UserComment'] = $data['COMPUTED']['UserComment'];
}
if (!isset($exif['UserComment']) && isset($data['COMMENT']) && is_array($data['COMMENT'])) {
$exif['UserComment'] = implode("\n", $data['COMMENT']);
unset($exif['COMMENT']);
}
return $exif;
}
}

View file

@ -7,7 +7,7 @@
namespace Friendica\Model\Post; namespace Friendica\Model\Post;
use \BadMethodCallException; use BadMethodCallException;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
@ -73,4 +73,15 @@ class Thread
{ {
return DBA::delete('post-thread', $conditions); return DBA::delete('post-thread', $conditions);
} }
/**
* Check if a post-thread entry exists for the given URI ID
*
* @param integer $uri_id
* @return boolean exists?
*/
public static function exists(int $uri_id): bool
{
return DBA::exists('post-thread', ['uri-id' => $uri_id]);
}
} }

View file

@ -9,6 +9,7 @@ namespace Friendica\Model;
use Friendica\App\Mode; use Friendica\App\Mode;
use Friendica\AppHelper; use Friendica\AppHelper;
use Friendica\Content\Feature;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Content\Widget\ContactBlock; use Friendica\Content\Widget\ContactBlock;
use Friendica\Core\Cache\Enum\Duration; use Friendica\Core\Cache\Enum\Duration;
@ -309,7 +310,7 @@ class Profile
} }
$local_user_is_self = DI::userSession()->getMyUrl() && ($profile['url'] == DI::userSession()->getMyUrl()); $local_user_is_self = DI::userSession()->getMyUrl() && ($profile['url'] == DI::userSession()->getMyUrl());
$visitor_is_authenticated = (bool)DI::userSession()->getMyUrl(); $visitor_is_authenticated = (bool) DI::userSession()->getMyUrl();
$visitor_is_following = in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND]) $visitor_is_following = in_array($visitor_contact['rel'] ?? 0, [Contact::FOLLOWER, Contact::FRIEND])
|| in_array($profile_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]); || in_array($profile_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]);
$visitor_is_followed = in_array($visitor_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND]) $visitor_is_followed = in_array($visitor_contact['rel'] ?? 0, [Contact::SHARING, Contact::FRIEND])
@ -319,7 +320,7 @@ class Profile
if (!$local_user_is_self) { if (!$local_user_is_self) {
if (!$visitor_is_authenticated) { if (!$visitor_is_authenticated) {
// Remote follow is only available for local profiles // Remote follow is only available for local profiles
if (!empty($profile['nickname']) && strpos($profile_url, (string)DI::baseUrl()) === 0) { if (!empty($profile['nickname']) && strpos($profile_url, (string) DI::baseUrl()) === 0) {
$follow_link = 'profile/' . $profile['nickname'] . '/remote_follow'; $follow_link = 'profile/' . $profile['nickname'] . '/remote_follow';
} }
} else { } else {
@ -444,12 +445,23 @@ class Profile
} }
$network_url = 'contact/' . $cid . '/conversations'; $network_url = 'contact/' . $cid . '/conversations';
$member_since = null;
if (isset($p['register_date']) && Feature::isEnabled($p['uid'], Feature::MEMBER_SINCE)) {
$member_since = [ DI::l10n()->t('Joined:'), DI::l10n()->mediumDate($p['register_date']) ];
}
$tpl = Renderer::getMarkupTemplate('profile/vcard.tpl'); $tpl = Renderer::getMarkupTemplate('profile/vcard.tpl');
$o .= Renderer::replaceMacros($tpl, [ $o .= Renderer::replaceMacros($tpl, [
'$profile' => $p, '$is_owner' => DI::userSession()->getLocalUserId() == $profile['uid'],
'$profile' => $p,
'$edit_profile_link' => [
'url' => 'settings/profile',
'text' => DI::l10n()->t('Edit profile'),
],
'$picture_dest_url' => $picture_dest_url, '$picture_dest_url' => $picture_dest_url,
'$change_profile_picture_text' => $change_profile_picture_text, '$change_profile_picture_text' => $change_profile_picture_text,
'$xmpp' => $xmpp, '$xmpp' => $xmpp,
'$member_since' => $member_since,
'$matrix' => $matrix, '$matrix' => $matrix,
'$follow' => DI::l10n()->t('Follow'), '$follow' => DI::l10n()->t('Follow'),
'$follow_link' => $follow_link, '$follow_link' => $follow_link,
@ -521,8 +533,6 @@ class Profile
*/ */
public static function getBirthdays(int $uid): string public static function getBirthdays(int $uid): string
{ {
$bd_short = DI::l10n()->t('F d');
$cacheKey = 'get_birthdays:' . $uid; $cacheKey = 'get_birthdays:' . $uid;
$events = DI::cache()->get($cacheKey); $events = DI::cache()->get($cacheKey);
if (is_null($events)) { if (is_null($events)) {
@ -542,7 +552,7 @@ class Profile
Contact::FRIEND, Contact::FRIEND,
$uid, $uid,
DateTimeFormat::utc('now + 6 days'), DateTimeFormat::utc('now + 6 days'),
DateTimeFormat::utcNow() DateTimeFormat::utcNow(),
); );
if (DBA::isResult($result)) { if (DBA::isResult($result)) {
$events = DBA::toArray($result); $events = DBA::toArray($result);
@ -585,7 +595,7 @@ class Profile
'id' => $event['id'], 'id' => $event['id'],
'link' => Contact::magicLinkById($event['cid']), 'link' => Contact::magicLinkById($event['cid']),
'title' => $event['name'], 'title' => $event['name'],
'date' => DI::l10n()->getDay(DateTimeFormat::local($event['start'], $bd_short)) . (($today) ? ' ' . DI::l10n()->t('[today]') : '') 'date' => DI::l10n()->longDate($event['start']) . (($today) ? ' ' . DI::l10n()->t('[today]') : ''),
]; ];
} }
} }
@ -598,7 +608,7 @@ class Profile
'$event_title' => DI::l10n()->t('Birthdays this week:'), '$event_title' => DI::l10n()->t('Birthdays this week:'),
'$events' => $tpl_events, '$events' => $tpl_events,
'$lbr' => '{', // raw brackets mess up if/endif macro processing '$lbr' => '{', // raw brackets mess up if/endif macro processing
'$rbr' => '}' '$rbr' => '}',
]); ]);
} }
@ -611,12 +621,11 @@ class Profile
*/ */
public static function getEventsReminderHTML(int $uid, int $pcid): string public static function getEventsReminderHTML(int $uid, int $pcid): string
{ {
$bd_format = DI::l10n()->t('g A l F d'); // 8 AM Friday January 18
$classtoday = ''; $classtoday = '';
$condition = [ $condition = [
"`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?", "`uid` = ? AND `type` != 'birthday' AND `start` < ? AND `start` >= ?",
$uid, DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days') $uid, DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utc('now - 1 days'),
]; ];
$s = DBA::select('event', [], $condition, ['order' => ['start']]); $s = DBA::select('event', [], $condition, ['order' => ['start']]);
@ -630,7 +639,7 @@ class Profile
$condition = [ $condition = [
'parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => $pcid, 'parent-uri' => $rr['uri'], 'uid' => $rr['uid'], 'author-id' => $pcid,
'vid' => [Verb::getID(Activity::ATTEND), Verb::getID(Activity::ATTENDMAYBE)], 'vid' => [Verb::getID(Activity::ATTEND), Verb::getID(Activity::ATTENDMAYBE)],
'visible' => true, 'deleted' => false 'visible' => true, 'deleted' => false,
]; ];
if (!Post::exists($condition)) { if (!Post::exists($condition)) {
continue; continue;
@ -666,7 +675,7 @@ class Profile
$rr['title'] = $title; $rr['title'] = $title;
$rr['description'] = $description; $rr['description'] = $description;
$rr['date'] = DI::l10n()->getDay(DateTimeFormat::local($rr['start'], $bd_format)) . (($today) ? ' ' . DI::l10n()->t('[today]') : ''); $rr['date'] = DI::l10n()->fullDateTime($rr['start']) . ($today ? ' ' . DI::l10n()->t('[today]') : '');
$rr['startime'] = $strt; $rr['startime'] = $strt;
$rr['today'] = $today; $rr['today'] = $today;
@ -730,7 +739,7 @@ class Profile
(`pub_keywords` LIKE ?) OR (`pub_keywords` LIKE ?) OR
(`prv_keywords` LIKE ?))", (`prv_keywords` LIKE ?))",
$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm,
$searchTerm, $searchTerm, $searchTerm, $searchTerm $searchTerm, $searchTerm, $searchTerm, $searchTerm,
]; ];
} else { } else {
$condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]; $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false];
@ -772,7 +781,7 @@ class Profile
if (!$profile['is-default']) { if (!$profile['is-default']) {
$contacts = Contact::selectToArray(['id'], [ $contacts = Contact::selectToArray(['id'], [
'uid' => $profile['uid'], 'uid' => $profile['uid'],
'profile-id' => $profile['id'] 'profile-id' => $profile['id'],
]); ]);
if (!count($contacts)) { if (!count($contacts)) {
// No contact visibility selected defaults to user-only permission // No contact visibility selected defaults to user-only permission
@ -783,8 +792,8 @@ class Profile
$permissionSet = DI::permissionSet()->selectOrCreate( $permissionSet = DI::permissionSet()->selectOrCreate(
new PermissionSet( new PermissionSet(
$profile['uid'], $profile['uid'],
array_column($contacts, 'id') ?? [] array_column($contacts, 'id') ?? [],
) ),
); );
$order = 1; $order = 1;
@ -819,7 +828,7 @@ class Profile
$order, $order,
trim($label, ':'), trim($label, ':'),
$profile[$field], $profile[$field],
$permissionSet $permissionSet,
)); ));
} }
@ -851,7 +860,7 @@ class Profile
} }
$parent = User::getOwnerDataById($parent_uid); $parent = User::getOwnerDataById($parent_uid);
if (strpos($about, $parent['addr']) || strpos($about, $parent['url'])) { if (strpos($about, (string) $parent['addr']) || strpos($about, (string) $parent['url'])) {
return $about; return $about;
} }

View file

@ -54,13 +54,13 @@ class User
* *
* @{ * @{
*/ */
const PAGE_FLAGS_NORMAL = 0; public const PAGE_FLAGS_NORMAL = 0;
const PAGE_FLAGS_SOAPBOX = 1; public const PAGE_FLAGS_SOAPBOX = 1;
const PAGE_FLAGS_COMMUNITY = 2; public const PAGE_FLAGS_COMMUNITY = 2;
const PAGE_FLAGS_FREELOVE = 3; public const PAGE_FLAGS_FREELOVE = 3;
const PAGE_FLAGS_BLOG = 4; public const PAGE_FLAGS_BLOG = 4;
const PAGE_FLAGS_PRVGROUP = 5; public const PAGE_FLAGS_PRVGROUP = 5;
const PAGE_FLAGS_COMM_MAN = 6; public const PAGE_FLAGS_COMM_MAN = 6;
/** /**
* @} * @}
*/ */
@ -84,12 +84,12 @@ class User
* This will only be assigned to contacts, not to user accounts * This will only be assigned to contacts, not to user accounts
* @{ * @{
*/ */
const ACCOUNT_TYPE_PERSON = 0; public const ACCOUNT_TYPE_PERSON = 0;
const ACCOUNT_TYPE_ORGANISATION = 1; public const ACCOUNT_TYPE_ORGANISATION = 1;
const ACCOUNT_TYPE_NEWS = 2; public const ACCOUNT_TYPE_NEWS = 2;
const ACCOUNT_TYPE_COMMUNITY = 3; public const ACCOUNT_TYPE_COMMUNITY = 3;
const ACCOUNT_TYPE_RELAY = 4; public const ACCOUNT_TYPE_RELAY = 4;
const ACCOUNT_TYPE_DELETED = 127; public const ACCOUNT_TYPE_DELETED = 127;
/** /**
* @} * @}
*/ */
@ -170,7 +170,7 @@ class User
$system['region'] = ''; $system['region'] = '';
$system['postal-code'] = ''; $system['postal-code'] = '';
$system['country-name'] = ''; $system['country-name'] = '';
$system['homepage'] = (string)DI::baseUrl(); $system['homepage'] = (string) DI::baseUrl();
$system['dob'] = '0000-00-00'; $system['dob'] = '0000-00-00';
// Ensure that the user contains data // Ensure that the user contains data
@ -503,7 +503,7 @@ class User
// Check if the avatar field is filled and the photo directs to the correct path // Check if the avatar field is filled and the photo directs to the correct path
$avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]); $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
if (DBA::isResult($avatar)) { if (DBA::isResult($avatar)) {
$repair = empty($owner['avatar']) || !strpos($owner['photo'], $avatar['resource-id']); $repair = empty($owner['avatar']) || !strpos($owner['photo'], (string) $avatar['resource-id']);
} }
} }
@ -755,7 +755,7 @@ class User
'username' => $username, 'username' => $username,
'password' => $password, 'password' => $password,
'authenticated' => 0, 'authenticated' => 0,
'user_record' => null 'user_record' => null,
]; ];
$eventDispatcher = DI::eventDispatcher(); $eventDispatcher = DI::eventDispatcher();
@ -815,7 +815,7 @@ class User
'uid' => $user_info, 'uid' => $user_info,
'account_expired' => false, 'account_expired' => false,
'account_removed' => false, 'account_removed' => false,
'verified' => true 'verified' => true,
]; ];
if (!$with_blocked) { if (!$with_blocked) {
$condition = DBA::mergeConditions($condition, ['blocked' => false]); $condition = DBA::mergeConditions($condition, ['blocked' => false]);
@ -825,7 +825,7 @@ class User
$condition = [ $condition = [
"(`email` = ? OR `username` = ? OR `nickname` = ?) "(`email` = ? OR `username` = ? OR `nickname` = ?)
AND `verified` AND NOT `account_removed` AND NOT `account_expired`", AND `verified` AND NOT `account_removed` AND NOT `account_expired`",
$user_info, $user_info, $user_info $user_info, $user_info, $user_info,
]; ];
if (!$with_blocked) { if (!$with_blocked) {
$condition = DBA::mergeConditions($condition, ['blocked' => false]); $condition = DBA::mergeConditions($condition, ['blocked' => false]);
@ -905,7 +905,7 @@ class User
'code' => $e->getCode(), 'code' => $e->getCode(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'trace' => $e->getTraceAsString() 'trace' => $e->getTraceAsString(),
]); ]);
return false; return false;
@ -1008,7 +1008,7 @@ class User
'password' => $password_hashed, 'password' => $password_hashed,
'pwdreset' => null, 'pwdreset' => null,
'pwdreset_time' => null, 'pwdreset_time' => null,
'legacy_password' => false 'legacy_password' => false,
]; ];
return DBA::update('user', $fields, ['uid' => $uid]); return DBA::update('user', $fields, ['uid' => $uid]);
} }
@ -1025,7 +1025,7 @@ class User
{ {
return DBA::exists('user', [ return DBA::exists('user', [
'uid' => $uid, 'uid' => $uid,
'email' => self::getAdminEmailList() 'email' => self::getAdminEmailList(),
]); ]);
} }
@ -1337,7 +1337,7 @@ class User
'language' => $language, 'language' => $language,
'timezone' => 'UTC', 'timezone' => 'UTC',
'register_date' => DateTimeFormat::utcNow(), 'register_date' => DateTimeFormat::utcNow(),
'default-location' => '' 'default-location' => '',
]); ]);
if ($insert_result) { if ($insert_result) {
@ -1566,7 +1566,7 @@ class User
$user, $user,
DI::config()->get('config', 'sitename'), DI::config()->get('config', 'sitename'),
DI::baseUrl(), DI::baseUrl(),
($register['password'] ?? '') ?: 'Sent in a previous email' ($register['password'] ?? '') ?: 'Sent in a previous email',
); );
} }
@ -1597,8 +1597,8 @@ class User
// Delete the avatar // Delete the avatar
Photo::delete(['uid' => $register['uid']]); Photo::delete(['uid' => $register['uid']]);
return DBA::delete('user', ['uid' => $register['uid']]) && return DBA::delete('user', ['uid' => $register['uid']])
Register::deleteByHash($register['hash']); && Register::deleteByHash($register['hash']);
} }
/** /**
@ -1616,9 +1616,9 @@ class User
*/ */
public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT, string $avatar = ''): bool public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT, string $avatar = ''): bool
{ {
if (empty($name) || if (empty($name)
empty($email) || || empty($email)
empty($nick)) { || empty($nick)) {
throw new HTTPException\InternalServerErrorException('Invalid arguments.'); throw new HTTPException\InternalServerErrorException('Invalid arguments.');
} }
@ -1629,7 +1629,7 @@ class User
'nickname' => $nick, 'nickname' => $nick,
'verified' => 1, 'verified' => 1,
'language' => $lang, 'language' => $lang,
'photo' => $avatar 'photo' => $avatar,
]); ]);
$user = $result['user']; $user = $result['user'];
@ -1702,7 +1702,7 @@ class User
$sitename, $sitename,
$siteurl, $siteurl,
$user['nickname'], $user['nickname'],
$password $password,
)); ));
$email = DI::emailer() $email = DI::emailer()
@ -1736,7 +1736,7 @@ class User
Thank you for registering at %2$s. Your account has been created. Thank you for registering at %2$s. Your account has been created.
', ',
$user['username'], $user['username'],
$sitename $sitename,
)); ));
$body = Strings::deindent($l10n->t( $body = Strings::deindent($l10n->t(
' '
@ -1769,7 +1769,7 @@ class User
$sitename, $sitename,
$siteurl, $siteurl,
$user['username'], $user['username'],
$password $password,
)); ));
$email = DI::emailer() $email = DI::emailer()
@ -1877,14 +1877,14 @@ class User
$identities = [[ $identities = [[
'uid' => $user['uid'], 'uid' => $user['uid'],
'username' => $user['username'], 'username' => $user['username'],
'nickname' => $user['nickname'] 'nickname' => $user['nickname'],
]]; ]];
// Then add all the children // Then add all the children
$r = DBA::select( $r = DBA::select(
'user', 'user',
['uid', 'username', 'nickname'], ['uid', 'username', 'nickname'],
['parent-uid' => $user['uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false] ['parent-uid' => $user['uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false],
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r)); $identities = array_merge($identities, DBA::toArray($r));
@ -1894,7 +1894,7 @@ class User
$r = DBA::select( $r = DBA::select(
'user', 'user',
['uid', 'username', 'nickname'], ['uid', 'username', 'nickname'],
['uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false] ['uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false],
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = DBA::toArray($r); $identities = DBA::toArray($r);
@ -1904,7 +1904,7 @@ class User
$r = DBA::select( $r = DBA::select(
'user', 'user',
['uid', 'username', 'nickname'], ['uid', 'username', 'nickname'],
['parent-uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false] ['parent-uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false],
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r)); $identities = array_merge($identities, DBA::toArray($r));
@ -1916,7 +1916,7 @@ class User
FROM `manage` FROM `manage`
INNER JOIN `user` ON `manage`.`mid` = `user`.`uid` INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
WHERE NOT `user`.`account_removed` AND `manage`.`uid` = ?", WHERE NOT `user`.`account_removed` AND `manage`.`uid` = ?",
$user['uid'] $user['uid'],
); );
if (DBA::isResult($r)) { if (DBA::isResult($r)) {
$identities = array_merge($identities, DBA::toArray($r)); $identities = array_merge($identities, DBA::toArray($r));
@ -1978,7 +1978,7 @@ class User
['uid', 'last-activity', 'last-item'], ['uid', 'last-activity', 'last-item'],
["`verified` AND `last-activity` > ? AND NOT `blocked` ["`verified` AND `last-activity` > ? AND NOT `blocked`
AND NOT `account_removed` AND NOT `account_expired`", AND NOT `account_removed` AND NOT `account_expired`",
DBA::NULL_DATETIME] DBA::NULL_DATETIME],
); );
if (!DBA::isResult($userStmt)) { if (!DBA::isResult($userStmt)) {
return $statistics; return $statistics;

View file

@ -17,15 +17,15 @@ use Friendica\Core\Config\Capability\IManageConfigValues;
class Cookie class Cookie
{ {
/** @var int Default expire duration in days */ /** @var int Default expire duration in days */
const DEFAULT_EXPIRE = 7; public const DEFAULT_EXPIRE = 7;
/** @var string The name of the Friendica cookie */ /** @var string The name of the Friendica cookie */
const NAME = 'Friendica'; public const NAME = 'Friendica';
/** @var string The path of the Friendica cookie */ /** @var string The path of the Friendica cookie */
const PATH = '/'; public const PATH = '/';
/** @var string The domain name of the Friendica cookie */ /** @var string The domain name of the Friendica cookie */
const DOMAIN = ''; public const DOMAIN = '';
/** @var bool True, if the cookie should only be accessible through HTTP */ /** @var bool True, if the cookie should only be accessible through HTTP */
const HTTPONLY = true; public const HTTPONLY = true;
/** @var string The remote address of this node */ /** @var string The remote address of this node */
private $remoteAddr; private $remoteAddr;
@ -49,8 +49,11 @@ class Cookie
$this->sslEnabled = $baseURL->getScheme() === 'https'; $this->sslEnabled = $baseURL->getScheme() === 'https';
$this->sitePrivateKey = $config->get('system', 'site_prvkey'); $this->sitePrivateKey = $config->get('system', 'site_prvkey');
$authCookieDays = $config->get('system', 'auth_cookie_lifetime', $authCookieDays = $config->get(
self::DEFAULT_EXPIRE); 'system',
'auth_cookie_lifetime',
self::DEFAULT_EXPIRE,
);
$this->lifetime = $authCookieDays * 24 * 60 * 60; $this->lifetime = $authCookieDays * 24 * 60 * 60;
$this->remoteAddr = $request->getRemoteAddress(); $this->remoteAddr = $request->getRemoteAddress();
@ -120,8 +123,8 @@ class Cookie
*/ */
public function reset(array $data): bool public function reset(array $data): bool
{ {
return $this->clear() && return $this->clear()
$this->setMultiple($data); && $this->setMultiple($data);
} }
/** /**
@ -144,7 +147,7 @@ class Cookie
return $this->setCookie( return $this->setCookie(
json_encode(['ip' => $this->remoteAddr] + $this->data), json_encode(['ip' => $this->remoteAddr] + $this->data),
$this->lifetime + time(), $this->lifetime + time(),
$this->sslEnabled $this->sslEnabled,
); );
} }
@ -160,10 +163,12 @@ class Cookie
* @return bool If output exists prior to calling this function, * @return bool If output exists prior to calling this function,
* *
*/ */
protected function setCookie(string $value = null, int $expire = null, protected function setCookie(
bool $secure = null): bool string $value = null,
{ int $expire = null,
return setcookie(self::NAME, $value, $expire, self::PATH, self::DOMAIN, $secure, self::HTTPONLY); bool $secure = null
): bool {
return setcookie(self::NAME, $value, ['expires' => $expire, 'path' => self::PATH, 'domain' => self::DOMAIN, 'secure' => $secure, 'httponly' => self::HTTPONLY]);
} }
/** /**
@ -180,7 +185,7 @@ class Cookie
return hash_hmac( return hash_hmac(
'sha256', 'sha256',
hash_hmac('sha256', $privateData, $privateKey), hash_hmac('sha256', $privateData, $privateKey),
$this->sitePrivateKey $this->sitePrivateKey,
); );
} }
@ -196,7 +201,7 @@ class Cookie
{ {
return hash_equals( return hash_equals(
$this->hashPrivateData($privateData, $privateKey), $this->hashPrivateData($privateData, $privateKey),
$hash $hash,
); );
} }
} }

View file

@ -53,6 +53,9 @@ class Inbox extends BaseApi
$inbox = ActivityPub\ClientToServer::getPublicInbox($uid, $page, $request['max_id'] ?? null); $inbox = ActivityPub\ClientToServer::getPublicInbox($uid, $page, $request['max_id'] ?? null);
} }
// Relaxed CORS header already authorized
header('Access-Control-Allow-Origin: *');
$this->jsonExit($inbox, 'application/activity+json'); $this->jsonExit($inbox, 'application/activity+json');
} }

View file

@ -71,20 +71,20 @@ class Settings extends BaseAdmin
$t = Renderer::getMarkupTemplate('admin/logs/settings.tpl'); $t = Renderer::getMarkupTemplate('admin/logs/settings.tpl');
return Renderer::replaceMacros($t, [ return Renderer::replaceMacros($t, [
'$title' => DI::l10n()->t('Administration'), '$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('Logs'), '$page' => DI::l10n()->t('Log settings'),
'$submit' => DI::l10n()->t('Save Settings'), '$submit' => DI::l10n()->t('Save Settings'),
'$clear' => DI::l10n()->t('Clear'), '$clear' => DI::l10n()->t('Clear'),
'$logname' => DI::config()->get('system', 'logfile'), '$logname' => DI::config()->get('system', 'logfile'),
// see /help/smarty3-templates#1_1 on any Friendica node // see /help/smarty3-templates#1_1 on any Friendica node
'$debugging' => ['debugging', DI::l10n()->t('Enable Debugging'), DI::config()->get('system', 'debugging'), !DI::config()->isWritable('system', 'debugging') ? DI::l10n()->t('<strong>Read-only</strong> because it is set by an environment variable') : '', !DI::config()->isWritable('system', 'debugging') ? 'disabled' : ''], '$debugging' => ['debugging', DI::l10n()->t('Enable Debugging'), DI::config()->get('system', 'debugging'), !DI::config()->isWritable('system', 'debugging') ? DI::l10n()->t('<strong>Read-only</strong> because it is set by an environment variable') : '', !DI::config()->isWritable('system', 'debugging') ? 'disabled' : ''],
'$logfile' => ['logfile', DI::l10n()->t('Log file'), DI::config()->get('system', 'logfile'), DI::l10n()->t('Must be writable by web server. Relative to your Friendica top-level directory.') . (!DI::config()->isWritable('system', 'logfile') ? '<br>' . DI::l10n()->t('<strong>Read-only</strong> because it is set by an environment variable') : ''), '', !DI::config()->isWritable('system', 'logfile') ? 'disabled' : ''], '$logfile' => ['logfile', DI::l10n()->t('Log file'), DI::config()->get('system', 'logfile'), DI::l10n()->t('Must be writable by web server. Relative to your Friendica top-level directory.') . (!DI::config()->isWritable('system', 'logfile') ? '<br>' . DI::l10n()->t('<strong>Read-only</strong> because it is set by an environment variable') : ''), '', !DI::config()->isWritable('system', 'logfile') ? 'disabled' : ''],
'$loglevel' => ['loglevel', DI::l10n()->t("Log level"), DI::config()->get('system', 'loglevel'), !DI::config()->isWritable('system', 'loglevel') ? DI::l10n()->t('<strong>Read-only</strong> because it is set by an environment variable') : '', $log_choices, !DI::config()->isWritable('system', 'loglevel') ? 'disabled' : ''], '$loglevel' => ['loglevel', DI::l10n()->t("Log level"), DI::config()->get('system', 'loglevel'), !DI::config()->isWritable('system', 'loglevel') ? DI::l10n()->t('<strong>Read-only</strong> because it is set by an environment variable') : '', $log_choices, !DI::config()->isWritable('system', 'loglevel') ? 'disabled' : ''],
'$form_security_token' => self::getFormSecurityToken("admin_logs"), '$form_security_token' => self::getFormSecurityToken("admin_logs"),
'$phpheader' => DI::l10n()->t("PHP logging"), '$phpheader' => DI::l10n()->t("PHP logging"),
'$phphint' => DI::l10n()->t("To temporarily enable logging of PHP errors and warnings you can prepend the following to the index.php file of your installation. The filename set in the 'error_log' line is relative to the friendica top-level directory and must be writeable by the web server. The option '1' for 'log_errors' and 'display_errors' is to enable these options, set to '0' to disable them."), '$phphint' => DI::l10n()->t("To temporarily enable logging of PHP errors and warnings you can prepend the following to the index.php file of your installation. The filename set in the 'error_log' line is relative to the friendica top-level directory and must be writeable by the web server. The option '1' for 'log_errors' and 'display_errors' is to enable these options, set to '0' to disable them."),
'$phplogcode' => "error_reporting(E_ERROR | E_WARNING | E_PARSE);\nini_set('error_log','php.out');\nini_set('log_errors','1');\nini_set('display_errors', '1');", '$phplogcode' => "error_reporting(E_ERROR | E_WARNING | E_PARSE);\nini_set('error_log','php.out');\nini_set('log_errors','1');\nini_set('display_errors', '1');",
'$phplogenabled' => $phplogenabled, '$phplogenabled' => $phplogenabled,
]); ]);
} }
} }

View file

@ -45,7 +45,7 @@ class View extends BaseAdmin
'context' => ['', 'index', 'worker', 'daemon'], 'context' => ['', 'index', 'worker', 'daemon'],
]; ];
$filters = [ $filters = [
'level' => $_GET['level'] ?? '', 'level' => $_GET['level'] ?? '',
'context' => $_GET['context'] ?? '', 'context' => $_GET['context'] ?? '',
]; ];
foreach ($filters as $k => $v) { foreach ($filters as $k => $v) {
@ -68,11 +68,10 @@ class View extends BaseAdmin
} }
} }
return Renderer::replaceMacros($t, [ return Renderer::replaceMacros($t, [
'$title' => DI::l10n()->t('Administration'), '$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('View Logs'), '$page' => DI::l10n()->t('View Logs'),
'$l10n' => [ '$l10n' => [
'Search' => DI::l10n()->t('Search'), 'Search' => DI::l10n()->t('Search in logs'),
'Search_in_logs' => DI::l10n()->t('Search in logs'),
'Show_all' => DI::l10n()->t('Show all'), 'Show_all' => DI::l10n()->t('Show all'),
'Date' => DI::l10n()->t('Date'), 'Date' => DI::l10n()->t('Date'),
'Level' => DI::l10n()->t('Level'), 'Level' => DI::l10n()->t('Level'),
@ -96,7 +95,7 @@ class View extends BaseAdmin
'$filters' => $filters, '$filters' => $filters,
'$filtersvalues' => $filters_valid_values, '$filtersvalues' => $filters_valid_values,
'$error' => $error, '$error' => $error,
'$logname' => DI::config()->get('system', 'logfile'), '$logname' => DI::l10n()->t('Current log path: %s', DI::config()->get('system', 'logfile')),
]); ]);
} }
} }

View file

@ -34,11 +34,11 @@ class Queue extends BaseAdmin
if ($status == 'deferred') { if ($status == 'deferred') {
$condition = ["NOT `done` AND `retrial` > ?", 0]; $condition = ["NOT `done` AND `retrial` > ?", 0];
$sub_title = DI::l10n()->t('Inspect Deferred Worker Queue'); $sub_title = DI::l10n()->t('Inspect Deferred Worker Queue');
$info = DI::l10n()->t("This page lists the deferred worker jobs. This are jobs that couldn't be executed at the first time."); $info = DI::l10n()->t("This page lists the deferred worker jobs. This are jobs that couldn't be executed at the first time.");
} else { } else {
$condition = ["NOT `done` AND `retrial` = ?", 0]; $condition = ["NOT `done` AND `retrial` = ?", 0];
$sub_title = DI::l10n()->t('Inspect Worker Queue'); $sub_title = DI::l10n()->t('Inspect Worker Queue');
$info = DI::l10n()->t('This page lists the currently queued worker jobs. These jobs are handled by the worker cronjob you\'ve set up during install.'); $info = DI::l10n()->t('This page lists the currently queued worker jobs. These jobs are handled by the worker cronjob you\'ve set up during install.');
} }
// @TODO Move to Model\WorkerQueue::getEntries() // @TODO Move to Model\WorkerQueue::getEntries()
@ -47,27 +47,32 @@ class Queue extends BaseAdmin
$r = []; $r = [];
while ($entry = DBA::fetch($entries)) { while ($entry = DBA::fetch($entries)) {
// fix GH-5469. ref: src/Core/Worker.php:217 // fix GH-5469. ref: src/Core/Worker.php:217
$entry['parameter'] = Arrays::recursiveImplode(json_decode($entry['parameter'], true), ': '); $param = Arrays::recursiveImplode(json_decode($entry['parameter'], true), ': ');
$entry['created'] = DateTimeFormat::local($entry['created']); // Truncate long parameters to prevent text overflow
$entry['next_try'] = DateTimeFormat::local($entry['next_try']); if (strlen($param) > 300) {
$r[] = $entry; $param = substr($param, 0, 300) . '...';
}
$entry['parameter'] = $param;
$entry['created'] = DateTimeFormat::local($entry['created']);
$entry['next_try'] = DateTimeFormat::local($entry['next_try']);
$r[] = $entry;
} }
DBA::close($entries); DBA::close($entries);
$t = Renderer::getMarkupTemplate('admin/queue.tpl'); $t = Renderer::getMarkupTemplate('admin/queue.tpl');
return Renderer::replaceMacros($t, [ return Renderer::replaceMacros($t, [
'$title' => DI::l10n()->t('Administration'), '$title' => DI::l10n()->t('Administration'),
'$page' => $sub_title, '$page' => $sub_title,
'$count' => count($r), '$count' => count($r),
'$id_header' => DI::l10n()->t('ID'), '$id_header' => DI::l10n()->t('ID'),
'$command_header' => DI::l10n()->t('Command'), '$command_header' => DI::l10n()->t('Command'),
'$param_header' => DI::l10n()->t('Job Parameters'), '$param_header' => DI::l10n()->t('Job Parameters'),
'$created_header' => DI::l10n()->t('Created'), '$created_header' => DI::l10n()->t('Created'),
'$next_try_header' => DI::l10n()->t('Next Try'), '$next_try_header' => DI::l10n()->t('Next Try'),
'$prio_header' => DI::l10n()->t('Priority'), '$prio_header' => DI::l10n()->t('Priority'),
'$info' => $info, '$info' => $info,
'$status' => $status, '$status' => $status,
'$entries' => $r, '$entries' => $r,
]); ]);
} }
} }

View file

@ -13,7 +13,6 @@ use Friendica\Core\Storage\Exception\InvalidClassStorageException;
use Friendica\Core\Storage\Capability\ICanConfigureStorage; use Friendica\Core\Storage\Capability\ICanConfigureStorage;
use Friendica\Core\Storage\Capability\ICanWriteToStorage; use Friendica\Core\Storage\Capability\ICanWriteToStorage;
use Friendica\Module\BaseAdmin; use Friendica\Module\BaseAdmin;
use Friendica\Util\Strings;
class Storage extends BaseAdmin class Storage extends BaseAdmin
{ {
@ -62,7 +61,7 @@ class Storage extends BaseAdmin
} }
} }
if (!empty($_POST['submit_save_set']) && DI::config()->isWritable('storage', 'name') ) { if (!empty($_POST['submit_save_set']) && DI::config()->isWritable('storage', 'name')) {
try { try {
$newstorage = DI::storageManager()->getWritableStorageByName($storagebackend); $newstorage = DI::storageManager()->getWritableStorageByName($storagebackend);

View file

@ -8,6 +8,7 @@
namespace Friendica\Module\Admin; namespace Friendica\Module\Admin;
use Friendica\App; use Friendica\App;
use Friendica\Core\Addon\Exception\InvalidAddonException;
use Friendica\Core\Config\ValueObject\Cache; use Friendica\Core\Config\ValueObject\Cache;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\Update; use Friendica\Core\Update;
@ -17,7 +18,6 @@ use Friendica\DI;
use Friendica\Core\Config\Factory\Config; use Friendica\Core\Config\Factory\Config;
use Friendica\Module\BaseAdmin; use Friendica\Module\BaseAdmin;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\Probe;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
class Summary extends BaseAdmin class Summary extends BaseAdmin
@ -99,13 +99,13 @@ class Summary extends BaseAdmin
} }
// Check server vitality // Check server vitality
if (!self::checkSelfHostMeta()) { if (!self::checkSelfNodeinfo()) {
$well_known = DI::baseUrl() . Probe::HOST_META; $well_known = DI::baseUrl() . '/.well-known/nodeinfo';
$warningtext[] = DI::l10n()->t( $warningtext[] = DI::l10n()->t(
'<a href="%s">%s</a> is not reachable on your system. This is a severe configuration issue that prevents server to server communication. See <a href="%s">the installation page</a> for help.', '<a href="%s">%s</a> is not reachable on your system. This is a severe configuration issue that prevents server to server communication. See <a href="%s">the installation page</a> for help.',
$well_known, $well_known,
$well_known, $well_known,
DI::baseUrl() . '/help/admin/install' DI::baseUrl() . '/help/admin/install',
); );
} }
@ -132,7 +132,7 @@ class Summary extends BaseAdmin
$warningtext[] = DI::l10n()->t( $warningtext[] = DI::l10n()->t(
'Friendica\'s system.basepath was updated from \'%s\' to \'%s\'. Please remove the system.basepath from your db to avoid differences.', 'Friendica\'s system.basepath was updated from \'%s\' to \'%s\'. Please remove the system.basepath from your db to avoid differences.',
$currBasepath, $currBasepath,
$confBasepath $confBasepath,
); );
} elseif (!is_dir($currBasepath)) { } elseif (!is_dir($currBasepath)) {
DI::logger()->alert('Friendica\'s system.basepath is wrong.', [ DI::logger()->alert('Friendica\'s system.basepath is wrong.', [
@ -142,7 +142,7 @@ class Summary extends BaseAdmin
$warningtext[] = DI::l10n()->t( $warningtext[] = DI::l10n()->t(
'Friendica\'s current system.basepath \'%s\' is wrong and the config file \'%s\' isn\'t used.', 'Friendica\'s current system.basepath \'%s\' is wrong and the config file \'%s\' isn\'t used.',
$currBasepath, $currBasepath,
$confBasepath $confBasepath,
); );
} else { } else {
DI::logger()->alert('Friendica\'s system.basepath is wrong.', [ DI::logger()->alert('Friendica\'s system.basepath is wrong.', [
@ -152,7 +152,7 @@ class Summary extends BaseAdmin
$warningtext[] = DI::l10n()->t( $warningtext[] = DI::l10n()->t(
'Friendica\'s current system.basepath \'%s\' is not equal to the config file \'%s\'. Please fix your configuration.', 'Friendica\'s current system.basepath \'%s\' is not equal to the config file \'%s\'. Please fix your configuration.',
$currBasepath, $currBasepath,
$confBasepath $confBasepath,
); );
} }
} }
@ -171,32 +171,56 @@ class Summary extends BaseAdmin
'php.ini' => php_ini_loaded_file(), 'php.ini' => php_ini_loaded_file(),
'upload_max_filesize' => ini_get('upload_max_filesize'), 'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'), 'post_max_size' => ini_get('post_max_size'),
'memory_limit' => ini_get('memory_limit') 'memory_limit' => ini_get('memory_limit'),
], ],
'mysql' => [ 'mysql' => [
'max_allowed_packet' => DBA::getVariable('max_allowed_packet'), 'max_allowed_packet' => DBA::getVariable('max_allowed_packet'),
] ],
]; ];
$addons = [];
$addonHelper = DI::addonHelper();
foreach ($addonHelper->getEnabledAddons() as $addonId) {
try {
$addonInfo = $addonHelper->getAddonInfo($addonId);
} catch (InvalidAddonException $th) {
$this->logger->error('Invalid addon found: ' . $addonId, ['exception' => $th]);
continue;
}
$info = [
'name' => $addonInfo->getName(),
'description' => $addonInfo->getDescription(),
'version' => $addonInfo->getVersion(),
];
$addons[] = [
$addonId,
$info,
];
}
$t = Renderer::getMarkupTemplate('admin/summary.tpl'); $t = Renderer::getMarkupTemplate('admin/summary.tpl');
return Renderer::replaceMacros($t, [ return Renderer::replaceMacros($t, [
'$title' => DI::l10n()->t('Administration'), '$title' => DI::l10n()->t('Administration'),
'$page' => DI::l10n()->t('Summary'), '$page' => DI::l10n()->t('Summary'),
'$queues' => $queues, '$queues' => $queues,
'$version_label' => DI::l10n()->t('Version'), '$version_label' => DI::l10n()->t('Version'),
'$platform' => App::PLATFORM, '$platform' => App::PLATFORM,
'$codename' => App::CODENAME, '$codename' => App::CODENAME,
'$build' => DI::config()->get('system', 'build'), '$build' => DI::config()->get('system', 'build'),
'$addons' => [DI::l10n()->t('Active addons'), DI::addonHelper()->getEnabledAddons()], '$addons' => [DI::l10n()->t('Active addons'), $addons],
'$serversettings' => $server_settings, '$serversettings' => $server_settings,
'$warningtext' => $warningtext, '$warningtext' => $warningtext,
'$link_enable_addons' => DI::l10n()->t('Enable new addons'),
]); ]);
} }
private static function checkSelfHostMeta() private static function checkSelfNodeinfo()
{ {
// Fetch the host-meta to check if this really is a vital server // Fetch the webfinger to check if this really is a vital server
return DI::httpClient()->get(DI::baseUrl() . Probe::HOST_META, HttpClientAccept::XRD_XML)->isSuccess(); return DI::httpClient()->get(DI::baseUrl() . '/.well-known/nodeinfo', HttpClientAccept::JSON)->isSuccess();
} }
} }

View file

@ -40,12 +40,12 @@ class ApiResponse extends Response
public function __construct(L10n $l10n, Arguments $args, LoggerInterface $logger, BaseURL $baseUrl, TwitterUser $twitterUser, array $server = [], string $jsonpCallback = '') public function __construct(L10n $l10n, Arguments $args, LoggerInterface $logger, BaseURL $baseUrl, TwitterUser $twitterUser, array $server = [], string $jsonpCallback = '')
{ {
$this->l10n = $l10n; $this->l10n = $l10n;
$this->args = $args; $this->args = $args;
$this->logger = $logger; $this->logger = $logger;
$this->baseUrl = $baseUrl; $this->baseUrl = $baseUrl;
$this->twitterUser = $twitterUser; $this->twitterUser = $twitterUser;
$this->server = $server; $this->server = $server;
$this->jsonpCallback = $jsonpCallback; $this->jsonpCallback = $jsonpCallback;
} }
@ -67,7 +67,7 @@ class ApiResponse extends Response
'' => 'http://api.twitter.com', '' => 'http://api.twitter.com',
'statusnet' => 'http://status.net/schema/api/1/', 'statusnet' => 'http://status.net/schema/api/1/',
'friendica' => 'http://friendi.ca/schema/api/1/', 'friendica' => 'http://friendi.ca/schema/api/1/',
'georss' => 'http://www.georss.org/georss' 'georss' => 'http://www.georss.org/georss',
]; ];
/// @todo Auto detection of needed namespaces /// @todo Auto detection of needed namespaces
@ -77,7 +77,7 @@ class ApiResponse extends Response
if (is_array($data2)) { if (is_array($data2)) {
$key = key($data2); $key = key($data2);
Arrays::walkRecursive($data2, ['Friendica\Module\Api\ApiResponse', 'reformatXML']); Arrays::walkRecursive($data2, [\Friendica\Module\Api\ApiResponse::class, 'reformatXML']);
if ($key == '0') { if ($key == '0') {
$data4 = []; $data4 = [];
@ -114,7 +114,7 @@ class ApiResponse extends Response
$user_info = $this->twitterUser->createFromContactId($cid)->toArray(); $user_info = $this->twitterUser->createFromContactId($cid)->toArray();
$arr['$user'] = $user_info; $arr['$user'] = $user_info;
$arr['$rss'] = [ $arr['$rss'] = [
'alternate' => $user_info['url'], 'alternate' => $user_info['url'],
'self' => $this->baseUrl . '/' . $this->args->getQueryString(), 'self' => $this->baseUrl . '/' . $this->args->getQueryString(),
'base' => $this->baseUrl, 'base' => $this->baseUrl,
@ -143,6 +143,7 @@ class ApiResponse extends Response
switch ($type) { switch ($type) {
case 'rss': case 'rss':
$data = $this->addRSSValues($data, $cid); $data = $this->addRSSValues($data, $cid);
// no break
case 'atom': case 'atom':
case 'xml': case 'xml':
return $this->createXML($data, $root_element); return $this->createXML($data, $root_element);
@ -191,7 +192,7 @@ class ApiResponse extends Response
$error = [ $error = [
'error' => $message ?: $description, 'error' => $message ?: $description,
'code' => $code . ' ' . $description, 'code' => $code . ' ' . $description,
'request' => $this->args->getQueryString() 'request' => $this->args->getQueryString(),
]; ];
$this->setHeader(($this->server['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . ' ' . $code . ' ' . $description); $this->setHeader(($this->server['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . ' ' . $code . ' ' . $description);
@ -212,7 +213,7 @@ class ApiResponse extends Response
*/ */
public function addFormattedContent(string $root_element, array $data, string $format = null, int $cid = 0) public function addFormattedContent(string $root_element, array $data, string $format = null, int $cid = 0)
{ {
$format = $format ?? 'json'; $format ??= 'json';
$return = $this->formatData($root_element, $format, $data, $cid); $return = $this->formatData($root_element, $format, $data, $cid);
@ -269,13 +270,15 @@ class ApiResponse extends Response
public function unsupported(string $method = 'all', array $request = []) public function unsupported(string $method = 'all', array $request = [])
{ {
$path = $this->args->getQueryString(); $path = $this->args->getQueryString();
$this->logger->info('Unimplemented API call', $this->logger->info(
'Unimplemented API call',
[ [
'method' => $method, 'method' => $method,
'path' => $path, 'path' => $path,
'agent' => $this->server['HTTP_USER_AGENT'] ?? '', 'agent' => $this->server['HTTP_USER_AGENT'] ?? '',
'request' => $request, 'request' => $request,
]); ],
);
$error = $this->l10n->t('API endpoint %s %s is not implemented but might be in the future.', strtoupper($method), $path); $error = $this->l10n->t('API endpoint %s %s is not implemented but might be in the future.', strtoupper($method), $path);
$this->error(501, 'Not Implemented', $error); $this->error(501, 'Not Implemented', $error);

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Friendica\Statuses; namespace Friendica\Module\Api\Friendica\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -35,6 +34,6 @@ class Dislike extends BaseApi
Item::performActivity($item['id'], 'dislike', $uid); Item::performActivity($item['id'], 'dislike', $uid);
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes())->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Friendica\Statuses; namespace Friendica\Module\Api\Friendica\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -35,6 +34,6 @@ class Undislike extends BaseApi
Item::performActivity($item['id'], 'undislike', $uid); Item::performActivity($item['id'], 'undislike', $uid);
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes())->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid)->toArray());
} }
} }

View file

@ -93,12 +93,10 @@ class Statuses extends BaseApi
$items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params); $items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params);
} }
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
try { try {
$status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
$this->updateBoundaries($status, $item, $request['friendica_order']); $this->updateBoundaries($status, $item, $request['friendica_order']);
$statuses[] = $status; $statuses[] = $status;
} catch (\Throwable $th) { } catch (\Throwable $th) {

View file

@ -11,13 +11,13 @@ use Friendica\DI;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
/** /**
* @see https://docs.joinmastodon.org/methods/apps/ * @see https://docs.joinmastodon.org/methods/apps/#verify_credentials
*/ */
class VerifyCredentials extends BaseApi class VerifyCredentials extends BaseApi
{ {
protected function rawContent(array $request = []) protected function rawContent(array $request = [])
{ {
$this->checkAllowedScope(self::SCOPE_READ); $this->checkAllowedScope(self::SCOPE_ANY);
$application = self::getCurrentApplication(); $application = self::getCurrentApplication();
if (empty($application['id'])) { if (empty($application['id'])) {

View file

@ -54,13 +54,11 @@ class Bookmarks extends BaseApi
$items = Post::selectThreadForUser($uid, ['uri-id'], $condition, $params); $items = Post::selectThreadForUser($uid, ['uri-id'], $condition, $params);
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
self::setBoundaries($item['uri-id']); self::setBoundaries($item['uri-id']);
try { try {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]); $this->logger->info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]);
} }

View file

@ -42,8 +42,10 @@ class Directory extends BaseApi
$condition = ['uid' => 0, 'hidden' => false, 'network' => Protocol::FEDERATED]; $condition = ['uid' => 0, 'hidden' => false, 'network' => Protocol::FEDERATED];
} }
$params = ['limit' => [$request['offset'], $request['limit']], $params = [
'order' => [($request['order'] == 'active') ? 'last-item' : 'created' => true]]; 'limit' => [$request['offset'], $request['limit']],
'order' => [($request['order'] == 'active') ? 'last-item' : 'created' => true]
];
$accounts = []; $accounts = [];
$contacts = DBA::select($table, ['id', 'uid'], $condition, $params); $contacts = DBA::select($table, ['id', 'uid'], $condition, $params);

View file

@ -56,13 +56,11 @@ class Favourited extends BaseApi
$items = Post::selectForUser($uid, ['thr-parent-id'], $condition, $params); $items = Post::selectForUser($uid, ['thr-parent-id'], $condition, $params);
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
self::setBoundaries($item['thr-parent-id']); self::setBoundaries($item['thr-parent-id']);
try { try {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['thr-parent-id'], $uid, $display_quotes); $statuses[] = DI::mstdnStatus()->createFromUriId($item['thr-parent-id'], $uid);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info('Post not fetchable', ['uri-id' => $item['thr-parent-id'], 'uid' => $uid, 'exception' => $exception]); $this->logger->info('Post not fetchable', ['uri-id' => $item['thr-parent-id'], 'uid' => $uid, 'exception' => $exception]);
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon; namespace Friendica\Module\Api\Mastodon;
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;
@ -34,7 +33,7 @@ class Notifications extends BaseApi
$id = $this->parameters['id']; $id = $this->parameters['id'];
try { try {
$notification = DI::notification()->selectOneForUser($uid, ['id' => $id]); $notification = DI::notification()->selectOneForUser($uid, ['id' => $id]);
$this->jsonExit(DI::mstdnNotification()->createFromNotification($notification, self::appSupportsQuotes())); $this->jsonExit(DI::mstdnNotification()->createFromNotification($notification));
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logAndJsonError(404, $this->errorFactory->RecordNotFound()); $this->logAndJsonError(404, $this->errorFactory->RecordNotFound());
} }
@ -132,7 +131,7 @@ class Notifications extends BaseApi
foreach ($Notifications as $Notification) { foreach ($Notifications as $Notification) {
try { try {
$mstdnNotifications[] = DI::mstdnNotification()->createFromNotification($Notification, self::appSupportsQuotes()); $mstdnNotifications[] = DI::mstdnNotification()->createFromNotification($Notification);
self::setBoundaries($Notification->id); self::setBoundaries($Notification->id);
} catch (\Exception $e) { } catch (\Exception $e) {
// Skip this notification // Skip this notification

View file

@ -102,7 +102,7 @@ class Search extends BaseApi
// If the user-specific search failed, we search and probe a public post // If the user-specific search failed, we search and probe a public post
$item_id = Item::fetchByLink($q, $uid) ?: Item::fetchByLink($q); $item_id = Item::fetchByLink($q, $uid) ?: Item::fetchByLink($q);
if ($item_id && $item = Post::selectFirst(['uri-id'], ['id' => $item_id])) { if ($item_id && $item = Post::selectFirst(['uri-id'], ['id' => $item_id])) {
$result['statuses'] = [DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, self::appSupportsQuotes())]; $result['statuses'] = [DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid)];
$this->jsonExit($result); $this->jsonExit($result);
} }
} }
@ -190,13 +190,11 @@ class Search extends BaseApi
$items = DBA::select($table, ['uri-id'], $condition, $params); $items = DBA::select($table, ['uri-id'], $condition, $params);
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
self::setBoundaries($item['uri-id']); self::setBoundaries($item['uri-id']);
try { try {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]); $this->logger->info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]);
} }

View file

@ -183,7 +183,7 @@ class Statuses extends BaseApi
Item::updateDisplayCache($post['uri-id']); Item::updateDisplayCache($post['uri-id']);
$this->jsonExit(DI::mstdnStatus()->createFromUriId($post['uri-id'], $uid, self::appSupportsQuotes())); $this->jsonExit(DI::mstdnStatus()->createFromUriId($post['uri-id'], $uid));
} }
protected function post(array $request = []) protected function post(array $request = [])
@ -217,7 +217,6 @@ class Statuses extends BaseApi
$item['body'] = $this->formatStatus($request['status'], $uid); $item['body'] = $this->formatStatus($request['status'], $uid);
$item['app'] = $this->getApp(); $item['app'] = $this->getApp();
$item['sensitive'] = $request['sensitive']; $item['sensitive'] = $request['sensitive'];
$item['visibility'] = $request['visibility'];
switch ($request['visibility']) { switch ($request['visibility']) {
case 'public': case 'public':
@ -238,11 +237,15 @@ class Statuses extends BaseApi
if ($request['in_reply_to_id']) { if ($request['in_reply_to_id']) {
$parent_item = Post::selectFirst(Item::ITEM_FIELDLIST, ['uri-id' => $request['in_reply_to_id'], 'uid' => $uid, 'private' => Item::PRIVATE]); $parent_item = Post::selectFirst(Item::ITEM_FIELDLIST, ['uri-id' => $request['in_reply_to_id'], 'uid' => $uid, 'private' => Item::PRIVATE]);
if (!empty($parent_item)) { if (!empty($parent_item)) {
$item['allow_cid'] = $parent_item['allow_cid']; // When 'visibility' is set to 'private' we want the permissions be copied from the parent post in the "insert" function.
$item['allow_gid'] = $parent_item['allow_gid']; // But this must only happen when the parent post was private as well.
$item['deny_cid'] = $parent_item['deny_cid']; // In all other cases we want to keep the permissions we defined here. The copying is controlled via the 'visibility' field.
$item['deny_gid'] = $parent_item['deny_gid']; $item['visibility'] = $request['visibility'];
$item['private'] = $parent_item['private']; $item['allow_cid'] = $parent_item['allow_cid'];
$item['allow_gid'] = $parent_item['allow_gid'];
$item['deny_cid'] = $parent_item['deny_cid'];
$item['deny_gid'] = $parent_item['deny_gid'];
$item['private'] = $parent_item['private'];
break; break;
} }
} }
@ -345,7 +348,7 @@ class Statuses extends BaseApi
if (!empty($id)) { if (!empty($id)) {
$item = Post::selectFirst(['uri-id'], ['id' => $id]); $item = Post::selectFirst(['uri-id'], ['id' => $id]);
if (!empty($item['uri-id'])) { if (!empty($item['uri-id'])) {
$this->jsonExit(DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, self::appSupportsQuotes())); $this->jsonExit(DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid));
} }
} }
@ -394,9 +397,10 @@ class Statuses extends BaseApi
if (Post::exists(['uri-id' => $this->parameters['id'], 'uid' => $uid, 'unseen' => true])) { if (Post::exists(['uri-id' => $this->parameters['id'], 'uid' => $uid, 'unseen' => true])) {
Post::update(['unseen' => false], ['uri-id' => $this->parameters['id'], 'uid' => $uid, 'unseen' => true]); Post::update(['unseen' => false], ['uri-id' => $this->parameters['id'], 'uid' => $uid, 'unseen' => true]);
} }
$this->item->setViewed($this->parameters['id'], $uid);
} }
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), false)); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, false));
} }
private function getApp(): string private function getApp(): string

View file

@ -55,6 +55,6 @@ class Bookmark extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -46,7 +45,7 @@ class Context extends BaseApi
$parent = Post::selectOriginal(['uri-id', 'parent-uri-id'], ['uri-id' => $id]); $parent = Post::selectOriginal(['uri-id', 'parent-uri-id'], ['uri-id' => $id]);
if (DBA::isResult($parent)) { if (DBA::isResult($parent)) {
$id = $parent['uri-id']; $id = $parent['uri-id'];
$params = ['order' => ['uri-id' => true]]; $params = ['order' => ['uri-id' => true]];
$condition = ['parent-uri-id' => $parent['parent-uri-id'], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]; $condition = ['parent-uri-id' => $parent['parent-uri-id'], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]];
@ -59,7 +58,7 @@ class Context extends BaseApi
} }
if (!empty($request['min_id'])) { if (!empty($request['min_id'])) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $request['min_id']]); $condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $request['min_id']]);
$params['order'] = ['uri-id']; $params['order'] = ['uri-id'];
} }
@ -112,11 +111,9 @@ class Context extends BaseApi
asort($ancestors); asort($ancestors);
$display_quotes = self::appSupportsQuotes();
foreach (array_slice($ancestors, 0, $request['limit']) as $ancestor) { foreach (array_slice($ancestors, 0, $request['limit']) as $ancestor) {
try { try {
$statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid, $display_quotes); $statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
} catch (\Throwable $th) { } catch (\Throwable $th) {
$this->logger->info('Post not fetchable', ['uri-id' => $ancestor, 'uid' => $uid, 'error' => $th]); $this->logger->info('Post not fetchable', ['uri-id' => $ancestor, 'uid' => $uid, 'error' => $th]);
} }
@ -128,7 +125,7 @@ class Context extends BaseApi
foreach (array_slice($descendants, 0, $request['limit']) as $descendant) { foreach (array_slice($descendants, 0, $request['limit']) as $descendant) {
try { try {
$statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid, $display_quotes); $statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
} catch (\Throwable $th) { } catch (\Throwable $th) {
$this->logger->info('Post not fetchable', ['uri-id' => $descendant, 'uid' => $uid, 'error' => $th]); $this->logger->info('Post not fetchable', ['uri-id' => $descendant, 'uid' => $uid, 'error' => $th]);
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -40,6 +39,6 @@ class Favourite extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -44,6 +43,6 @@ class Mute extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Post; use Friendica\Model\Post;
@ -39,6 +38,6 @@ class Pin extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(),$isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -9,7 +9,6 @@ namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Content\ContactSelector; use Friendica\Content\ContactSelector;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -52,6 +51,6 @@ class Reblog extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -56,6 +55,6 @@ class Unbookmark extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -40,6 +39,6 @@ class Unfavourite extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -44,6 +43,6 @@ class Unmute extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -7,7 +7,6 @@
namespace Friendica\Module\Api\Mastodon\Statuses; namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Post; use Friendica\Model\Post;
@ -39,6 +38,6 @@ class Unpin extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -9,7 +9,6 @@ namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Content\ContactSelector; use Friendica\Content\ContactSelector;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item; use Friendica\Model\Item;
@ -58,6 +57,6 @@ class Unreblog extends BaseApi
// Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350 // Issue tracking the behavior of createFromUriId: https://github.com/friendica/friendica/issues/13350
$isReblog = $item['uri-id'] != $this->parameters['id']; $isReblog = $item['uri-id'] != $this->parameters['id'];
$this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, self::appSupportsQuotes(), $isReblog)->toArray()); $this->jsonExit(DI::mstdnStatus()->createFromUriId($this->parameters['id'], $uid, $isReblog)->toArray());
} }
} }

View file

@ -69,12 +69,10 @@ class Home extends BaseApi
$items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params); $items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params);
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
try { try {
$status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
$this->updateBoundaries($status, $item, $request['friendica_order']); $this->updateBoundaries($status, $item, $request['friendica_order']);
$statuses[] = $status; $statuses[] = $status;
} catch (\Throwable $th) { } catch (\Throwable $th) {

View file

@ -66,8 +66,6 @@ class ListTimeline extends BaseApi
'friendica_order' => TimelineOrderByTypes::ID, // Sort order options (defaults to ID) 'friendica_order' => TimelineOrderByTypes::ID, // Sort order options (defaults to ID)
], $request); ], $request);
$display_quotes = self::appSupportsQuotes();
if (substr($this->parameters['id'], 0, 6) == 'group:') { if (substr($this->parameters['id'], 0, 6) == 'group:') {
$items = $this->getStatusesForGroup($uid, $request); $items = $this->getStatusesForGroup($uid, $request);
} elseif (substr($this->parameters['id'], 0, 8) == 'channel:') { } elseif (substr($this->parameters['id'], 0, 8) == 'channel:') {
@ -79,7 +77,7 @@ class ListTimeline extends BaseApi
$statuses = []; $statuses = [];
foreach ($items as $item) { foreach ($items as $item) {
try { try {
$status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
$this->updateBoundaries($status, $item, $request['friendica_order']); $this->updateBoundaries($status, $item, $request['friendica_order']);
$statuses[] = $status; $statuses[] = $status;
} catch (\Throwable $th) { } catch (\Throwable $th) {

View file

@ -103,12 +103,10 @@ class PublicTimeline extends BaseApi
$items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params); $items = Post::selectTimelineForUser($uid, ['uri-id'], $condition, $params);
} }
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
try { try {
$status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $status = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
$this->updateBoundaries($status, $item, $request['friendica_order']); $this->updateBoundaries($status, $item, $request['friendica_order']);
$statuses[] = $status; $statuses[] = $status;
} catch (\Throwable $th) { } catch (\Throwable $th) {

View file

@ -97,13 +97,11 @@ class Tag extends BaseApi
$items = DBA::select('tag-search-view', ['uri-id'], $condition, $params); $items = DBA::select('tag-search-view', ['uri-id'], $condition, $params);
$display_quotes = self::appSupportsQuotes();
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
self::setBoundaries($item['uri-id']); self::setBoundaries($item['uri-id']);
try { try {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid, $display_quotes); $statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]); $this->logger->info('Post not fetchable', ['uri-id' => $item['uri-id'], 'uid' => $uid, 'exception' => $exception]);
} }

View file

@ -8,7 +8,6 @@
namespace Friendica\Module\Api\Mastodon\Trends; namespace Friendica\Module\Api\Mastodon\Trends;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Post; use Friendica\Model\Post;
@ -26,7 +25,7 @@ class Links extends BaseApi
protected function rawContent(array $request = []) protected function rawContent(array $request = [])
{ {
$request = $this->getRequest([ $request = $this->getRequest([
'limit' => 10, // Maximum number of results to return. Defaults to 10. 'limit' => 10, // Maximum number of results to return. Defaults to 10.
'offset' => 0, // Offset in set, Defaults to 0. 'offset' => 0, // Offset in set, Defaults to 0.
], $request); ], $request);
@ -37,8 +36,11 @@ class Links extends BaseApi
$trending = []; $trending = [];
$statuses = Post::selectPostThread(['uri-id', 'total-comments', 'total-actors'], $condition, ['limit' => [$request['offset'], $request['limit']], 'offset' => $request['offset'], 'order' => ['total-actors' => true]]); $statuses = Post::selectPostThread(['uri-id', 'total-comments', 'total-actors'], $condition, ['limit' => [$request['offset'], $request['limit']], 'offset' => $request['offset'], 'order' => ['total-actors' => true]]);
while ($status = Post::fetch($statuses)) { while ($status = Post::fetch($statuses)) {
$history = [['day' => (string)time(), 'uses' => (string)$status['total-comments'], 'accounts' => (string)$status['total-actors']]]; $history = [['day' => (string)time(), 'uses' => (string)$status['total-comments'], 'accounts' => (string)$status['total-actors']]];
$trending[] = DI::mstdnCard()->createFromUriId($status['uri-id'], $history)->toArray(); $link = DI::mstdnCard()->createFromUriId($status['uri-id'], $history)->toArray();
if ($link) {
$trending[] = $link;
}
} }
DBA::close($statuses); DBA::close($statuses);

View file

@ -58,13 +58,11 @@ class Statuses extends BaseApi
$condition = ["NOT `private` AND `commented` > ? AND `created` > ?", DateTimeFormat::utc('now -1 day'), DateTimeFormat::utc('now -1 week')]; $condition = ["NOT `private` AND `commented` > ? AND `created` > ?", DateTimeFormat::utc('now -1 day'), DateTimeFormat::utc('now -1 week')];
$condition = DBA::mergeConditions($condition, ['network' => Protocol::FEDERATED]); $condition = DBA::mergeConditions($condition, ['network' => Protocol::FEDERATED]);
$display_quotes = self::appSupportsQuotes();
$trending = []; $trending = [];
$statuses = Post::selectPostThread(['uri-id'], $condition, ['limit' => [$request['offset'], $request['limit']], 'order' => ['total-actors' => true]]); $statuses = Post::selectPostThread(['uri-id'], $condition, ['limit' => [$request['offset'], $request['limit']], 'order' => ['total-actors' => true]]);
while ($status = Post::fetch($statuses)) { while ($status = Post::fetch($statuses)) {
try { try {
$trending[] = DI::mstdnStatus()->createFromUriId($status['uri-id'], $uid, $display_quotes); $trending[] = DI::mstdnStatus()->createFromUriId($status['uri-id'], $uid);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->logger->info('Post not fetchable', ['uri-id' => $status['uri-id'], 'uid' => $uid, 'exception' => $exception]); $this->logger->info('Post not fetchable', ['uri-id' => $status['uri-id'], 'uid' => $uid, 'exception' => $exception]);
} }

View file

@ -85,8 +85,8 @@ abstract class BaseAdmin extends BaseModule
'workerqueue' => ['admin/queue' , DI::l10n()->t('Inspect worker Queue') , 'workerqueue'], 'workerqueue' => ['admin/queue' , DI::l10n()->t('Inspect worker Queue') , 'workerqueue'],
]], ]],
'logs' => [DI::l10n()->t('Logs'), [ 'logs' => [DI::l10n()->t('Logs'), [
'logsconfig' => ['admin/logs/', DI::l10n()->t('Logs') , 'logs'], 'logsconfig' => ['admin/logs/', DI::l10n()->t('Settings') , 'logs'],
'logsview' => ['admin/logs/view' , DI::l10n()->t('View Logs') , 'viewlogs'], 'logsview' => ['admin/logs/view' , DI::l10n()->t('View') , 'viewlogs'],
]], ]],
'diagnostics' => [DI::l10n()->t('Diagnostics'), [ 'diagnostics' => [DI::l10n()->t('Diagnostics'), [
'phpinfo' => ['admin/phpinfo?t=' . self::getFormSecurityToken('phpinfo'), DI::l10n()->t('PHP Info') , 'phpinfo'], 'phpinfo' => ['admin/phpinfo?t=' . self::getFormSecurityToken('phpinfo'), DI::l10n()->t('PHP Info') , 'phpinfo'],
@ -112,7 +112,7 @@ abstract class BaseAdmin extends BaseModule
'$admin' => ['addons_admin' => $addons_admin], '$admin' => ['addons_admin' => $addons_admin],
'$subpages' => $aside_sub, '$subpages' => $aside_sub,
'$admtxt' => DI::l10n()->t('Admin'), '$admtxt' => DI::l10n()->t('Admin'),
'$plugadmtxt' => DI::l10n()->t('Addon Features'), '$plugadmtxt' => DI::l10n()->t('Addon settings'),
'$h_pending' => DI::l10n()->t('User registrations waiting for confirmation'), '$h_pending' => DI::l10n()->t('User registrations waiting for confirmation'),
'$admurl' => 'admin/' '$admurl' => 'admin/'
]); ]);

View file

@ -35,12 +35,13 @@ use Psr\Log\LoggerInterface;
class BaseApi extends BaseModule class BaseApi extends BaseModule
{ {
const LOG_PREFIX = 'API {action} - '; public const LOG_PREFIX = 'API {action} - ';
const SCOPE_READ = 'read'; public const SCOPE_READ = 'read';
const SCOPE_WRITE = 'write'; public const SCOPE_WRITE = 'write';
const SCOPE_FOLLOW = 'follow'; public const SCOPE_FOLLOW = 'follow';
const SCOPE_PUSH = 'push'; public const SCOPE_PUSH = 'push';
public const SCOPE_ANY = 'any';
/** /**
* @var array * @var array
@ -356,17 +357,6 @@ class BaseApi extends BaseModule
} }
} }
/**
* Check if the app is known to support quoted posts
*
* @return bool
*/
public static function appSupportsQuotes(): bool
{
// @todo Clean up the whole functionality since it isn't of any use anymore.
return true;
}
/** /**
* Get current application token * Get current application token
* *
@ -396,14 +386,14 @@ class BaseApi extends BaseModule
$uid = BasicAuth::getCurrentUserID(false); $uid = BasicAuth::getCurrentUserID(false);
} }
return (int)$uid; return (int) $uid;
} }
/** /**
* Check if the provided scope does exist. * Check if the provided scope does exist.
* halts execution on missing scope or when not logged in. * halts execution on missing scope or when not logged in.
* *
* @param string $scope the requested scope (read, write, follow, push) * @param string $scope the requested scope (read, write, follow, push, any)
*/ */
public function checkAllowedScope(string $scope) public function checkAllowedScope(string $scope)
{ {
@ -414,6 +404,10 @@ class BaseApi extends BaseModule
$this->logAndJsonError(403, $this->errorFactory->Forbidden()); $this->logAndJsonError(403, $this->errorFactory->Forbidden());
} }
if ($scope === self::SCOPE_ANY) {
return;
}
if (!isset($token[$scope])) { if (!isset($token[$scope])) {
$this->logger->warning('The requested scope does not exist', ['scope' => $scope, 'application' => $token]); $this->logger->warning('The requested scope does not exist', ['scope' => $scope, 'application' => $token]);
$this->logAndJsonError(403, $this->errorFactory->Forbidden()); $this->logAndJsonError(403, $this->errorFactory->Forbidden());

View file

@ -155,7 +155,7 @@ abstract class BaseNotifications extends BaseModule
foreach (self::URL_TYPES as $type => $url) { foreach (self::URL_TYPES as $type => $url) {
$tabs[] = [ $tabs[] = [
'label' => $this->t(self::PRINT_TYPES[$type]), 'label' => $this->t(self::PRINT_TYPES[$type]),
'url' => 'notifications/' . $url, 'url' => 'notifications/' . $url . (($url == "personal") ? "?show=all" : ""),
'sel' => (($selected == $url) ? 'active' : ''), 'sel' => (($selected == $url) ? 'active' : ''),
'id' => $type . '-tab', 'id' => $type . '-tab',
'accesskey' => self::ACCESS_KEYS[$type], 'accesskey' => self::ACCESS_KEYS[$type],

View file

@ -76,7 +76,7 @@ class BaseProfile extends BaseModule
]; ];
} else { } else {
$owner = User::getByNickname($nickname, ['uid']); $owner = User::getByNickname($nickname, ['uid']);
if(DI::userSession()->isAuthenticated() || $owner && Feature::isEnabled($owner['uid'], Feature::PUBLIC_CALENDAR)) { if (DI::userSession()->isAuthenticated() || $owner && Feature::isEnabled($owner['uid'], Feature::PUBLIC_CALENDAR)) {
$tabs[] = [ $tabs[] = [
'label' => DI::l10n()->t('Calendar'), 'label' => DI::l10n()->t('Calendar'),
'url' => DI::baseUrl() . '/calendar/show/' . $nickname, 'url' => DI::baseUrl() . '/calendar/show/' . $nickname,
@ -90,7 +90,7 @@ class BaseProfile extends BaseModule
if ($is_owner) { if ($is_owner) {
$tabs[] = [ $tabs[] = [
'label' => DI::l10n()->t('Personal Notes'), 'label' => DI::l10n()->t('Personal notes'),
'url' => DI::baseUrl() . '/notes', 'url' => DI::baseUrl() . '/notes',
'sel' => $current == 'notes' ? 'active' : '', 'sel' => $current == 'notes' ? 'active' : '',
'title' => DI::l10n()->t('Only You Can See This'), 'title' => DI::l10n()->t('Only You Can See This'),

View file

@ -108,8 +108,10 @@ class Form extends BaseModule
$share_disabled = ''; $share_disabled = '';
if (empty($orig_event)) { if (empty($orig_event)) {
$orig_event = User::getById($this->session->getLocalUserId(), $orig_event = User::getById(
['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']); $this->session->getLocalUserId(),
['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']
);
} elseif ($orig_event['allow_cid'] !== '<' . $this->session->getLocalUserId() . '>' } elseif ($orig_event['allow_cid'] !== '<' . $this->session->getLocalUserId() . '>'
|| $orig_event['allow_gid'] || $orig_event['allow_gid']
|| $orig_event['deny_cid'] || $orig_event['deny_cid']
@ -206,14 +208,14 @@ class Form extends BaseModule
true true
), ),
'$n_text' => $this->t('Finish date/time is not known or not relevant'), '$n_text' => $this->t('End date/time is unknown or irrelevant'),
'$n_checked' => $n_checked, '$n_checked' => $n_checked,
'$f_text' => $this->t('Event Finishes:'), '$f_text' => $this->t('Event ends:'),
'$f_dsel' => Temporal::getDateTimeField( '$f_dsel' => Temporal::getDateTimeField(
new \DateTime(), new \DateTime(),
\DateTime::createFromFormat('Y', intval($fyear) + 5), \DateTime::createFromFormat('Y', intval($fyear) + 5),
\DateTime::createFromFormat('Y-m-d H:i', "$fyear-$fmonth-$fday $fhour:$fminute"), \DateTime::createFromFormat('Y-m-d H:i', "$fyear-$fmonth-$fday $fhour:$fminute"),
$this->t('Event Finishes:'), $this->t('Event ends:'),
'finish_text', 'finish_text',
true, true,
true, true,
@ -230,7 +232,7 @@ class Form extends BaseModule
'$sh_text' => $this->t('Share this event'), '$sh_text' => $this->t('Share this event'),
'$share' => ['share', $this->t('Share this event'), $share_checked, '', $share_disabled], '$share' => ['share', $this->t('Share this event'), $share_checked, '', $share_disabled],
'$sh_checked' => $share_checked, '$sh_checked' => $share_checked,
'$nofinish' => ['nofinish', $this->t('Finish date/time is not known or not relevant'), $n_checked], '$nofinish' => ['nofinish', $this->t('End date/time is unknown or irrelevant'), $n_checked],
'$preview' => $this->t('Preview'), '$preview' => $this->t('Preview'),
'$acl' => $acl, '$acl' => $acl,
'$submit' => $this->t('Submit'), '$submit' => $this->t('Submit'),

View file

@ -90,11 +90,11 @@ class Show extends BaseModule
'$i18n' => $i18n, '$i18n' => $i18n,
]); ]);
Nav::setSelected($is_owner ? 'home' : 'calendar');
if ($is_owner) { if ($is_owner) {
// Removing the vCard added by Profile::load for owners // Removing the vCard added by Profile::load for owners
$this->page['aside'] = ''; $this->page['aside'] = '';
// Only highlight the calendar link if you're on your own calendar
Nav::setSelected('calendar');
} }
$this->page['aside'] .= Widget\CalendarExport::getHTML($owner['uid']); $this->page['aside'] .= Widget\CalendarExport::getHTML($owner['uid']);
@ -110,7 +110,7 @@ class Show extends BaseModule
$tpl = Renderer::getMarkupTemplate("calendar/calendar.tpl"); $tpl = Renderer::getMarkupTemplate("calendar/calendar.tpl");
$o = Renderer::replaceMacros($tpl, [ $o = Renderer::replaceMacros($tpl, [
'$tabs' => $tabs, '$tabs' => $tabs,
'$title' => $this->t('Events'), '$title' => $this->t('Calendar'),
'$view' => $this->t('View'), '$view' => $this->t('View'),
'$new_event' => ['calendar/event/new', $this->t('New Event'), '', ''], '$new_event' => ['calendar/event/new', $this->t('New Event'), '', ''],

View file

@ -33,7 +33,7 @@ class Circle extends BaseModule
BaseModule::checkFormSecurityTokenRedirectOnError('/circle/new', 'circle_edit'); BaseModule::checkFormSecurityTokenRedirectOnError('/circle/new', 'circle_edit');
$name = trim($request['circle_name']); $name = trim($request['circle_name']);
$r = Model\Circle::create(DI::userSession()->getLocalUserId(), $name); $r = Model\Circle::create(DI::userSession()->getLocalUserId(), $name);
if ($r) { if ($r) {
$r = Model\Circle::getIdByName(DI::userSession()->getLocalUserId(), $name); $r = Model\Circle::getIdByName(DI::userSession()->getLocalUserId(), $name);
if ($r) { if ($r) {
@ -73,7 +73,7 @@ class Circle extends BaseModule
$message = ''; $message = '';
if (isset($this->parameters['command'])) { if (isset($this->parameters['command'])) {
$circle_id = $this->parameters['circle']; $circle_id = $this->parameters['circle'];
$contact_id = $this->parameters['contact']; $contact_id = $this->parameters['contact'];
if (!Model\Circle::exists($circle_id, DI::userSession()->getLocalUserId())) { if (!Model\Circle::exists($circle_id, DI::userSession()->getLocalUserId())) {
@ -130,8 +130,8 @@ class Circle extends BaseModule
protected function content(array $request = []): string protected function content(array $request = []): string
{ {
DI::page()['title'] = DI::l10n()->t("Circles"); DI::page()['title'] = DI::l10n()->t("Circles");
$change = false; $change = false;
$relation = $request['rel'] ?? ''; $relation = $request['rel'] ?? '';
if (!DI::userSession()->getLocalUserId()) { if (!DI::userSession()->getLocalUserId()) {
throw new \Friendica\Network\HTTPException\ForbiddenException(); throw new \Friendica\Network\HTTPException\ForbiddenException();
@ -147,27 +147,25 @@ class Circle extends BaseModule
} }
// Switch to text mode interface if we have more than 'n' contacts or circle members // Switch to text mode interface if we have more than 'n' contacts or circle members
$switchtotext = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'circle_edit_image_limit') ?? $switchtotext = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'circle_edit_image_limit') ?? DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'groupedit_image_limit');
DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'groupedit_image_limit');
if (is_null($switchtotext)) { if (is_null($switchtotext)) {
$switchtotext = DI::config()->get('system', 'groupedit_image_limit') ?? $switchtotext = DI::config()->get('system', 'groupedit_image_limit') ?? DI::config()->get('system', 'circle_edit_image_limit');
DI::config()->get('system', 'circle_edit_image_limit');
} }
$tpl = Renderer::getMarkupTemplate('circle_edit.tpl'); $tpl = Renderer::getMarkupTemplate('circle_edit.tpl');
$context = [ $context = [
'$submit' => DI::l10n()->t('Save Circle'), '$submit' => DI::l10n()->t('Save Circle'),
'$submit_filter' => DI::l10n()->t('Filter'), '$submit_filter' => DI::l10n()->t('Filter'),
]; ];
// @TODO: Replace with parameter from router // @TODO: Replace with parameter from router
if ((DI::args()->getArgc() == 2) && (DI::args()->getArgv()[1] === 'new')) { if ((DI::args()->getArgc() == 2) && (DI::args()->getArgv()[1] === 'new')) {
return Renderer::replaceMacros($tpl, $context + [ return Renderer::replaceMacros($tpl, $context + [
'$title' => DI::l10n()->t('Create a circle of contacts/friends.'), '$title' => DI::l10n()->t('Create a circle of contacts/friends.'),
'$gname' => ['circle_name', DI::l10n()->t('Circle Name: '), '', ''], '$gname' => ['circle_name', DI::l10n()->t('Circle Name: '), '', ''],
'$gid' => 'new', '$gid' => 'new',
'$form_security_token' => BaseModule::getFormSecurityToken('circle_edit'), '$form_security_token' => BaseModule::getFormSecurityToken('circle_edit'),
]); ]);
} }
@ -179,17 +177,17 @@ class Circle extends BaseModule
// @TODO: Replace with parameter from router // @TODO: Replace with parameter from router
if ((DI::args()->getArgc() == 2) && (DI::args()->getArgv()[1] === 'none') || if ((DI::args()->getArgc() == 2) && (DI::args()->getArgv()[1] === 'none') ||
(DI::args()->getArgc() == 1) && (DI::args()->getArgv()[0] === 'nocircle')) { (DI::args()->getArgc() == 1) && (DI::args()->getArgv()[0] === 'nocircle')) {
$id = -1; $id = -1;
$nocircle = true; $nocircle = true;
$circle = [ $circle = [
'id' => $id, 'id' => $id,
'name' => DI::l10n()->t('Contacts not in any circle'), 'name' => DI::l10n()->t('Contacts not in any circle'),
]; ];
$context = $context + [ $context = $context + [
'$title' => $circle['name'], '$title' => $circle['name'],
'$gname' => ['circle_name', DI::l10n()->t('Circle Name: '), $circle['name'], ''], '$gname' => ['circle_name', DI::l10n()->t('Circle Name: '), $circle['name'], ''],
'$gid' => $id, '$gid' => $id,
'$editable' => 0, '$editable' => 0,
]; ];
} }
@ -212,6 +210,17 @@ class Circle extends BaseModule
DI::baseUrl()->redirect('circle'); DI::baseUrl()->redirect('circle');
} }
// @TODO: Replace with parameter from router
if ((DI::args()->getArgc() == 3) && (DI::args()->getArgv()[1] === 'markread')) {
BaseModule::checkFormSecurityTokenRedirectOnError('/circle', 'circle_markread', 't');
$circle_id = intval(DI::args()->getArgv()[2]);
if ($circle_id && Model\Circle::exists($circle_id, DI::userSession()->getLocalUserId())) {
DBA::update('post-user', ['unseen' => false], ["`uid` = ? AND `unseen` AND `contact-id` IN (SELECT `contact-id` FROM `group_member` WHERE `gid` = ?)", DI::userSession()->getLocalUserId(), $circle_id]);
}
DI::baseUrl()->redirect('circle/' . $circle_id);
}
// @TODO: Replace with parameter from router // @TODO: Replace with parameter from router
if ((DI::args()->getArgc() > 2) && intval(DI::args()->getArgv()[1]) && intval(DI::args()->getArgv()[2])) { if ((DI::args()->getArgc() > 2) && intval(DI::args()->getArgv()[1]) && intval(DI::args()->getArgv()[2])) {
BaseModule::checkFormSecurityTokenForbiddenOnError('circle_member_change', 't'); BaseModule::checkFormSecurityTokenForbiddenOnError('circle_member_change', 't');
@ -229,7 +238,7 @@ class Circle extends BaseModule
DI::baseUrl()->redirect('contact'); DI::baseUrl()->redirect('contact');
} }
$members = Model\Contact\Circle::getById($circle['id']); $members = Model\Contact\Circle::getById($circle['id']);
$preselected = []; $preselected = [];
if (count($members)) { if (count($members)) {
@ -245,7 +254,7 @@ class Circle extends BaseModule
Model\Circle::addMember($circle['id'], $change); Model\Circle::addMember($circle['id'], $change);
} }
$members = Model\Contact\Circle::getById($circle['id']); $members = Model\Contact\Circle::getById($circle['id']);
$preselected = []; $preselected = [];
if (count($members)) { if (count($members)) {
foreach ($members as $member) { foreach ($members as $member) {
@ -256,19 +265,21 @@ class Circle extends BaseModule
$drop_tpl = Renderer::getMarkupTemplate('circle_drop.tpl'); $drop_tpl = Renderer::getMarkupTemplate('circle_drop.tpl');
$drop_txt = Renderer::replaceMacros($drop_tpl, [ $drop_txt = Renderer::replaceMacros($drop_tpl, [
'$id' => $circle['id'], '$id' => $circle['id'],
'$delete' => DI::l10n()->t('Delete Circle'), '$delete' => DI::l10n()->t('Delete Circle'),
'$form_security_token' => BaseModule::getFormSecurityToken('circle_drop'), '$form_security_token' => BaseModule::getFormSecurityToken('circle_drop'),
]); ]);
$context = $context + [ $context = $context + [
'$title' => $circle['name'], '$title' => $circle['name'],
'$gname' => ['circle_name', DI::l10n()->t('Circle Name: '), $circle['name'], ''], '$gname' => ['circle_name', DI::l10n()->t('Circle Name: '), $circle['name'], ''],
'$gid' => $circle['id'], '$gid' => $circle['id'],
'$drop' => $drop_txt, '$drop' => $drop_txt,
'$form_security_token' => BaseModule::getFormSecurityToken('circle_edit'), '$form_security_token' => BaseModule::getFormSecurityToken('circle_edit'),
'$edit_name' => DI::l10n()->t('Edit Circle Name'), '$form_security_token_markread' => BaseModule::getFormSecurityToken('circle_markread'),
'$editable' => 1, '$edit_name' => DI::l10n()->t('Edit Circle Name'),
'$markread_label' => DI::l10n()->t('Mark all as read'),
'$editable' => 1,
]; ];
} }
@ -277,11 +288,11 @@ class Circle extends BaseModule
} }
$circle_editor = [ $circle_editor = [
'label_members' => DI::l10n()->t('Members'), 'label_members' => DI::l10n()->t('Members'),
'members' => [], 'members' => [],
'label_contacts' => DI::l10n()->t('All Contacts'), 'label_contacts' => DI::l10n()->t('All Contacts'),
'circle_is_empty' => DI::l10n()->t('Circle is empty'), 'circle_is_empty' => DI::l10n()->t('Circle is empty'),
'contacts' => [], 'contacts' => [],
]; ];
$sec_token = addslashes(BaseModule::getFormSecurityToken('circle_member_change')); $sec_token = addslashes(BaseModule::getFormSecurityToken('circle_member_change'));
@ -292,9 +303,9 @@ class Circle extends BaseModule
continue; continue;
} }
if ($member['url']) { if ($member['url']) {
$entry = Contact::getContactTemplateVars($member); $entry = Contact::getContactTemplateVars($member);
$entry['label'] = 'members'; $entry['label'] = 'members';
$entry['photo_menu'] = ''; $entry['photo_menu'] = '';
$entry['change_member'] = [ $entry['change_member'] = [
'title' => DI::l10n()->t('Remove contact from circle'), 'title' => DI::l10n()->t('Remove contact from circle'),
'gid' => $circle['id'], 'gid' => $circle['id'],
@ -312,13 +323,13 @@ class Circle extends BaseModule
$contacts = Model\Contact\Circle::listUncircled(DI::userSession()->getLocalUserId()); $contacts = Model\Contact\Circle::listUncircled(DI::userSession()->getLocalUserId());
} else { } else {
$networks = Widget::unavailableNetworks(); $networks = Widget::unavailableNetworks();
$query = "`uid` = ? AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `failed` $query = "`uid` = ? AND NOT `self` AND NOT `deleted` AND NOT `blocked` AND NOT `pending` AND NOT `failed`
AND `rel` IN (?, ?, ?) AND `rel` IN (?, ?, ?)
AND NOT `network` IN (" . substr(str_repeat('?, ', count($networks)), 0, -2) . ")"; AND NOT `network` IN (" . substr(str_repeat('?, ', count($networks)), 0, -2) . ")";
$condition = array_merge([$query], [DI::userSession()->getLocalUserId(), Model\Contact::FOLLOWER, Model\Contact::FRIEND, Model\Contact::SHARING], $networks); $condition = array_merge([$query], [DI::userSession()->getLocalUserId(), Model\Contact::FOLLOWER, Model\Contact::FRIEND, Model\Contact::SHARING], $networks);
$contacts_stmt = DBA::select('contact', [], $condition, ['order' => ['name']]); $contacts_stmt = DBA::select('contact', [], $condition, ['order' => ['name']]);
$contacts = DBA::toArray($contacts_stmt); $contacts = DBA::toArray($contacts_stmt);
$context['$desc'] = DI::l10n()->t('Click on a contact to add or remove.'); $context['$desc'] = DI::l10n()->t('Click on a contact to add or remove.');
} }
@ -329,10 +340,11 @@ class Circle extends BaseModule
continue; continue;
} }
if (!in_array($member['id'], $preselected)) { if (!in_array($member['id'], $preselected)) {
$entry = Contact::getContactTemplateVars($member); $entry = Contact::getContactTemplateVars($member);
$entry['label'] = 'contacts'; $entry['label'] = 'contacts';
if (!$nocircle) if (!$nocircle) {
$entry['photo_menu'] = []; $entry['photo_menu'] = [];
}
if (!$nocircle) { if (!$nocircle) {
$entry['change_member'] = [ $entry['change_member'] = [
@ -351,7 +363,7 @@ class Circle extends BaseModule
$context['$circle_editor'] = $circle_editor; $context['$circle_editor'] = $circle_editor;
// If there are to many contacts we could provide an alternative view mode // If there are to many contacts we could provide an alternative view mode
$total = count($circle_editor['members']) + count($circle_editor['contacts']); $total = count($circle_editor['members']) + count($circle_editor['contacts']);
$context['$shortmode'] = (($switchtotext && ($total > $switchtotext)) ? true : false); $context['$shortmode'] = (($switchtotext && ($total > $switchtotext)) ? true : false);
if ($change) { if ($change) {

View file

@ -204,7 +204,7 @@ class Contact extends BaseModule
DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, []); DI::page()['htmlhead'] .= Renderer::replaceMacros($tpl, []);
$o = ''; $o = '';
Nav::setSelected('contact'); Nav::setSelected('contacts');
$_SESSION['return_path'] = DI::args()->getQueryString(); $_SESSION['return_path'] = DI::args()->getQueryString();

View file

@ -88,19 +88,23 @@ class Conversations extends BaseModule
$this->baseUrl->redirect('profile/' . $contact['nick']); $this->baseUrl->redirect('profile/' . $contact['nick']);
} }
// Load necessary libraries for the status editor $raw = isset($request['mode']) && ($request['mode'] == 'raw');
$this->page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
$this->page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
$this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
$this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
$this->page['aside'] .= VCard::getHTML($contact, true); if (!$raw) {
// Load necessary libraries for the status editor
$this->page->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
$this->page->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
$this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
$this->page->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
Nav::setSelected('contact'); $this->page['aside'] .= VCard::getHTML($contact, true);
}
Nav::setSelected('contacts');
$output = ''; $output = '';
if (!$contact['ap-posting-restricted']) { if (!$contact['ap-posting-restricted'] && !$raw) {
$options = [ $options = [
'lockstate' => ACL::getLockstateForUserId($this->userSession->getLocalUserId()) ? 'lock' : 'unlock', 'lockstate' => ACL::getLockstateForUserId($this->userSession->getLocalUserId()) ? 'lock' : 'unlock',
'acl' => ACL::getFullSelectorHTML($this->page, $this->userSession->getLocalUserId(), true, []), 'acl' => ACL::getFullSelectorHTML($this->page, $this->userSession->getLocalUserId(), true, []),
@ -111,8 +115,10 @@ class Conversations extends BaseModule
} }
Contact::setPageTitle($contact); Contact::setPageTitle($contact);
$output .= Contact::getTabsHTML($contact, Contact::TAB_CONVERSATIONS); if (!$raw) {
$output .= ModelContact::getThreadsFromId($contact['id'], $this->userSession->getLocalUserId(), 0, 0, $request['last_created'] ?? ''); $output .= Contact::getTabsHTML($contact, Contact::TAB_CONVERSATIONS);
}
$output .= ModelContact::getThreadsFromId($contact['id'], $this->userSession->getLocalUserId(), 0, 0, $request);
return $output; return $output;
} }

View file

@ -175,11 +175,11 @@ class Follow extends BaseModule
$output .= Renderer::replaceMacros( $output .= Renderer::replaceMacros(
Renderer::getMarkupTemplate('section_title.tpl'), Renderer::getMarkupTemplate('section_title.tpl'),
['$title' => $this->t('Posts and Replies')] ['$title' => $this->t('Posts and Replies')],
); );
// Show last public posts // Show last public posts
$output .= Contact::getPostsFromUrl($contact['url'], $this->session->getLocalUserId()); $output .= Contact::getPostsFromUrl($contact['url'], $this->session->getLocalUserId(), false, $request);
} }
return $output; return $output;

View file

@ -53,7 +53,7 @@ class Media extends BaseModule
$o = Contact::getTabsHTML($contact, Contact::TAB_MEDIA); $o = Contact::getTabsHTML($contact, Contact::TAB_MEDIA);
$o .= ModelContact::getPostsFromUrl($contact['url'], $this->userSession->getLocalUserId(), true, $request['last_created'] ?? ''); $o .= ModelContact::getPostsFromUrl($contact['url'], $this->userSession->getLocalUserId(), true, $request);
return $o; return $o;
} }

View file

@ -81,13 +81,13 @@ class Posts extends BaseModule
$this->page['aside'] .= Widget\VCard::getHTML($contact); $this->page['aside'] .= Widget\VCard::getHTML($contact);
Nav::setSelected('contact'); Nav::setSelected('contacts');
Contact::setPageTitle($contact); Contact::setPageTitle($contact);
$o = Contact::getTabsHTML($contact, Contact::TAB_POSTS); $o = Contact::getTabsHTML($contact, Contact::TAB_POSTS);
$o .= Model\Contact::getPostsFromId($contact['id'], $this->userSession->getLocalUserId(), false, $request['last_created'] ?? ''); $o .= Model\Contact::getPostsFromId($contact['id'], $this->userSession->getLocalUserId(), false, $request);
return $o; return $o;
} }

View file

@ -187,7 +187,7 @@ class Profile extends BaseModule
} }
} }
if (empty($contact['network']) && ContactModel::isLocal($contact['url']) ) { if (empty($contact['network']) && ContactModel::isLocal($contact['url'])) {
$contact['network'] = Protocol::DFRN; $contact['network'] = Protocol::DFRN;
$contact['protocol'] = Protocol::ACTIVITYPUB; $contact['protocol'] = Protocol::ACTIVITYPUB;
} }
@ -280,7 +280,7 @@ class Profile extends BaseModule
$this->page['aside'] .= $vcard_widget . $circles_widget; $this->page['aside'] .= $vcard_widget . $circles_widget;
$o = ''; $o = '';
Nav::setSelected('contact'); Nav::setSelected('contacts');
$_SESSION['return_path'] = $this->args->getQueryString(); $_SESSION['return_path'] = $this->args->getQueryString();
@ -320,9 +320,9 @@ class Profile extends BaseModule
$this->logger->notice('Empty gsid for contact', ['contact' => $contact]); $this->logger->notice('Empty gsid for contact', ['contact' => $contact]);
} }
$serverIgnored = $contact['gsid'] && $serverIgnored = $contact['gsid']
$this->userGServer->isIgnoredByUser($this->session->getLocalUserId(), $contact['gsid']) ? && $this->userGServer->isIgnoredByUser($this->session->getLocalUserId(), $contact['gsid'])
$this->t('This contact is on a server you ignored.') ? $this->t('This contact is on a server you ignored.')
: ''; : '';
$last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A')); $last_update = (($contact['last-update'] <= DBA::NULL_DATETIME) ? $this->t('Never') : DateTimeFormat::local($contact['last-update'], 'D, j M Y, g:i A'));
@ -354,8 +354,8 @@ class Profile extends BaseModule
LocalRelationshipEntity::FFI_NONE => $this->t('Disabled'), LocalRelationshipEntity::FFI_NONE => $this->t('Disabled'),
LocalRelationshipEntity::FFI_INFORMATION => $this->t('Fetch information'), LocalRelationshipEntity::FFI_INFORMATION => $this->t('Fetch information'),
LocalRelationshipEntity::FFI_KEYWORD => $this->t('Fetch keywords'), LocalRelationshipEntity::FFI_KEYWORD => $this->t('Fetch keywords'),
LocalRelationshipEntity::FFI_BOTH => $this->t('Fetch information and keywords') LocalRelationshipEntity::FFI_BOTH => $this->t('Fetch information and keywords'),
] ],
]; ];
} }
@ -365,23 +365,23 @@ class Profile extends BaseModule
if ($contact['network'] == Protocol::FEED) { if ($contact['network'] == Protocol::FEED) {
$remote_self_options = [ $remote_self_options = [
LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'), LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'),
LocalRelationshipEntity::MIRROR_OWN_POST => $this->t('Mirror as my own posting') LocalRelationshipEntity::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
]; ];
} elseif ($contact['network'] == Protocol::ACTIVITYPUB) { } elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
$remote_self_options = [ $remote_self_options = [
LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'), LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'),
LocalRelationshipEntity::MIRROR_NATIVE_RESHARE => $this->t('Native reshare') LocalRelationshipEntity::MIRROR_NATIVE_RESHARE => $this->t('Native reshare'),
]; ];
} elseif ($contact['network'] == Protocol::DFRN) { } elseif ($contact['network'] == Protocol::DFRN) {
$remote_self_options = [ $remote_self_options = [
LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'), LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'),
LocalRelationshipEntity::MIRROR_OWN_POST => $this->t('Mirror as my own posting'), LocalRelationshipEntity::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
LocalRelationshipEntity::MIRROR_NATIVE_RESHARE => $this->t('Native reshare') LocalRelationshipEntity::MIRROR_NATIVE_RESHARE => $this->t('Native reshare'),
]; ];
} else { } else {
$remote_self_options = [ $remote_self_options = [
LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'), LocalRelationshipEntity::MIRROR_DEACTIVATED => $this->t('No mirroring'),
LocalRelationshipEntity::MIRROR_OWN_POST => $this->t('Mirror as my own posting') LocalRelationshipEntity::MIRROR_OWN_POST => $this->t('Mirror as my own posting'),
]; ];
} }
@ -475,7 +475,7 @@ class Profile extends BaseModule
$this->t('Mirror postings from this contact'), $this->t('Mirror postings from this contact'),
$localRelationship->remoteSelf, $localRelationship->remoteSelf,
$this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'), $this->t('Mark this contact as remote_self, this will cause friendica to repost new entries from this contact.'),
$remote_self_options $remote_self_options,
], ],
'$channel_settings_label' => $this->t('Channel Settings'), '$channel_settings_label' => $this->t('Channel Settings'),
'$frequency_label' => $this->t('Frequency of this contact in relevant channels'), '$frequency_label' => $this->t('Frequency of this contact in relevant channels'),

View file

@ -39,7 +39,7 @@ class Revoke extends BaseModule
{ {
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->dba = $dba; $this->dba = $dba;
if (!DI::userSession()->getLocalUserId()) { if (!DI::userSession()->getLocalUserId()) {
return; return;
@ -82,7 +82,7 @@ class Revoke extends BaseModule
return Login::form($_SERVER['REQUEST_URI']); return Login::form($_SERVER['REQUEST_URI']);
} }
Nav::setSelected('contact'); Nav::setSelected('contacts');
return Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_drop_confirm.tpl'), [ return Renderer::replaceMacros(Renderer::getMarkupTemplate('contact_drop_confirm.tpl'), [
'$l10n' => [ '$l10n' => [

View file

@ -116,7 +116,7 @@ class Unfollow extends \Friendica\BaseModule
'$myaddr' => $self['url'], '$myaddr' => $self['url'],
'$action' => $this->baseUrl . '/contact/unfollow', '$action' => $this->baseUrl . '/contact/unfollow',
'$keywords' => '', '$keywords' => '',
'$keywords_label' => '' '$keywords_label' => '',
]); ]);
$this->page['aside'] = Widget\VCard::getHTML(Contact::getByURL($contact['url'], false), false, true); $this->page['aside'] = Widget\VCard::getHTML(Contact::getByURL($contact['url'], false), false, true);
@ -124,7 +124,7 @@ class Unfollow extends \Friendica\BaseModule
$o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), ['$title' => $this->t('Posts and Replies')]); $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), ['$title' => $this->t('Posts and Replies')]);
// Show last public posts // Show last public posts
$o .= Contact::getPostsFromUrl($contact['url'], $this->userSession->getLocalUserId()); $o .= Contact::getPostsFromUrl($contact['url'], $this->userSession->getLocalUserId(), false, $request);
return $o; return $o;
} }

View file

@ -84,11 +84,6 @@ class Channel extends Timeline
'$header' => '', '$header' => '',
]); ]);
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) {
$tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
$o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]);
}
if (!$this->raw) { if (!$this->raw) {
$tabs = $this->getTabArray($this->channel->getTimelines($this->session->getLocalUserId()), 'channel'); $tabs = $this->getTabArray($this->channel->getTimelines($this->session->getLocalUserId()), 'channel');
$tabs = array_merge($tabs, $this->getTabArray($this->channelRepository->selectByUid($this->session->getLocalUserId()), 'channel')); $tabs = array_merge($tabs, $this->getTabArray($this->channelRepository->selectByUid($this->session->getLocalUserId()), 'channel'));
@ -126,18 +121,18 @@ class Channel extends Timeline
return $o; return $o;
} }
$o .= $this->conversation->render($items, Conversation::MODE_CHANNEL, false, false, $order, $this->session->getLocalUserId()); $o .= $this->conversation->render($items, Conversation::MODE_CHANNEL, $this->raw, false, $order, $this->session->getLocalUserId());
$pager = new BoundariesPager( $pager = new BoundariesPager(
$this->l10n, $this->l10n,
$this->args->getQueryString(), $this->args->getQueryString(),
$items[0][$order], $items[array_key_first($items)][$order],
$items[count($items) - 1][$order], $items[array_key_last($items)][$order],
$this->itemsPerPage $this->itemsPerPage,
); );
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) { if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) {
$o .= HTML::scrollLoader(); $o .= HTML::scrollLoader($request);
} else { } else {
$o .= $pager->renderMinimal(count($items)); $o .= $pager->renderMinimal(count($items));
} }

View file

@ -41,11 +41,11 @@ class Community extends Timeline
* Type of the community page * Type of the community page
* @{ * @{
*/ */
const DISABLED = -2; public const DISABLED = -2;
const DISABLED_VISITOR = -1; public const DISABLED_VISITOR = -1;
const LOCAL = 0; public const LOCAL = 0;
const GLOBAL = 1; public const GLOBAL = 1;
const LOCAL_AND_GLOBAL = 2; public const LOCAL_AND_GLOBAL = 2;
protected $pageStyle; protected $pageStyle;
@ -77,14 +77,9 @@ class Community extends Timeline
'$content' => '', '$content' => '',
'$header' => '', '$header' => '',
'$show_global_community_hint' => ($this->selectedTab == CommunityEntity::GLOBAL) && $this->config->get('system', 'show_global_community_hint'), '$show_global_community_hint' => ($this->selectedTab == CommunityEntity::GLOBAL) && $this->config->get('system', 'show_global_community_hint'),
'$global_community_hint' => $this->l10n->t("This community stream shows all public posts received by this node. They may not reflect the opinions of this nodes users.") '$global_community_hint' => $this->l10n->t("This community stream shows all public posts received by this node. They may not reflect the opinions of this nodes users."),
]); ]);
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) {
$tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
$o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]);
}
if (!$this->raw) { if (!$this->raw) {
$tabs = $this->getTabArray($this->community->getTimelines($this->session->isAuthenticated()), 'community'); $tabs = $this->getTabArray($this->community->getTimelines($this->session->isAuthenticated()), 'community');
$tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl'); $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
@ -112,23 +107,23 @@ class Community extends Timeline
if (!$this->database->isResult($items)) { if (!$this->database->isResult($items)) {
$o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [ $o .= Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [
'$title' => $this->l10n->t('No results.') '$title' => $this->l10n->t('No results.'),
]); ]);
return $o; return $o;
} }
$o .= $this->conversation->render($items, Conversation::MODE_COMMUNITY, false, false, 'received', $this->session->getLocalUserId()); $o .= $this->conversation->render($items, Conversation::MODE_COMMUNITY, $this->raw, false, 'received', $this->session->getLocalUserId());
$pager = new BoundariesPager( $pager = new BoundariesPager(
$this->l10n, $this->l10n,
$this->args->getQueryString(), $this->args->getQueryString(),
$items[array_key_first($items)]['received'], $items[array_key_first($items)]['received'],
$items[array_key_last($items)]['received'], $items[array_key_last($items)]['received'],
$this->itemsPerPage $this->itemsPerPage,
); );
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) { if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) {
$o .= HTML::scrollLoader(); $o .= HTML::scrollLoader($request);
} else { } else {
$o .= $pager->renderMinimal(count($items)); $o .= $pager->renderMinimal(count($items));
} }

View file

@ -163,7 +163,7 @@ class Network extends Timeline
]; ];
$this->eventDispatcher->dispatch( $this->eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::NETWORK_CONTENT_START, $hook_data) new ArrayFilterEvent(ArrayFilterEvent::NETWORK_CONTENT_START, $hook_data),
); );
$o = ''; $o = '';
@ -180,7 +180,7 @@ class Network extends Timeline
Feature::SEARCHES, Feature::SEARCHES,
Feature::FOLDERS, Feature::FOLDERS,
Feature::NOSHARER, Feature::NOSHARER,
Feature::TRENDING_TAGS Feature::TRENDING_TAGS,
]; ];
} }
@ -215,8 +215,8 @@ class Network extends Timeline
$this->page['aside'] .= TrendingTags::getHTML($this->selectedTab); $this->page['aside'] .= TrendingTags::getHTML($this->selectedTab);
break; break;
case Feature::NOSHARER: case Feature::NOSHARER:
if (($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) && if (($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId()))
!in_array($this->selectedTab, [Channel::FOLLOWERS, Channel::FORYOU, Channel::DISCOVER])) { && !in_array($this->selectedTab, [Channel::FOLLOWERS, Channel::FORYOU, Channel::DISCOVER])) {
$this->page['aside'] .= $this->getNoSharerWidget('network'); $this->page['aside'] .= $this->getNoSharerWidget('network');
} }
break; break;
@ -224,11 +224,6 @@ class Network extends Timeline
} }
} }
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true) && ($_GET['mode'] ?? '') != 'minimal') {
$tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
$o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]);
}
if (!$this->raw) { if (!$this->raw) {
$o .= $this->getTabsHTML(); $o .= $this->getTabsHTML();
@ -277,7 +272,7 @@ class Network extends Timeline
} }
$o = Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [ $o = Renderer::replaceMacros(Renderer::getMarkupTemplate('section_title.tpl'), [
'$title' => $this->l10n->t('Circle: %s', $circle['name']) '$title' => $this->l10n->t('Circle: %s', $circle['name']),
]) . $o; ]) . $o;
} elseif (Profile::shouldDisplayEventList($this->session->getLocalUserId(), $this->mode)) { } elseif (Profile::shouldDisplayEventList($this->session->getLocalUserId(), $this->mode)) {
$o .= Profile::getBirthdays($this->session->getLocalUserId()); $o .= Profile::getBirthdays($this->session->getLocalUserId());
@ -294,7 +289,7 @@ class Network extends Timeline
$items = $this->getItems(); $items = $this->getItems();
} }
$o .= $this->conversation->render($items, Conversation::MODE_NETWORK, false, false, $this->getOrder(), $this->session->getLocalUserId()); $o .= $this->conversation->render($items, Conversation::MODE_NETWORK, $this->raw, false, $this->getOrder(), $this->session->getLocalUserId());
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Exception when fetching items', ['code' => $e->getCode(), 'message' => $e->getMessage()]); $this->logger->error('Exception when fetching items', ['code' => $e->getCode(), 'message' => $e->getMessage()]);
$o .= $this->l10n->t('Error %d (%s) while fetching the timeline.', $e->getCode(), $e->getMessage()); $o .= $this->l10n->t('Error %d (%s) while fetching the timeline.', $e->getCode(), $e->getMessage());
@ -302,14 +297,14 @@ class Network extends Timeline
} }
if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) { if ($this->pConfig->get($this->session->getLocalUserId(), 'system', 'infinite_scroll', true)) {
$o .= HTML::scrollLoader(); $o .= HTML::scrollLoader($request);
} else { } else {
$pager = new BoundariesPager( $pager = new BoundariesPager(
$this->l10n, $this->l10n,
$this->args->getQueryString(), $this->args->getQueryString(),
$items[0][$this->order] ?? null, $items[array_key_first($items)][$this->order] ?? null,
$items[count($items) - 1][$this->order] ?? null, $items[array_key_last($items)][$this->order] ?? null,
$this->itemsPerPage $this->itemsPerPage,
); );
$o .= $pager->renderMinimal(count($items)); $o .= $pager->renderMinimal(count($items));
@ -364,7 +359,7 @@ class Network extends Timeline
]; ];
$hook_data = $this->eventDispatcher->dispatch( $hook_data = $this->eventDispatcher->dispatch(
new ArrayFilterEvent(ArrayFilterEvent::NETWORK_CONTENT_TABS, $hook_data) new ArrayFilterEvent(ArrayFilterEvent::NETWORK_CONTENT_TABS, $hook_data),
)->getArray(); )->getArray();
if (!empty($network_timelines)) { if (!empty($network_timelines)) {
@ -388,7 +383,7 @@ class Network extends Timeline
{ {
parent::parseRequest($request); parent::parseRequest($request);
$this->circleId = (int)($this->parameters['circle_id'] ?? 0); $this->circleId = (int) ($this->parameters['circle_id'] ?? 0);
if (!$this->selectedTab) { if (!$this->selectedTab) {
$this->selectedTab = $this->getTimelineOrderBySession(); $this->selectedTab = $this->getTimelineOrderBySession();
@ -427,7 +422,7 @@ class Network extends Timeline
$this->order = 'commented'; $this->order = 'commented';
} }
$this->selectedTab = $this->selectedTab ?? $this->order; $this->selectedTab ??= $this->order;
// Upon updates in the background and order by last comment we order by received date, // Upon updates in the background and order by last comment we order by received date,
// since otherwise the feed will optically jump, when some already visible thread has been updated. // since otherwise the feed will optically jump, when some already visible thread has been updated.
@ -468,51 +463,51 @@ class Network extends Timeline
protected function getItems() protected function getItems()
{ {
$conditionFields = ['uid' => $this->session->getLocalUserId()]; $timelineCondition = [];
$conditionStrings = []; $commonCondition = ['uid' => $this->session->getLocalUserId()];
if (!is_null($this->accountType)) { if (!is_null($this->accountType)) {
$conditionFields['contact-type'] = $this->accountType; $commonCondition['contact-type'] = $this->accountType;
} }
if ($this->star) { if ($this->star) {
$conditionFields['starred'] = true; $timelineCondition['starred'] = true;
} }
if ($this->mention) { if ($this->mention) {
$conditionFields['mention'] = true; $timelineCondition['mention'] = true;
} }
if ($this->network) { if ($this->network) {
$conditionFields['network'] = $this->network; $commonCondition['network'] = $this->network;
} }
if ($this->dateFrom) { if ($this->dateFrom) {
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` <= ? ", DateTimeFormat::convert($this->dateFrom, 'UTC', $this->appHelper->getTimeZone())]); $commonCondition = DBA::mergeConditions($commonCondition, ["`received` <= ? ", DateTimeFormat::convert($this->dateFrom, 'UTC', $this->appHelper->getTimeZone())]);
} }
if ($this->dateTo) { if ($this->dateTo) {
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` >= ? ", DateTimeFormat::convert($this->dateTo, 'UTC', $this->appHelper->getTimeZone())]); $commonCondition = DBA::mergeConditions($commonCondition, ["`received` >= ? ", DateTimeFormat::convert($this->dateTo, 'UTC', $this->appHelper->getTimeZone())]);
} }
if ($this->circleId) { if ($this->circleId) {
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`contact-id` IN (SELECT `contact-id` FROM `group_member` WHERE `gid` = ?)", $this->circleId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`contact-id` IN (SELECT `contact-id` FROM `group_member` WHERE `gid` = ?)", $this->circleId]);
} }
// Currently only the order modes "received" and "commented" are in use // Currently only the order modes "received" and "commented" are in use
if (!empty($this->itemUriId)) { if (!empty($this->itemUriId)) {
$conditionStrings = DBA::mergeConditions($conditionStrings, ['uri-id' => $this->itemUriId]); $commonCondition = DBA::mergeConditions($commonCondition, ['uri-id' => $this->itemUriId]);
} else { } else {
if (isset($this->maxId)) { if (isset($this->maxId)) {
switch ($this->order) { switch ($this->order) {
case 'received': case 'received':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` < ?", $this->maxId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`received` < ?", $this->maxId]);
break; break;
case 'commented': case 'commented':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`commented` < ?", $this->maxId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`commented` < ?", $this->maxId]);
break; break;
case 'created': case 'created':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`created` < ?", $this->maxId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`created` < ?", $this->maxId]);
break; break;
case 'uriid': case 'uriid':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`uri-id` < ?", $this->maxId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`uri-id` < ?", $this->maxId]);
break; break;
} }
} }
@ -520,16 +515,16 @@ class Network extends Timeline
if (isset($this->minId)) { if (isset($this->minId)) {
switch ($this->order) { switch ($this->order) {
case 'received': case 'received':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`received` > ?", $this->minId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`received` > ?", $this->minId]);
break; break;
case 'commented': case 'commented':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`commented` > ?", $this->minId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`commented` > ?", $this->minId]);
break; break;
case 'created': case 'created':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`created` > ?", $this->minId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`created` > ?", $this->minId]);
break; break;
case 'uriid': case 'uriid':
$conditionStrings = DBA::mergeConditions($conditionStrings, ["`uri-id` > ?", $this->minId]); $commonCondition = DBA::mergeConditions($commonCondition, ["`uri-id` > ?", $this->minId]);
break; break;
} }
} }
@ -546,13 +541,65 @@ class Network extends Timeline
$params['order'] = [$this->order => true]; $params['order'] = [$this->order => true];
} }
$items = $this->database->selectToArray($this->circleId ? 'network-thread-circle-view' : 'network-thread-view', [], DBA::mergeConditions($conditionFields, $conditionStrings), $params); $filterchannels = $this->pConfig->get($this->session->getLocalUserId(), 'channel', 'filter_channels') ?? [];
if ($filterchannels) {
$query = "`uri-id` NOT IN (SELECT `uri-id` FROM `channel-post-view` WHERE `uid` = ? AND `channel` IN (" . substr(str_repeat('?, ', count($filterchannels)), 0, -2) . "))";
$commonCondition = DBA::mergeConditions($commonCondition, array_merge([$query], [$this->session->getLocalUserId()], $filterchannels));
}
$fields = ['uri-id', 'created', 'received', 'commented', 'channel', 'contact-id'];
$condition = DBA::mergeConditions($timelineCondition, $commonCondition);
$timeline = $this->database->getSQL($this->circleId ? 'network-thread-circle-view' : 'network-thread-view', $fields, $condition, $params);
array_shift($condition);
$sql = '(' . $timeline . ')';
$systemchannels = [];
$userchannels = [];
if (!$this->mention && !$this->star) {
foreach ($this->pConfig->get($this->session->getLocalUserId(), 'channel', 'timeline_channels') ?? [] as $channel) {
if (is_numeric($channel)) {
$userchannels[] = (int) $channel;
} else {
$systemchannels[] = $channel;
}
}
}
if ($systemchannels) {
$channel_condition = DBA::mergeConditions(['channel' => $systemchannels, 'in-timeline' => false], $commonCondition);
$sql .= ' UNION ALL (' . $this->database->getSQL('system-channel-post-view', $fields, $channel_condition, $params) . ')';
array_shift($channel_condition);
$condition = array_merge($condition, $channel_condition);
}
if ($userchannels) {
$channel_condition = DBA::mergeConditions(['channel' => $userchannels, 'in-timeline' => false], $commonCondition);
$sql .= ' UNION ALL (' . $this->database->getSQL('channel-post-view', $fields, $channel_condition, $params) . ')';
array_shift($channel_condition);
$condition = array_merge($condition, $channel_condition);
}
if ($systemchannels || $userchannels) {
$sql .= DBA::buildParameter($params);
}
$result = $this->database->p($sql, $condition);
$items = $this->database->toArray($result);
// min_id quirk, continued // min_id quirk, continued
if (isset($this->minId) && !isset($this->maxId)) { if (isset($this->minId) && !isset($this->maxId)) {
$items = array_reverse($items); $items = array_reverse($items);
} }
// Avoid duplicates
$posts = [];
foreach ($items as $item) {
$posts[$item['uri-id']] = $item;
}
$items = $posts;
if ($this->ping || !$this->database->isResult($items)) { if ($this->ping || !$this->database->isResult($items)) {
return $items; return $items;
} }

View file

@ -14,6 +14,7 @@ use Friendica\BaseModule;
use Friendica\Content\Conversation\Collection\Timelines; use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity; use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Content\Conversation\Entity\Community; use Friendica\Content\Conversation\Entity\Community;
use Friendica\Content\Conversation\Entity\UserDefinedChannel as EntityUserDefinedChannel;
use Friendica\Content\Conversation\Repository\UserDefinedChannel; use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\Config\Capability\IManageConfigValues;
@ -29,6 +30,7 @@ use Friendica\Database\DBA;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Post\SearchIndex; use Friendica\Model\Post\SearchIndex;
use Friendica\Model\Verb;
use Friendica\Module\Response; use Friendica\Module\Response;
use Friendica\Network\HTTPException\BadRequestException; use Friendica\Network\HTTPException\BadRequestException;
use Friendica\Network\HTTPException\ForbiddenException; use Friendica\Network\HTTPException\ForbiddenException;
@ -115,14 +117,14 @@ class Timeline extends BaseModule
$this->session->getLocalUserId(), $this->session->getLocalUserId(),
'system', 'system',
'itemspage_mobile_network', 'itemspage_mobile_network',
$this->config->get('system', 'itemspage_network_mobile') $this->config->get('system', 'itemspage_network_mobile'),
); );
} else { } else {
$this->itemsPerPage = $this->pConfig->get( $this->itemsPerPage = $this->pConfig->get(
$this->session->getLocalUserId(), $this->session->getLocalUserId(),
'system', 'system',
'itemspage_network', 'itemspage_network',
$this->config->get('system', 'itemspage_network') $this->config->get('system', 'itemspage_network'),
); );
} }
@ -319,9 +321,9 @@ class Timeline extends BaseModule
if ($this->selectedTab == ChannelEntity::WHATSHOT) { if ($this->selectedTab == ChannelEntity::WHATSHOT) {
if (!$cache) { if (!$cache) {
if (!is_null($this->accountType)) { if (!is_null($this->accountType)) {
$condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` = ?", $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $this->accountType]; $condition = ["(`comments` > ? OR `activities` > ? OR `views` > ?) AND `contact-type` = ?", $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $this->channelRepository->getMedianViews($uid, 4), $this->accountType];
} else { } else {
$condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` != ?", $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), Contact::TYPE_COMMUNITY]; $condition = ["(`comments` > ? OR `activities` > ? OR `views` > ?) AND `contact-type` != ?", $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $this->channelRepository->getMedianViews($uid, 4), Contact::TYPE_COMMUNITY];
} }
} }
} elseif ($this->selectedTab == ChannelEntity::FORYOU) { } elseif ($this->selectedTab == ChannelEntity::FORYOU) {
@ -330,10 +332,10 @@ class Timeline extends BaseModule
$condition = [ $condition = [
"(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?) OR "(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?) OR
((`comments` >= ? OR `activities` >= ?) AND `owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?)) OR ((`comments` >= ? OR `activities` >= ? OR `views` >= ?) AND `owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?)) OR
(`owner-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`notify_new_posts` OR `channel-frequency` = ?))))", (`owner-id` IN (SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND (`notify_new_posts` OR `channel-frequency` = ?))))",
$cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $cid, $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $this->channelRepository->getMedianViews($uid, 4), $cid,
$uid, Contact\User::FREQUENCY_ALWAYS $uid, Contact\User::FREQUENCY_ALWAYS,
]; ];
} }
} elseif ($this->selectedTab == ChannelEntity::DISCOVER) { } elseif ($this->selectedTab == ChannelEntity::DISCOVER) {
@ -344,11 +346,11 @@ class Timeline extends BaseModule
"`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND NOT `follows`) AND "`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND NOT `follows`) AND
(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND NOT `follows` AND `relation-thread-score` > ?) OR (`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND NOT `follows` AND `relation-thread-score` > ?) OR
`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `cid` = ? AND `relation-thread-score` > ?) OR `owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `cid` = ? AND `relation-thread-score` > ?) OR
((`comments` >= ? OR `activities` >= ?) AND ((`comments` >= ? OR `activities` >= ? OR `views` >= ?) AND
(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `cid` = ? AND `relation-thread-score` > ?)) OR (`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `cid` = ? AND `relation-thread-score` > ?)) OR
(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?))))", (`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?))))",
$cid, $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $cid, $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4),
$this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $cid, 0, $cid, 0 $this->channelRepository->getMedianComments($uid, 4), $this->channelRepository->getMedianActivities($uid, 4), $this->channelRepository->getMedianViews($uid, 4), $cid, 0, $cid, 0,
]; ];
} }
} elseif ($this->selectedTab == ChannelEntity::FOLLOWERS) { } elseif ($this->selectedTab == ChannelEntity::FOLLOWERS) {
@ -364,7 +366,7 @@ class Timeline extends BaseModule
"`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `last-interaction` > ? "`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `last-interaction` > ?
AND `relation-cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `relation-thread-score` >= ?) AND `relation-cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `relation-thread-score` >= ?)
AND NOT `cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?))", AND NOT `cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?))",
DateTimeFormat::utc('now - ' . $this->config->get('channel', 'sharer_interaction_days') . ' day'), $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $cid DateTimeFormat::utc('now - ' . $this->config->get('channel', 'sharer_interaction_days') . ' day'), $cid, $this->channelRepository->getMedianRelationThreadScore($cid, 4), $cid,
]; ];
} }
} elseif ($this->selectedTab == ChannelEntity::QUIETSHARERS) { } elseif ($this->selectedTab == ChannelEntity::QUIETSHARERS) {
@ -373,7 +375,7 @@ class Timeline extends BaseModule
$condition = [ $condition = [
"`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `post-score` <= ?)", "`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `post-score` <= ?)",
$cid, $this->channelRepository->getMedianPostScore($cid, 2) $cid, $this->channelRepository->getMedianPostScore($cid, 2),
]; ];
} }
} elseif ($this->selectedTab == ChannelEntity::IMAGE) { } elseif ($this->selectedTab == ChannelEntity::IMAGE) {
@ -395,7 +397,7 @@ class Timeline extends BaseModule
} elseif (is_numeric($this->selectedTab) && !empty($channel = $this->channelRepository->selectById($this->selectedTab, $uid))) { } elseif (is_numeric($this->selectedTab) && !empty($channel = $this->channelRepository->selectById($this->selectedTab, $uid))) {
if (!$this->config->get('system', 'channel_cache')) { if (!$this->config->get('system', 'channel_cache')) {
$condition = $this->channelRepository->getCondition($channel, $uid); $condition = $this->channelRepository->getCondition($channel, $uid);
if (in_array($channel->circle, [-3, -4, -5])) { if (in_array($channel->circle, [EntityUserDefinedChannel::CIRCLE_CREATION, EntityUserDefinedChannel::CIRCLE_POSTS, EntityUserDefinedChannel::CIRCLE_ACTIVITY])) {
$table = SearchIndex::getSearchView(); $table = SearchIndex::getSearchView();
$condition = DBA::mergeConditions($condition, ['uid' => $uid]); $condition = DBA::mergeConditions($condition, ['uid' => $uid]);
} }
@ -403,9 +405,13 @@ class Timeline extends BaseModule
$condition = ['channel' => $this->selectedTab]; $condition = ['channel' => $this->selectedTab];
$table = 'channel-post-view'; $table = 'channel-post-view';
} }
if (in_array($channel->circle, [-3, -4, -5])) { if (in_array($channel->circle, [EntityUserDefinedChannel::CIRCLE_CREATION, EntityUserDefinedChannel::CIRCLE_POSTS, EntityUserDefinedChannel::CIRCLE_ACTIVITY])) {
$orders = ['-3' => 'created', '-4' => 'received', '-5' => 'commented']; $orders = [
$this->order = $orders[$channel->circle]; EntityUserDefinedChannel::CIRCLE_CREATION => 'created',
EntityUserDefinedChannel::CIRCLE_POSTS => 'received',
EntityUserDefinedChannel::CIRCLE_ACTIVITY => 'commented',
];
$this->order = $orders[(int) $channel->circle];
} }
} }
@ -493,10 +499,10 @@ class Timeline extends BaseModule
$maxpostperauthor = 0; $maxpostperauthor = 0;
if ($this->selectedTab == Community::LOCAL) { if ($this->selectedTab == Community::LOCAL) {
$maxpostperauthor = (int)$this->config->get('system', 'max_author_posts_community_page'); $maxpostperauthor = (int) $this->config->get('system', 'max_author_posts_community_page');
$key = 'author-id'; $key = 'author-id';
} elseif ($this->selectedTab == Community::GLOBAL) { } elseif ($this->selectedTab == Community::GLOBAL) {
$maxpostperauthor = (int)$this->config->get('system', 'max_server_posts_community_page'); $maxpostperauthor = (int) $this->config->get('system', 'max_server_posts_community_page');
$key = 'author-gsid'; $key = 'author-gsid';
} }
@ -612,7 +618,7 @@ class Timeline extends BaseModule
$uriids = array_keys($items); $uriids = array_keys($items);
foreach (Post\Counts::get(['parent-uri-id' => $uriids, 'verb' => Activity::POST]) as $count) { foreach (Post\Counts::get(['parent-uri-id' => $uriids, 'vid' => Verb::getID(Activity::POST)]) as $count) {
$items[$count['parent-uri-id']]['comments'] += $count['count']; $items[$count['parent-uri-id']]['comments'] += $count['count'];
} }

View file

@ -42,19 +42,19 @@ class Babel extends BaseModule
$bbcode = $request['text']; $bbcode = $request['text'];
$results[] = [ $results[] = [
'title' => 'Source input', 'title' => 'Source input',
'content' => $visible_whitespace($bbcode) 'content' => $visible_whitespace($bbcode),
]; ];
$plain = Text\BBCode::toPlaintext($bbcode, false); $plain = Text\BBCode::toPlaintext($bbcode, false);
$results[] = [ $results[] = [
'title' => 'BBCode::toPlaintext', 'title' => 'BBCode::toPlaintext',
'content' => $visible_whitespace($plain) 'content' => $visible_whitespace($plain),
]; ];
$html = Text\BBCode::convertForUriId(0, $bbcode); $html = Text\BBCode::convertForUriId(0, $bbcode);
$results[] = [ $results[] = [
'title' => 'BBCode::convert (raw HTML)', 'title' => 'BBCode::convert (raw HTML)',
'content' => $visible_whitespace($html) 'content' => $visible_whitespace($html),
]; ];
$results[] = [ $results[] = [
@ -64,41 +64,41 @@ class Babel extends BaseModule
$results[] = [ $results[] = [
'title' => 'BBCode::convert', 'title' => 'BBCode::convert',
'content' => $html 'content' => $html,
]; ];
$bbcode2 = Text\HTML::toBBCode($html); $bbcode2 = Text\HTML::toBBCode($html);
$results[] = [ $results[] = [
'title' => 'BBCode::convert => HTML::toBBCode', 'title' => 'BBCode::convert => HTML::toBBCode',
'content' => $visible_whitespace($bbcode2) 'content' => $visible_whitespace($bbcode2),
]; ];
$markdown = Text\BBCode::toMarkdown($bbcode); $markdown = Text\BBCode::toMarkdown($bbcode);
$results[] = [ $results[] = [
'title' => 'BBCode::toMarkdown', 'title' => 'BBCode::toMarkdown',
'content' => $visible_whitespace($markdown) 'content' => $visible_whitespace($markdown),
]; ];
$html2 = Text\Markdown::convert($markdown); $html2 = Text\Markdown::convert($markdown);
$results[] = [ $results[] = [
'title' => 'BBCode::toMarkdown => Markdown::convert (raw HTML)', 'title' => 'BBCode::toMarkdown => Markdown::convert (raw HTML)',
'content' => $visible_whitespace($html2) 'content' => $visible_whitespace($html2),
]; ];
$results[] = [ $results[] = [
'title' => 'BBCode::toMarkdown => Markdown::convert', 'title' => 'BBCode::toMarkdown => Markdown::convert',
'content' => $html2 'content' => $html2,
]; ];
$bbcode3 = Text\Markdown::toBBCode($markdown); $bbcode3 = Text\Markdown::toBBCode($markdown);
$results[] = [ $results[] = [
'title' => 'BBCode::toMarkdown => Markdown::toBBCode', 'title' => 'BBCode::toMarkdown => Markdown::toBBCode',
'content' => $visible_whitespace($bbcode3) 'content' => $visible_whitespace($bbcode3),
]; ];
$bbcode4 = Text\HTML::toBBCode($html2); $bbcode4 = Text\HTML::toBBCode($html2);
$results[] = [ $results[] = [
'title' => 'BBCode::toMarkdown => Markdown::convert => HTML::toBBCode', 'title' => 'BBCode::toMarkdown => Markdown::convert => HTML::toBBCode',
'content' => $visible_whitespace($bbcode4) 'content' => $visible_whitespace($bbcode4),
]; ];
$tags = Text\BBCode::getTags($bbcode); $tags = Text\BBCode::getTags($bbcode);
@ -106,7 +106,7 @@ class Babel extends BaseModule
$body = Item::setHashtags($bbcode); $body = Item::setHashtags($bbcode);
$results[] = [ $results[] = [
'title' => 'Item Body', 'title' => 'Item Body',
'content' => $visible_whitespace($body) 'content' => $visible_whitespace($body),
]; ];
$results[] = [ $results[] = [
'title' => 'Item Tags', 'title' => 'Item Tags',
@ -116,16 +116,16 @@ class Babel extends BaseModule
$body2 = PageInfo::searchAndAppendToBody($bbcode, true); $body2 = PageInfo::searchAndAppendToBody($bbcode, true);
$results[] = [ $results[] = [
'title' => 'PageInfo::appendToBody', 'title' => 'PageInfo::appendToBody',
'content' => $visible_whitespace($body2) 'content' => $visible_whitespace($body2),
]; ];
$html3 = Text\BBCode::convertForUriId(0, $body2); $html3 = Text\BBCode::convertForUriId(0, $body2);
$results[] = [ $results[] = [
'title' => 'PageInfo::appendToBody => BBCode::convert (raw HTML)', 'title' => 'PageInfo::appendToBody => BBCode::convert (raw HTML)',
'content' => $visible_whitespace($html3) 'content' => $visible_whitespace($html3),
]; ];
$results[] = [ $results[] = [
'title' => 'PageInfo::appendToBody => BBCode::convert', 'title' => 'PageInfo::appendToBody => BBCode::convert',
'content' => $html3 'content' => $html3,
]; ];
break; break;
case 'diaspora': case 'diaspora':
@ -138,7 +138,7 @@ class Babel extends BaseModule
$markdown = XML::unescape($diaspora); $markdown = XML::unescape($diaspora);
// no break // no break
case 'markdown': case 'markdown':
$markdown = $markdown ?? trim($request['text']); $markdown ??= trim($request['text']);
$results[] = [ $results[] = [
'title' => 'Source input (Markdown)', 'title' => 'Source input (Markdown)',
@ -153,7 +153,7 @@ class Babel extends BaseModule
$results[] = [ $results[] = [
'title' => 'Markdown::convert', 'title' => 'Markdown::convert',
'content' => $html 'content' => $html,
]; ];
$bbcode = Text\Markdown::toBBCode($markdown); $bbcode = Text\Markdown::toBBCode($markdown);
@ -171,7 +171,7 @@ class Babel extends BaseModule
$results[] = [ $results[] = [
'title' => 'HTML Input', 'title' => 'HTML Input',
'content' => $html 'content' => $html,
]; ];
$purified = Text\HTML::purify($html); $purified = Text\HTML::purify($html);
@ -194,18 +194,18 @@ class Babel extends BaseModule
$bbcode = Text\HTML::toBBCode($html); $bbcode = Text\HTML::toBBCode($html);
$results[] = [ $results[] = [
'title' => 'HTML::toBBCode', 'title' => 'HTML::toBBCode',
'content' => $visible_whitespace($bbcode) 'content' => $visible_whitespace($bbcode),
]; ];
$html2 = Text\BBCode::convertForUriId(0, $bbcode); $html2 = Text\BBCode::convertForUriId(0, $bbcode);
$results[] = [ $results[] = [
'title' => 'HTML::toBBCode => BBCode::convert', 'title' => 'HTML::toBBCode => BBCode::convert',
'content' => $html2 'content' => $html2,
]; ];
$results[] = [ $results[] = [
'title' => 'HTML::toBBCode => BBCode::convert (raw HTML)', 'title' => 'HTML::toBBCode => BBCode::convert (raw HTML)',
'content' => htmlspecialchars($html2) 'content' => htmlspecialchars($html2),
]; ];
$bbcode2plain = Text\BBCode::toPlaintext($bbcode); $bbcode2plain = Text\BBCode::toPlaintext($bbcode);
@ -217,7 +217,7 @@ class Babel extends BaseModule
$markdown = Text\HTML::toMarkdown($html); $markdown = Text\HTML::toMarkdown($html);
$results[] = [ $results[] = [
'title' => 'HTML::toMarkdown', 'title' => 'HTML::toMarkdown',
'content' => $visible_whitespace($markdown) 'content' => $visible_whitespace($markdown),
]; ];
$text = Text\HTML::toPlaintext($html, 0); $text = Text\HTML::toPlaintext($html, 0);

View file

@ -27,7 +27,7 @@ class WebFinger extends BaseModule
$res = ''; $res = '';
if (!empty($addr)) { if (!empty($addr)) {
$res = Probe::lrdd($addr); $res = Probe::getWebfingerArray($addr);
$res = print_r($res, true); $res = print_r($res, true);
} }

View file

@ -29,8 +29,8 @@ class Directory extends BaseModule
{ {
$config = DI::config(); $config = DI::config();
if (($config->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) || if (($config->get('system', 'block_public') && !DI::userSession()->isAuthenticated())
($config->get('system', 'block_local_dir') && !DI::userSession()->isAuthenticated())) { || ($config->get('system', 'block_local_dir') && !DI::userSession()->isAuthenticated())) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Public access denied.')); throw new HTTPException\ForbiddenException(DI::l10n()->t('Public access denied.'));
} }
@ -56,9 +56,7 @@ class Directory extends BaseModule
$profiles = Profile::searchProfiles($pager->getStart(), $pager->getItemsPerPage(), $search); $profiles = Profile::searchProfiles($pager->getStart(), $pager->getItemsPerPage(), $search);
if ($profiles['total'] === 0) { if ($profiles['total'] !== 0) {
DI::sysmsg()->addNotice(DI::l10n()->t('No entries (some entries may be hidden).'));
} else {
foreach ($profiles['entries'] as $entry) { foreach ($profiles['entries'] as $entry) {
$contact = Model\Contact::getByURLForUser($entry['url'], DI::userSession()->getLocalUserId()); $contact = Model\Contact::getByURLForUser($entry['url'], DI::userSession()->getLocalUserId());
if (!empty($contact)) { if (!empty($contact)) {
@ -70,17 +68,19 @@ class Directory extends BaseModule
$tpl = Renderer::getMarkupTemplate('directory_header.tpl'); $tpl = Renderer::getMarkupTemplate('directory_header.tpl');
$output .= Renderer::replaceMacros($tpl, [ $output .= Renderer::replaceMacros($tpl, [
'$search' => $search, '$search' => $search,
'$globaldir' => DI::l10n()->t('Global Directory'), '$globaldir' => DI::l10n()->t('Global Directory'),
'$gDirPath' => $gDirPath, '$gDirPath' => $gDirPath,
'$desc' => DI::l10n()->t('Find on this site'), '$desc' => DI::l10n()->t('Find on this site'),
'$contacts' => $entries, '$contacts' => $entries,
'$finding' => DI::l10n()->t('Results for:'), '$no_results' => DI::l10n()->t('No accounts found (some may be hidden).'),
'$findterm' => (strlen($search) ? $search : ""), '$finding' => DI::l10n()->t('Results for:'),
'$title' => DI::l10n()->t('Site Directory'), '$findterm' => (strlen($search) ? $search : ""),
'$search_mod' => 'directory', '$title' => DI::l10n()->t('Site Directory'),
'$submit' => DI::l10n()->t('Find'), '$search_mod' => 'directory',
'$paginate' => $pager->renderFull($profiles['total']), '$submit' => DI::l10n()->t('Find'),
'$paginate' => $pager->renderFull($profiles['total']),
'$num_results_text' => DI::l10n()->t("Accounts listed: %s", $profiles['total']),
]); ]);
return $output; return $output;
@ -140,7 +140,7 @@ class Directory extends BaseModule
$location_e = $location; $location_e = $location;
$photo_menu = [ $photo_menu = [
'profile' => [DI::l10n()->t("View Profile"), Model\Contact::magicLink($profile_link)] 'profile' => [DI::l10n()->t("View Profile"), Model\Contact::magicLink($profile_link)],
]; ];
$entry = [ $entry = [

View file

@ -78,7 +78,7 @@ class Compose extends BaseModule
protected function post(array $request = []) protected function post(array $request = [])
{ {
if (!empty($_REQUEST['body'])) { if (!empty($request['body'])) {
$_REQUEST['return'] = 'network'; $_REQUEST['return'] = 'network';
require_once 'mod/item.php'; require_once 'mod/item.php';
item_post(); item_post();
@ -131,10 +131,10 @@ class Compose extends BaseModule
$type = 'post'; $type = 'post';
$doesFederate = true; $doesFederate = true;
$contact_allow = $_REQUEST['contact_allow'] ?? ''; $contact_allow = $request['contact_allow'] ?? '';
$circle_allow = $_REQUEST['circle_allow'] ?? ''; $circle_allow = $request['circle_allow'] ?? '';
$contact_deny = $_REQUEST['contact_deny'] ?? ''; $contact_deny = $request['contact_deny'] ?? '';
$circle_deny = $_REQUEST['circle_deny'] ?? ''; $circle_deny = $request['circle_deny'] ?? '';
if ($contact_allow if ($contact_allow
. $circle_allow . $circle_allow
@ -149,11 +149,13 @@ class Compose extends BaseModule
break; break;
} }
$title = $_REQUEST['title'] ?? ''; $title = $request['title'] ?? '';
$category = $_REQUEST['category'] ?? ''; $summary = $request['summary'] ?? '';
$body = $_REQUEST['body'] ?? ''; $sensitive = $request['sensitive'] ?? false;
$location = $_REQUEST['location'] ?? $user['default-location']; $category = $request['category'] ?? '';
$wall = $_REQUEST['wall'] ?? $type == 'post'; $body = $request['body'] ?? '';
$location = $request['location'] ?? $user['default-location'];
$wall = $request['wall'] ?? $type == 'post';
$jotplugins = $this->eventDispatcher->dispatch( $jotplugins = $this->eventDispatcher->dispatch(
new HtmlFilterEvent(HtmlFilterEvent::JOT_TOOL, ''), new HtmlFilterEvent(HtmlFilterEvent::JOT_TOOL, ''),
@ -172,7 +174,7 @@ class Compose extends BaseModule
new DateTime('now'), new DateTime('now'),
null, null,
$this->l10n->t('Created at'), $this->l10n->t('Created at'),
'created_at' 'created_at',
); );
} else { } else {
$created_at = ''; $created_at = '';
@ -183,6 +185,7 @@ class Compose extends BaseModule
'$l10n' => [ '$l10n' => [
'compose_title' => $compose_title, 'compose_title' => $compose_title,
'default' => '', 'default' => '',
'summary' => $this->l10n->t('Summary'),
'visibility_title' => $this->l10n->t('Visibility'), 'visibility_title' => $this->l10n->t('Visibility'),
'mytitle' => $this->l10n->t('This is you'), 'mytitle' => $this->l10n->t('This is you'),
'submit' => $this->l10n->t('Submit'), 'submit' => $this->l10n->t('Submit'),
@ -204,14 +207,15 @@ class Compose extends BaseModule
'location_disabled' => $this->l10n->t('Location services are disabled. Please check the website\'s permissions on your device'), 'location_disabled' => $this->l10n->t('Location services are disabled. Please check the website\'s permissions on your device'),
'wait' => $this->l10n->t('Please wait'), 'wait' => $this->l10n->t('Please wait'),
'placeholdertitle' => $this->l10n->t('Set title'), 'placeholdertitle' => $this->l10n->t('Set title'),
'placeholdersummary' => Feature::isEnabled($this->session->getLocalUserId(), Feature::SUMMARY) ? $this->l10n->t('Set summary, abstract or spoiler text') : '',
'placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->l10n->t('Categories (comma-separated list)') : '', 'placeholdercategory' => Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->l10n->t('Categories (comma-separated list)') : '',
'always_open_compose' => $this->pConfig->get( 'always_open_compose' => $this->pConfig->get(
$this->session->getLocalUserId(), $this->session->getLocalUserId(),
'frio', 'frio',
'always_open_compose', 'always_open_compose',
$this->config->get('frio', 'always_open_compose', false) $this->config->get('frio', 'always_open_compose', false),
) ? '' : ) ? ''
$this->l10n->t('You can make this page always open when you use the New Post button in the <a href="/settings/display">Theme Customization settings</a>.'), : $this->l10n->t('If you want to always use this editor for posting, you can configure the New Post button to always open it in your <a href="/settings/display">Theme settings</a>.'),
], ],
'$id' => 0, '$id' => 0,
@ -220,15 +224,18 @@ class Compose extends BaseModule
'$wall' => $wall, '$wall' => $wall,
'$mylink' => $this->baseUrl->remove($contact['url']), '$mylink' => $this->baseUrl->remove($contact['url']),
'$myphoto' => $this->baseUrl->remove($contact['thumb']), '$myphoto' => $this->baseUrl->remove($contact['thumb']),
'$sensitive' => ['sensitive', $this->l10n->t('Sensitive post'), $request['sensitive'] ?? false],
'$scheduled_at' => Temporal::getDateTimeField( '$scheduled_at' => Temporal::getDateTimeField(
new DateTime(), new DateTime(),
new DateTime('now + 6 months'), new DateTime('now + 6 months'),
null, null,
$this->l10n->t('Scheduled at'), $this->l10n->t('Scheduled at'),
'scheduled_at' 'scheduled_at',
), ),
'$created_at' => $created_at, '$created_at' => $created_at,
'$title' => $title, '$title' => $title,
'$summary' => $summary,
'sensitive' => $sensitive,
'$category' => $category, '$category' => $category,
'$body' => $body, '$body' => $body,
'$location' => $location, '$location' => $location,

View file

@ -89,7 +89,7 @@ class Display extends BaseModule
$item = null; $item = null;
$itemUid = $this->session->getLocalUserId(); $itemUid = $this->session->getLocalUserId();
$fields = ['uri-id', 'parent-uri-id', 'author-id', 'author-link', 'contact-id', 'contact-contact-type', 'body', 'uid', 'guid', 'gravity', $fields = ['id', 'uri-id', 'parent-uri-id', 'author-id', 'author-link', 'contact-id', 'contact-contact-type', 'body', 'uid', 'guid', 'gravity',
'plink', 'origin', 'uri', 'post-reason', 'owner-contact-type', 'owner-network', 'owner-id', 'guid', 'plink', 'origin', 'uri', 'post-reason', 'owner-contact-type', 'owner-network', 'owner-id', 'guid',
'author-network', 'author-alias', 'private']; 'author-network', 'author-alias', 'private'];
@ -148,6 +148,10 @@ class Display extends BaseModule
$this->notify->setAllSeenForUser($this->session->getLocalUserId(), ['parent-uri-id' => $item['parent-uri-id']]); $this->notify->setAllSeenForUser($this->session->getLocalUserId(), ['parent-uri-id' => $item['parent-uri-id']]);
} }
if ($this->session->getLocalUserId() != 0) {
$this->contentItem->setViewed($item['uri-id'], $this->session->getLocalUserId());
}
$this->displaySidebar($item); $this->displaySidebar($item);
$this->displayHead($item['uri-id'], $item['parent-uri-id']); $this->displayHead($item['uri-id'], $item['parent-uri-id']);

236
src/Module/LostPass.php Normal file
View file

@ -0,0 +1,236 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module;
use Friendica\App\Arguments;
use Friendica\App\BaseURL;
use Friendica\BaseModule;
use Friendica\Core\Renderer;
use Friendica\Database\DBA;
use Friendica\Model\User;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
use Friendica\Navigation\SystemMessages;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Util\Emailer;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* Lost password module
*
* This module handles password reset functionality for users who have forgotten their passwords.
*/
final class LostPass extends BaseModule
{
private SystemMessages $sysMessages;
private IManageConfigValues $config;
private Emailer $emailer;
/**
* Initialize the module
*
* @param L10n $l10n
* @param BaseURL $baseUrl
* @param Arguments $args
* @param LoggerInterface $logger
* @param Profiler $profiler
* @param Response $response
* @param SystemMessages $sysMessages
* @param IManageConfigValues $config
* @param Emailer $emailer
* @param array $server
* @param array $parameters
*/
public function __construct(L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, SystemMessages $sysMessages, IManageConfigValues $config, Emailer $emailer, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->sysMessages = $sysMessages;
$this->config = $config;
$this->emailer = $emailer;
}
/**
* Handle POST requests for password reset form submission
*
* @param array $request
* @return void
*/
protected function post(array $request = [])
{
$loginame = trim($request['login-name'] ?? '');
if (!$loginame) {
$this->baseUrl->redirect();
}
$condition = ['(`email` = ? OR `nickname` = ?) AND `verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired`', $loginame, $loginame];
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'email', 'language'], $condition);
if (!DBA::isResult($user)) {
$this->sysMessages->addNotice($this->l10n->t('No valid account found.'));
$this->baseUrl->redirect();
}
$pwdreset_token = Strings::getRandomHex(32);
$fields = [
'pwdreset' => hash('sha256', $pwdreset_token),
'pwdreset_time' => DateTimeFormat::utcNow(),
];
$result = DBA::update('user', $fields, ['uid' => $user['uid']]);
if ($result) {
$this->sysMessages->addInfo($this->l10n->t('Password reset request issued. Check your email.'));
}
$sitename = $this->config->get('config', 'sitename');
$resetlink = $this->baseUrl . '/lostpass/' . $pwdreset_token;
$preamble = Strings::deindent($this->l10n->t('
Dear %1$s,
A request was recently received at "%2$s" to reset your account
password. In order to confirm this request, please select the verification link
below or paste it into your web browser address bar.
If you did NOT request this change, please DO NOT follow the link
provided and ignore and/or delete this email, the request will expire shortly.
Your password will not be changed unless we can verify that you
issued this request.', $user['username'], $sitename));
$body = Strings::deindent($this->l10n->t('
Follow this link soon to verify your identity:
%1$s
You will then receive a follow-up message containing the new password.
You may change that password from your account settings page after logging in.
The login details are as follows:
Site Location: %2$s
Login Name: %3$s', $resetlink, $this->baseUrl, $user['nickname']));
$email = $this->emailer->newSystemMail()
->withMessage($this->l10n->t('Password reset requested at %s', $sitename), $preamble, $body)
->forUser($user)
->withRecipient($user['email'])
->build();
$this->emailer->send($email);
$this->baseUrl->redirect();
}
/**
* Render page content
*
* @param array $request
* @return string Rendered page content
*/
protected function content(array $request = []): string
{
if (isset($this->parameters['token'])) {
$pwdreset_token = $this->parameters['token'];
$user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'email', 'pwdreset_time', 'language'], ['pwdreset' => hash('sha256', $pwdreset_token)]);
if (!DBA::isResult($user)) {
$this->sysMessages->addNotice($this->l10n->t("Request could not be verified. \x28You may have previously submitted it.\x29 Password reset failed."));
return $this->form();
}
// Password reset requests expire in 60 minutes
if ($user['pwdreset_time'] < DateTimeFormat::utc('now - 1 hour')) {
$fields = [
'pwdreset' => null,
'pwdreset_time' => null,
];
DBA::update('user', $fields, ['uid' => $user['uid']]);
$this->sysMessages->addNotice($this->l10n->t('Request has expired, please make a new one.'));
return $this->form();
}
return $this->generatePassword($user);
} else {
return $this->form();
}
}
/**
* Render the password reset form
*
* @return string Rendered form HTML
*/
private function form(): string
{
$tpl = Renderer::getMarkupTemplate('lostpass.tpl');
$o = Renderer::replaceMacros($tpl, [
'$title' => $this->l10n->t('Forgot your Password?'),
'$desc' => $this->l10n->t('Enter your email address and submit to have your password reset. Then check your email for further instructions.'),
'$name' => $this->l10n->t('Nickname or email'),
'$submit' => $this->l10n->t('Reset my password'),
]);
return $o;
}
/**
* Generate and send a new password to the user
*
* @param array $user User data array
* @return string Rendered success message HTML
*/
private function generatePassword(array $user): string
{
$o = '';
$new_password = User::generateNewPassword();
$result = User::updatePassword($user['uid'], $new_password);
if (DBA::isResult($result)) {
$tpl = Renderer::getMarkupTemplate('pwdreset.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$lbl1' => $this->l10n->t('Password Reset'),
'$lbl2' => $this->l10n->t('Your password has been reset as requested.'),
'$lbl3' => $this->l10n->t('Your new password is'),
'$lbl4' => $this->l10n->t('Save or copy your new password - and then'),
'$lbl5' => '<a href="' . $this->baseUrl . '">' . $this->l10n->t('click here to login') . '</a>.',
'$lbl6' => $this->l10n->t('Your password may be changed from the <em>Settings</em> page after successful login.'),
'$newpass' => $new_password,
]);
$this->sysMessages->addInfo($this->l10n->t("Your password has been reset."));
$sitename = $this->config->get('config', 'sitename');
$preamble = Strings::deindent($this->l10n->t('
Dear %1$s,
Your password has been changed as requested. Please retain this
information for your records ' . "\x28" . 'or change your password immediately to
something that you will remember' . "\x29" . '.
', $user['username']));
$body = Strings::deindent($this->l10n->t('
Your login details are as follows:
Site Location: %1$s
Login Name: %2$s
Password: %3$s
You may change that password from your account settings page after logging in.
', $this->baseUrl, $user['nickname'], $new_password));
$email = $this->emailer->newSystemMail()
->withMessage($this->l10n->t('Your password has been changed at %s', $sitename), $preamble, $body)
->forUser($user)
->withRecipient($user['email'])
->build();
$this->emailer->send($email);
}
return $o;
}
}

View file

@ -57,13 +57,20 @@ class Browser extends BaseModule
$tpl = Renderer::getMarkupTemplate('media/browser.tpl'); $tpl = Renderer::getMarkupTemplate('media/browser.tpl');
$output = Renderer::replaceMacros($tpl, [ $output = Renderer::replaceMacros($tpl, [
'$type' => 'attachment', '$type' => 'attachment',
'$path' => ['' => $this->t('Files')], '$path' => ['' => $this->t('Files')],
'$folders' => false, '$folders' => false,
'$files' => $fileArray, '$files' => $fileArray,
'$cancel' => $this->t('Cancel'), '$cancel' => $this->t('Cancel'),
'$nickname' => $this->session->getLocalUserNickname(), '$nickname' => $this->session->getLocalUserNickname(),
'$upload' => $this->t('Upload'), '$upload' => $this->t('Upload'),
'$photos_text' => $this->t('Photos'),
'$files_text' => $this->t('Files'),
'$aria_close' => $this->t('Close'),
'$aria_breadcrumb' => $this->t('Breadcrumb'),
'$aria_mode_switch' => $this->t('Switch between photo and attachment mode'),
'$aria_album_nav' => $this->t('Album navigation'),
'$aria_browser_content' => $this->t('Browser content'),
]); ]);
if (empty($request['mode'])) { if (empty($request['mode'])) {
@ -75,8 +82,8 @@ class Browser extends BaseModule
protected function map_files(array $record): array protected function map_files(array $record): array
{ {
list($m1, $m2) = explode('/', $record['filetype']); [$m1, $m2] = explode('/', $record['filetype']);
$filetype = file_exists(sprintf('images/icons/%s.png', $m1)) ? $m1 : 'text'; $filetype = file_exists(sprintf('images/icons/%s.png', $m1)) ? $m1 : 'text';
return [ return [
sprintf('%s/attach/%s', $this->baseUrl, $record['id']), sprintf('%s/attach/%s', $this->baseUrl, $record['id']),

View file

@ -69,13 +69,20 @@ class Browser extends BaseModule
$tpl = Renderer::getMarkupTemplate('media/browser.tpl'); $tpl = Renderer::getMarkupTemplate('media/browser.tpl');
$output = Renderer::replaceMacros($tpl, [ $output = Renderer::replaceMacros($tpl, [
'$type' => 'photo', '$type' => 'photo',
'$path' => $path, '$path' => $path,
'$folders' => $albums, '$folders' => $albums,
'$files' => $photosArray, '$files' => $photosArray,
'$cancel' => $this->t('Cancel'), '$cancel' => $this->t('Cancel'),
'$nickname' => $this->session->getLocalUserNickname(), '$nickname' => $this->session->getLocalUserNickname(),
'$upload' => $this->t('Upload'), '$upload' => $this->t('Upload'),
'$photos_text' => $this->t('Photos'),
'$files_text' => $this->t('Files'),
'$aria_close' => $this->t('Close'),
'$aria_breadcrumb' => $this->t('Breadcrumb'),
'$aria_mode_switch' => $this->t('Switch between photo and attachment mode'),
'$aria_album_nav' => $this->t('Album navigation'),
'$aria_browser_content' => $this->t('Browser content'),
]); ]);
if (empty($request['mode'])) { if (empty($request['mode'])) {
@ -99,7 +106,8 @@ class Browser extends BaseModule
Proxy::PIXEL_MEDIUM, Proxy::PIXEL_MEDIUM,
Proxy::PIXEL_MEDIUM Proxy::PIXEL_MEDIUM
], ],
['order' => ['scale']]); ['order' => ['scale']]
);
$scale = $photo['scale'] ?? $record['loq']; $scale = $photo['scale'] ?? $record['loq'];
return [ return [

View file

@ -32,10 +32,10 @@ use Psr\Log\LoggerInterface;
class Create extends BaseModule class Create extends BaseModule
{ {
const CONTACT_ACTION_NONE = 0; public const CONTACT_ACTION_NONE = 0;
const CONTACT_ACTION_COLLAPSE = 1; public const CONTACT_ACTION_COLLAPSE = 1;
const CONTACT_ACTION_IGNORE = 2; public const CONTACT_ACTION_IGNORE = 2;
const CONTACT_ACTION_BLOCK = 3; public const CONTACT_ACTION_BLOCK = 3;
/** @var SystemMessages */ /** @var SystemMessages */
private $systemMessages; private $systemMessages;
@ -99,7 +99,7 @@ class Create extends BaseModule
!empty($request['rule-ids']) ? explode(',', $request['rule-ids']) : [], !empty($request['rule-ids']) ? explode(',', $request['rule-ids']) : [],
$this->session->get('report_comment') ?? '', $this->session->get('report_comment') ?? '',
!empty($request['uri-ids']) ? explode(',', $request['uri-ids']) : [], !empty($request['uri-ids']) ? explode(',', $request['uri-ids']) : [],
(bool)($request['forward'] ?? false), (bool) ($request['forward'] ?? false),
); );
$this->repository->save($report); $this->repository->save($report);
@ -228,14 +228,14 @@ class Create extends BaseModule
DI::userSession()->getLocalUserId(), DI::userSession()->getLocalUserId(),
'system', 'system',
'itemspage_mobile_network', 'itemspage_mobile_network',
DI::config()->get('system', 'itemspage_network_mobile') DI::config()->get('system', 'itemspage_network_mobile'),
); );
} else { } else {
$itemsPerPage = DI::pConfig()->get( $itemsPerPage = DI::pConfig()->get(
DI::userSession()->getLocalUserId(), DI::userSession()->getLocalUserId(),
'system', 'system',
'itemspage_network', 'itemspage_network',
DI::config()->get('system', 'itemspage_network') DI::config()->get('system', 'itemspage_network'),
); );
} }
@ -273,7 +273,7 @@ class Create extends BaseModule
$tpl = Renderer::getMarkupTemplate('moderation/report/create/summary.tpl'); $tpl = Renderer::getMarkupTemplate('moderation/report/create/summary.tpl');
$forward_translation = $this->t('Would you like to forward this report to the remote server?'); $forward_translation = $this->t('Would you like to forward this report to the remote server?');
// @deprecated 2025.07 this translation is scheduled for removal as a new translation has been added without the typo // @deprecated 2026.01 this translation is scheduled for removal as a new translation has been added without the typo
$forward_translation = $this->t('Would you ike to forward this report to the remote server?'); $forward_translation = $this->t('Would you ike to forward this report to the remote server?');
return Renderer::replaceMacros($tpl, [ return Renderer::replaceMacros($tpl, [

View file

@ -61,12 +61,13 @@ class Summary extends BaseModeration
$t = Renderer::getMarkupTemplate('moderation/summary.tpl'); $t = Renderer::getMarkupTemplate('moderation/summary.tpl');
return Renderer::replaceMacros($t, [ return Renderer::replaceMacros($t, [
'$title' => $this->t('Moderation'), '$title' => $this->t('Moderation'),
'$page' => $this->t('Summary'), '$page' => $this->t('Summary'),
'$users' => [$this->t('Registered users'), $users], '$users' => [$this->t('Registered users'), $users],
'$accounts' => $accounts, '$accounts' => $accounts,
'$pending' => [$this->t('Pending registrations'), $pending], '$pending' => [$this->t('Pending registrations'), $pending],
'$warningtext' => [], '$warningtext' => [],
'$account_type_header' => $this->t('Registered accounts by type'),
]); ]);
} }
} }

View file

@ -44,32 +44,32 @@ class Notifications extends BaseNotifications
public function getNotifications() public function getNotifications()
{ {
$notificationHeader = ''; $notificationHeader = '';
$notifications = []; $notifications = [];
$factory = $this->formattedNotifyFactory; $factory = $this->formattedNotifyFactory;
if (($this->args->get(1) == 'network')) { if (($this->args->get(1) == 'network')) {
$notificationHeader = $this->t('Network Notifications'); $notificationHeader = $this->t('Network Notifications');
$notifications = [ $notifications = [
'ident' => FormattedNotify::NETWORK, 'ident' => FormattedNotify::NETWORK,
'notifications' => $factory->getNetworkList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE), 'notifications' => $factory->getNetworkList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
]; ];
} elseif (($this->args->get(1) == 'system')) { } elseif (($this->args->get(1) == 'system')) {
$notificationHeader = $this->t('System Notifications'); $notificationHeader = $this->t('System Notifications');
$notifications = [ $notifications = [
'ident' => FormattedNotify::SYSTEM, 'ident' => FormattedNotify::SYSTEM,
'notifications' => $factory->getSystemList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE), 'notifications' => $factory->getSystemList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
]; ];
} elseif (($this->args->get(1) == 'personal')) { } elseif (($this->args->get(1) == 'personal')) {
$notificationHeader = $this->t('Personal Notifications'); $notificationHeader = $this->t('Personal Notifications');
$notifications = [ $notifications = [
'ident' => FormattedNotify::PERSONAL, 'ident' => FormattedNotify::PERSONAL,
'notifications' => $factory->getPersonalList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE), 'notifications' => $factory->getPersonalList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
]; ];
} elseif (($this->args->get(1) == 'home')) { } elseif (($this->args->get(1) == 'home')) {
$notificationHeader = $this->t('Home Notifications'); $notificationHeader = $this->t('Home Notifications');
$notifications = [ $notifications = [
'ident' => FormattedNotify::HOME, 'ident' => FormattedNotify::HOME,
'notifications' => $factory->getHomeList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE), 'notifications' => $factory->getHomeList($this->showAll, $this->firstItemNum, self::ITEMS_PER_PAGE),
]; ];
} else { } else {
@ -97,7 +97,7 @@ class Notifications extends BaseNotifications
$notificationResult = $this->getNotifications(); $notificationResult = $this->getNotifications();
$notifications = $notificationResult['notifications'] ?? []; $notifications = $notificationResult['notifications'] ?? [];
$notificationHeader = $notificationResult['header'] ?? ''; $notificationHeader = $notificationResult['header'] ?? '';
if (!empty($notifications['notifications'])) { if (!empty($notifications['notifications'])) {
$notificationTemplates = [ $notificationTemplates = [
@ -127,10 +127,13 @@ class Notifications extends BaseNotifications
$notificationNoContent = $this->t('No more %s notifications.', $notificationResult['ident']); $notificationNoContent = $this->t('No more %s notifications.', $notificationResult['ident']);
} }
$notificationShowLink = [ $notificationShowLink = [];
'href' => ($this->showAll ? 'notifications/' . $notifications['ident'] : 'notifications/' . $notifications['ident'] . '?show=all'), if ($notifications['ident'] != "personal") {
'text' => ($this->showAll ? $this->t('Show unread') : $this->t('Show all')), $notificationShowLink = [
]; 'href' => ($this->showAll ? 'notifications/' . $notifications['ident'] : 'notifications/' . $notifications['ident'] . '?show=all'),
'text' => ($this->showAll ? $this->t('Show unread') : $this->t('Show all')),
];
}
return $this->printContent($notificationHeader, $notificationContent, $notificationNoContent, $notificationShowLink); return $this->printContent($notificationHeader, $notificationContent, $notificationNoContent, $notificationShowLink);
} }

View file

@ -92,7 +92,7 @@ class Ping extends BaseModule
$home_count = 0; $home_count = 0;
$register_count = 0; $register_count = 0;
$sysnotify_count = 0; $sysnotify_count = 0;
$circles_unseen = []; $circles_unseen = [];
$groups_unseen = []; $groups_unseen = [];
$event_count = 0; $event_count = 0;
@ -110,12 +110,12 @@ class Ping extends BaseModule
$condition = [ $condition = [
"`unseen` AND `uid` = ? AND NOT `origin` AND `wall` AND (`vid` != ? OR `vid` IS NULL)", "`unseen` AND `uid` = ? AND NOT `origin` AND `wall` AND (`vid` != ? OR `vid` IS NULL)",
$this->session->getLocalUserId(), Verb::getID(Activity::FOLLOW) $this->session->getLocalUserId(), Verb::getID(Activity::FOLLOW),
]; ];
$home_count = Post::count($condition); $home_count = Post::count($condition);
if ($this->config->get('system','compute_circle_counts')) { if ($this->config->get('system', 'compute_circle_counts')) {
// Find out how unseen network posts are spread across circles // Find out how unseen network posts are spread across circles
foreach (Circle::countUnseen($this->session->getLocalUserId()) as $circle_count) { foreach (Circle::countUnseen($this->session->getLocalUserId()) as $circle_count) {
if ($circle_count['count'] > 0) { if ($circle_count['count'] > 0) {
@ -138,16 +138,19 @@ class Ping extends BaseModule
$mail_count = $this->database->count('mail', ["`uid` = ? AND NOT `seen` AND `from-url` != ?", $this->session->getLocalUserId(), $myurl]); $mail_count = $this->database->count('mail', ["`uid` = ? AND NOT `seen` AND `from-url` != ?", $this->session->getLocalUserId(), $myurl]);
if (Register::getPolicy() === Register::APPROVE && $this->session->isSiteAdmin()) { if (Register::getPolicy() === Register::APPROVE && $this->session->isSiteAdmin()) {
$registrations = \Friendica\Model\Register::getPending(); $registrations = \Friendica\Model\Register::getPending();
$register_count = count($registrations); $register_count = count($registrations);
} }
$cachekey = 'ping:events:' . $this->session->getLocalUserId(); $cachekey = 'ping:events:' . $this->session->getLocalUserId();
$events = $this->cache->get($cachekey); $events = $this->cache->get($cachekey);
if (is_null($events)) { if (is_null($events)) {
$events = $this->database->selectToArray('event', ['type', 'start'], $events = $this->database->selectToArray(
'event',
['type', 'start'],
["`uid` = ? AND `start` < ? AND `finish` > ? AND NOT `ignore`", ["`uid` = ? AND `start` < ? AND `finish` > ? AND NOT `ignore`",
$this->session->getLocalUserId(), DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utcNow()]); $this->session->getLocalUserId(), DateTimeFormat::utc('now + 7 days'), DateTimeFormat::utcNow()],
);
$this->cache->set($cachekey, $events, Duration::HOUR); $this->cache->set($cachekey, $events, Duration::HOUR);
} }
@ -207,7 +210,7 @@ class Ping extends BaseModule
$registration['url'], $registration['url'],
$this->l10n->t('{0} requested registration'), $this->l10n->t('{0} requested registration'),
new \DateTime($registration['created'], new \DateTimeZone('UTC')), new \DateTime($registration['created'], new \DateTimeZone('UTC')),
new Uri($this->baseUrl . '/moderation/users/pending') new Uri($this->baseUrl . '/moderation/users/pending'),
); );
} }
} else { } else {
@ -216,7 +219,7 @@ class Ping extends BaseModule
$registrations[0]['url'], $registrations[0]['url'],
$this->l10n->t('{0} and %d others requested registration', count($registrations) - 1), $this->l10n->t('{0} and %d others requested registration', count($registrations) - 1),
new \DateTime($registrations[0]['created'], new \DateTimeZone('UTC')), new \DateTime($registrations[0]['created'], new \DateTimeZone('UTC')),
new Uri($this->baseUrl . '/moderation/users/pending') new Uri($this->baseUrl . '/moderation/users/pending'),
); );
} }
@ -227,11 +230,7 @@ class Ping extends BaseModule
// Unseen messages are kept at the top // Unseen messages are kept at the top
if ($a['seen'] == $b['seen']) { if ($a['seen'] == $b['seen']) {
if ($a['timestamp'] == $b['timestamp']) { return $b['timestamp'] <=> $a['timestamp'];
return 0;
} else {
return $a['timestamp'] < $b['timestamp'] ? 1 : -1;
}
} else { } else {
return $a['seen'] ? 1 : -1; return $a['seen'] ? 1 : -1;
} }

View file

@ -142,7 +142,7 @@ class Photo extends BaseApi
throw new HTTPException\NotFoundException(); throw new HTTPException\NotFoundException();
} }
$cacheable = ($photo['allow_cid'] . $photo['allow_gid'] . $photo['deny_cid'] . $photo['deny_gid'] === '') && (isset($photo['cacheable']) ? $photo['cacheable'] : true); $cacheable = ($photo['allow_cid'] . $photo['allow_gid'] . $photo['deny_cid'] . $photo['deny_gid'] === '') && ($photo['cacheable'] ?? true);
$stamp = microtime(true); $stamp = microtime(true);
$imgdata = ''; $imgdata = '';
@ -239,7 +239,7 @@ class Photo extends BaseApi
'scale' => $scale, 'resource' => $photo['resource-id'], 'scale' => $scale, 'resource' => $photo['resource-id'],
'total' => number_format($total, 3), 'fetch' => number_format($fetch, 3), 'total' => number_format($total, 3), 'fetch' => number_format($fetch, 3),
'data' => number_format($data, 3), 'checksum' => number_format($checksum, 3), 'data' => number_format($data, 3), 'checksum' => number_format($checksum, 3),
'output' => number_format($output, 3), 'rest' => number_format($rest, 3) 'output' => number_format($output, 3), 'rest' => number_format($rest, 3),
]); ]);
} }
@ -294,7 +294,7 @@ class Photo extends BaseApi
return MPhoto::getPhoto($matches[1], $matches[2], self::getCurrentUserID()); return MPhoto::getPhoto($matches[1], $matches[2], self::getCurrentUserID());
} }
return MPhoto::createPhotoForExternalResource($url, (int)DI::userSession()->getLocalUserId(), $media['mimetype'] ?? '', $media['blurhash'], $width, $height); return MPhoto::createPhotoForExternalResource($url, (int) DI::userSession()->getLocalUserId(), $media['mimetype'] ?? '', $media['blurhash'], $width, $height);
case 'media': case 'media':
$media = DBA::selectFirst('post-media', ['url', 'height', 'width', 'mimetype', 'uri-id', 'blurhash'], ['id' => $id, 'type' => Post\Media::IMAGE]); $media = DBA::selectFirst('post-media', ['url', 'height', 'width', 'mimetype', 'uri-id', 'blurhash'], ['id' => $id, 'type' => Post\Media::IMAGE]);
if (empty($media)) { if (empty($media)) {
@ -305,14 +305,14 @@ class Photo extends BaseApi
return MPhoto::getPhoto($matches[1], $matches[2], self::getCurrentUserID()); return MPhoto::getPhoto($matches[1], $matches[2], self::getCurrentUserID());
} }
return MPhoto::createPhotoForExternalResource($media['url'], (int)DI::userSession()->getLocalUserId(), $media['mimetype'], $media['blurhash'], $media['width'], $media['height']); return MPhoto::createPhotoForExternalResource($media['url'], (int) DI::userSession()->getLocalUserId(), $media['mimetype'], $media['blurhash'], $media['width'], $media['height']);
case 'link': case 'link':
$link = DBA::selectFirst('post-link', ['url', 'mimetype', 'blurhash', 'width', 'height'], ['id' => $id]); $link = DBA::selectFirst('post-link', ['url', 'mimetype', 'blurhash', 'width', 'height'], ['id' => $id]);
if (empty($link)) { if (empty($link)) {
return false; return false;
} }
return MPhoto::createPhotoForExternalResource($link['url'], (int)DI::userSession()->getLocalUserId(), $link['mimetype'] ?? '', $link['blurhash'] ?? '', $link['width'] ?? 0, $link['height'] ?? 0); return MPhoto::createPhotoForExternalResource($link['url'], (int) DI::userSession()->getLocalUserId(), $link['mimetype'] ?? '', $link['blurhash'] ?? '', $link['width'] ?? 0, $link['height'] ?? 0);
case 'contact': case 'contact':
$fields = ['uid', 'uri-id', 'url', 'nurl', 'avatar', 'photo', 'blurhash', 'xmpp', 'addr', 'network', 'failed', 'updated', 'next-update']; $fields = ['uid', 'uri-id', 'url', 'nurl', 'avatar', 'photo', 'blurhash', 'xmpp', 'addr', 'network', 'failed', 'updated', 'next-update'];
$contact = Contact::getById($id, $fields); $contact = Contact::getById($id, $fields);

View file

@ -79,8 +79,8 @@ class Edit extends BaseModule
} }
$fields = [ $fields = [
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'gravity', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'gravity', 'sensitive',
'body', 'title', 'uri-id', 'wall', 'post-type', 'guid' 'body', 'title', 'content-warning', 'uri-id', 'wall', 'post-type', 'guid',
]; ];
$item = Post::selectFirstForUser($this->session->getLocalUserId(), $fields, [ $item = Post::selectFirstForUser($this->session->getLocalUserId(), $fields, [
@ -153,6 +153,8 @@ class Edit extends BaseModule
'$public' => $this->t('Public post'), '$public' => $this->t('Public post'),
'$title' => $item['title'], '$title' => $item['title'],
'$placeholdertitle' => $this->t('Set title'), '$placeholdertitle' => $this->t('Set title'),
'$summary' => $item['content-warning'],
'$placeholdersummary' => (Feature::isEnabled($this->session->getLocalUserId(), Feature::SUMMARY) ? $this->t('Set summary, abstract or spoiler text') : ''),
'$category' => Post\Category::getCSVByURIId($item['uri-id'], $this->session->getLocalUserId(), Post\Category::CATEGORY), '$category' => Post\Category::getCSVByURIId($item['uri-id'], $this->session->getLocalUserId(), Post\Category::CATEGORY),
'$placeholdercategory' => (Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->t("Categories \x28comma-separated list\x29") : ''), '$placeholdercategory' => (Feature::isEnabled($this->session->getLocalUserId(), Feature::CATEGORIES) ? $this->t("Categories \x28comma-separated list\x29") : ''),
'$emtitle' => $this->t('Example: bob@example.com, mary@example.com'), '$emtitle' => $this->t('Example: bob@example.com, mary@example.com'),

View file

@ -47,7 +47,7 @@ class Media extends BaseProfile
) { ) {
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->appHelper = $appHelper; $this->appHelper = $appHelper;
$this->userSession = $userSession; $this->userSession = $userSession;
} }
@ -66,7 +66,7 @@ class Media extends BaseProfile
$o = self::getTabsHTML('media', $is_owner, $profile['nickname'], $profile['hide-friends']); $o = self::getTabsHTML('media', $is_owner, $profile['nickname'], $profile['hide-friends']);
$o .= Contact::getPostsFromUrl($profile['url'], $this->userSession->getLocalUserId(), true, $request['last_created'] ?? ''); $o .= Contact::getPostsFromUrl($profile['url'], $this->userSession->getLocalUserId(), true, $request);
return $o; return $o;
} }

View file

@ -0,0 +1,147 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\Profile;
use Friendica\Content\Conversation;
use Friendica\Content\Nav;
use Friendica\Content\Pager;
use Friendica\Core\ACL;
use Friendica\Core\L10n;
use Friendica\App\BaseURL;
use Friendica\App\Arguments;
use Friendica\App\Mode;
use Friendica\AppHelper;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues;
use Friendica\Core\Session\Model\UserSession;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Database\DBA;
use Friendica\Module\BaseProfile;
use Friendica\Network\HTTPException\ForbiddenException;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* Profile page for managing personal personal notes.
*
* Provides the UI to create, view and paginate personal notes associated
* with the currently authenticated local user.
*
* @package Friendica\Module\Profile
*/
class Notes extends BaseProfile
{
protected AppHelper $appHelper;
protected UserSession $userSession;
protected Mode $mode;
protected IManagePersonalConfigValues $pConfig;
protected IManageConfigValues $config;
protected Conversation $conversation;
/**
* Notes constructor.
*
* @param AppHelper $appHelper
* @param UserSession $userSession
* @param L10n $l10n
* @param BaseURL $baseUrl
* @param Arguments $args
* @param LoggerInterface $logger
* @param Profiler $profiler
* @param \Friendica\Module\Response $response
* @param array $server
* @param array $parameters
* @param Mode $mode
* @param IManagePersonalConfigValues $pConfig
* @param IManageConfigValues $config
* @param Conversation $conversation
*/
public function __construct(AppHelper $appHelper, UserSession $userSession, L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, \Friendica\Module\Response $response, array $server, array $parameters = [], Mode $mode, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, Conversation $conversation)
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->appHelper = $appHelper;
$this->userSession = $userSession;
$this->mode = $mode;
$this->pConfig = $pConfig;
$this->config = $config;
$this->conversation = $conversation;
}
/**
* Render the notes page content.
*
* @param array $request Optional request parameters
* @return string Rendered HTML for the notes page
* @throws ForbiddenException If no local user is logged in
*/
protected function content(array $request = []): string
{
if (!$this->userSession->getLocalUserId()) {
throw new ForbiddenException();
}
Nav::setSelected('home');
$contactId = $this->appHelper->getContactId();
$o = parent::getTabsHTML('notes', true, $this->userSession->getLocalUserNickname(), false);
$o .= '<h3>' . $this->l10n->t('Personal notes') . '</h3>';
$x = [
'lockstate' => 'lock',
'acl' => ACL::getSelfOnlyHTML($this->userSession->getLocalUserId(), $this->l10n->t('Personal notes are visible only by yourself.')),
'button' => $this->l10n->t('Save'),
'acl_data' => '',
];
$o .= $this->conversation->statusEditor($x, $contactId);
$condition = [
'uid' => $this->userSession->getLocalUserId(),
'post-type' => Item::PT_PERSONAL_NOTE,
'gravity' => Item::GRAVITY_PARENT,
'contact-id' => $contactId,
];
if ($this->mode->isMobile()) {
$itemsPerPage = $this->pConfig->get(
$this->userSession->getLocalUserId(),
'system',
'itemspage_mobile_network',
$this->config->get('system', 'itemspage_network_mobile'),
);
} else {
$itemsPerPage = $this->pConfig->get(
$this->userSession->getLocalUserId(),
'system',
'itemspage_network',
$this->config->get('system', 'itemspage_network'),
);
}
$pager = new Pager($this->l10n, $this->args->getQueryString(), $itemsPerPage);
$params = [
'order' => ['created' => true],
'limit' => [$pager->getStart(), $pager->getItemsPerPage()],
];
$r = Post::selectThreadForUser($this->userSession->getLocalUserId(), ['uri-id'], $condition, $params);
$count = 0;
if (DBA::isResult($r)) {
$notes = Post::toArray($r);
$count = count($notes);
$o .= $this->conversation->render($notes, Conversation::MODE_NOTES, false);
}
$o .= $pager->renderMinimal($count);
return $o;
}
}

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