Compare commits

...

170 commits

Author SHA1 Message Date
Jakobus Schürz
486b7caa46 add darkfeature "new posting" on rightclick and label on create button 2024-03-08 15:49:24 +01:00
Hypolite Petovan
013bba50bc
Merge pull request #13975 from annando/check-content-type
Check for the content type before fetching the content
2024-03-07 21:58:43 -05:00
Michael
5f0657a30c Don't show the body in the log 2024-03-07 22:29:04 +00:00
Michael
435b30be11 Check for the content type before fetching the content 2024-03-07 22:16:52 +00:00
Hypolite Petovan
73863561d2
Merge pull request #13974 from annando/videoheight
Set default value for max video height
2024-03-07 09:33:11 -05:00
Michael
67696d08da Set default value for max video height 2024-03-07 14:22:40 +00:00
Hypolite Petovan
1b00b91767
Merge pull request #13973 from annando/parent-activity
Change the last activity for delegation parents and siblings as well
2024-03-07 09:02:46 -05:00
Michael
68c2bdb98e Change the last activity for delegation parents and siblings as well 2024-03-07 06:12:36 +00:00
Michael Vogel
54852ecb56
Merge pull request #13970 from MrPetovan/bug/warnings
Address a couple of warnings
2024-03-06 20:13:40 +01:00
Hypolite Petovan
8c4b2107b5 Include author-alias to the field list in mod/photos
- It's used to generate magic links of authors of comments on photos
- Address https://github.com/friendica/friendica/issues/13761#issuecomment-1980738080
2024-03-06 12:07:43 -05:00
Hypolite Petovan
111df607bc Don't call mb_strlen() on $body if it isn't set in Model\Post\Counts
- Address https://github.com/friendica/friendica/issues/13761#issuecomment-1978354153
2024-03-06 12:01:25 -05:00
Tobias Diekershoff
b8b76e870d
Merge pull request #13967 from annando/diaspora-avatar
Issue 13939: Fix avatars for Diaspora
2024-03-06 06:55:39 +01:00
Michael
41c89abe68 Update routine added 2024-03-06 03:41:13 +00:00
Michael
24e7556f85 Transmit the user avatar path 2024-03-06 03:25:04 +00:00
Michael
8cc7bad1ea Issue 13939: Fix avatars for Diaspora 2024-03-06 03:00:09 +00:00
Hypolite Petovan
2357385162
Merge pull request #13966 from annando/max-video-height
Reduce the height of portrait videos
2024-03-05 19:01:48 -05:00
Michael
31b92b16ed Reduce the height of portrait videos 2024-03-05 21:25:00 +00:00
Hypolite Petovan
3ad4ab2940
Merge pull request #13965 from annando/last-activity
Improved assigning of "last-activity" and "login_date"
2024-03-05 10:38:41 -05:00
Michael Vogel
5ab81abaa6
Merge pull request #10 from MrPetovan/bug/last-activity-api-fixture-fix
Normalize API fixture data
2024-03-05 16:29:00 +01:00
Hypolite Petovan
1b651519a3 Normalize API fixture data
- Change public contact name to the related local user name
- Add location data to profile record that is used to update self and public contact during Auth
2024-03-05 10:10:15 -05:00
Michael
ba07172a65 Compare with the utc value 2024-03-05 14:24:40 +00:00
Michael
72e045e744 Improved assigning of "last-activity" and "login_date" 2024-03-05 14:06:26 +00:00
Michael Vogel
dc96a72173
Merge pull request #13962 from tobiasd/20240304-lng
translation updates
2024-03-04 16:49:13 +01:00
Tobias Diekershoff
38141edbea translation updates 2024-03-04 16:40:13 +01:00
Tobias Diekershoff
9b0e243350
Merge pull request #13961 from annando/issue-13765
Issue 13765: Fixed creation of self user contact for approval
2024-03-04 11:44:55 +01:00
Michael
52cc8ab73b Issue 13765: Fixed creation of self user contact for approval 2024-03-04 07:30:04 +00:00
Hypolite Petovan
ea4e66c74c
Merge pull request #13957 from annando/issue-13940
Issue 13940: handle posts that can't be found in contexts
2024-03-03 13:42:00 -05:00
Hypolite Petovan
f4826bae52
Merge pull request #13960 from annando/oembed-full-cleanup
Oembed: Some more cleanup
2024-03-03 13:41:35 -05:00
Michael
7471513269 Issue 13940: handle posts that can't be found in contexts 2024-03-03 18:32:26 +00:00
Michael
ae37c44cc0 Oembed: Some more cleanup 2024-03-03 18:06:25 +00:00
Hypolite Petovan
424e219c53
Merge pull request #13956 from annando/issue-13955
Issue 13955: Check for publish date upon receival
2024-03-03 12:23:13 -05:00
Michael
bae7644d6f Issue 13955: Check for publish date upon receival 2024-03-02 19:21:14 +00:00
Michael Vogel
f2ccce05b8
Merge pull request #13948 from MrPetovan/task/12420-frio-remove-legacy-scheme
[frio] Remove legacy schemes
2024-03-02 06:06:15 +01:00
Michael Vogel
89ffe6875f
Merge pull request #13942 from MrPetovan/bug/fix-api-fixture
Fix API fixture data
2024-03-02 05:48:19 +01:00
Hypolite Petovan
7284210bf7 Updated main translation file after changing some strings 2024-03-01 08:52:58 -05:00
Hypolite Petovan
4fcc92e532 [frio] Delete legacy scheme files 2024-03-01 08:48:41 -05:00
Hypolite Petovan
2c259c5c6f [frio] Remove legacy schemes
- [frio] Replace default scheme file by default scheme value
- [frio] Simplify frio theme settings
- [frio] Remove query string scheme setting
2024-03-01 08:48:38 -05:00
Hypolite Petovan
39d25b9699
Merge pull request #13954 from annando/issue-13953
Issue 13953: Fix warning during postupdate
2024-03-01 08:38:42 -05:00
Michael
5df1ead001 Issue 13953: Fix warning during postupdate 2024-03-01 08:41:12 +00:00
Hypolite Petovan
2d4f28dcde
Merge pull request #13951 from annando/issue-13949
Issue 13949: Block access via OAuth
2024-02-29 21:00:12 -05:00
Michael
dd55ba2d77 Issue 13949: Block access via OAuth 2024-02-29 22:03:57 +00:00
Hypolite Petovan
c9f7d9baff
Merge pull request #13946 from annando/issue-13819
Issue 13819: Ensure to not use OEmbed if not wanted
2024-02-29 07:54:43 -05:00
Hypolite Petovan
504a2e91e2
Merge pull request #13945 from annando/errors
Exceptions and warnings fixed
2024-02-29 07:53:13 -05:00
Michael
40e882004e Use the exact embed URLs 2024-02-29 07:40:36 +00:00
Michael
e394a6b0fa Issue 13819: Ensure to not use OEmbed if not wanted 2024-02-29 07:37:58 +00:00
Michael
8cf82a8449 Exceptions and warnings fixed 2024-02-29 04:40:04 +00:00
Hypolite Petovan
0d922b75af Use public contact ids where they should be used in API fixture data 2024-02-27 08:41:51 -05:00
Hypolite Petovan
ba0a8069c4 Normalize local node hostname across API fixtures
- This was causing the fixture data to be wrongly "repaired" in Model\User::getOwnerDataById because of a mismatch between the local base URL and the fixture-provided self contact URL
2024-02-27 08:41:51 -05:00
Hypolite Petovan
d37699bc08 Throw Not Found exception when $uid doesn't exist in Factory\Api\Twitter\User->createFromUserId
- Contact::getPublicIdByUserId() wrongly returns 0 when $uid doesn't exist, which is an existing albeit invalid record.
2024-02-27 08:41:51 -05:00
Hypolite Petovan
ac087749e3
Merge pull request #13938 from annando/output-type
Image handling: separate between output and input type, use Imagick on PNG
2024-02-25 10:01:34 -05:00
Michael
ddc9f5f595 Image handling: separate between outout and input type, use Imagick on PNG 2024-02-25 08:52:52 +00:00
Tobias Diekershoff
35bba685fa
Merge pull request #13936 from annando/rounding
Round the load to two digits
2024-02-24 18:56:11 +01:00
Michael
e52fa44d3f Round the load to two digits 2024-02-24 17:37:30 +00:00
Hypolite Petovan
f74d6f9ebb
Merge pull request #13932 from annando/oembed-cleanup
Unused OEmbed functionality is removed
2024-02-24 11:03:48 -05:00
Michael
ae358cae4c Updated messages.po 2024-02-24 15:29:33 +00:00
Michael
b572b8989f Use media link instead of proxy for pictures 2024-02-24 15:11:27 +00:00
Michael
5800a973cb Fixed positive list 2024-02-24 13:56:12 +00:00
Michael Vogel
44ce5471b3
Onepoll: Prevent errors with invalid mails (#13934) 2024-02-24 13:18:44 +01:00
Michael
e05b57cd5d messages.po updated 2024-02-24 11:56:55 +00:00
Michael
ecdf8f2b47 Merge remote-tracking branch 'upstream/2024.03-rc' into oembed-cleanup 2024-02-24 11:54:35 +00:00
Michael Vogel
1c5681c199
Merge pull request #13933 from annando/fix2
Accidentally merged changes are reverted
2024-02-24 12:40:19 +01:00
Michael
20fd25258a Accidentally changes are reverted 2024-02-24 11:35:32 +00:00
Michael
00bb538fd0 Merge branch '2024.03-rc' of https://github.com/friendica/friendica into 2024.03-rc 2024-02-24 11:01:44 +00:00
Michael
12bdbaaba8 OEmbed: Complete cleanup 2024-02-24 11:01:34 +00:00
Michael
821a135033 Unused OEmbed functionality is removed 2024-02-24 10:58:18 +00:00
Michael Vogel
0ff37c0075
Merge pull request #13931 from MrPetovan/bug/13930-photo-preview-sizes
Increase API photo preview size for Mastodon API to 640
2024-02-24 09:39:31 +01:00
Hypolite Petovan
0a73050de1 Increase API photo preview size for Mastodon API to 640 2024-02-23 22:41:21 -05:00
Hypolite Petovan
a25dbf839a Remove photo user id fallback from 2021
- Remove deprecated /photos/{nickname} fallback routes
- The contact id fallback is a lie, there's no replacement feature
2024-02-23 22:41:18 -05:00
Hypolite Petovan
e16b6ee6e1
Check form security token in /settings/userexport module (#13929)
* Escape HTML in the location field of a calendar event post

- This allowed script tags to be interpreted in the post display of an event.

* Add form security token check to /admin/phpinfo module

- This prevents basic XSS attacks against /admin/phpinfo

* Add form security token check to /babel module

- This prevents basic XSS attacks against /babel

* Prevent pass-through for attachments

- This addresses a straightforward Reflected XSS vulnerability if a malicious HTML/Javascript file is attached to a post through upload

* Prevent overwriting cid on event edit

- This allowed to share an event as any other user after zeroing the cid field of an existing event

* Check form security token in /settings/userexport module

- Prevents basic XSS attacks against /settings/userexport/*
2024-02-22 21:08:32 +01:00
Hypolite Petovan
5c5d7eb04f
Fix several vulnerabilities (#13927)
* Escape HTML in the location field of a calendar event post

- This allowed script tags to be interpreted in the post display of an event.

* Add form security token check to /admin/phpinfo module

- This prevents basic XSS attacks against /admin/phpinfo

* Add form security token check to /babel module

- This prevents basic XSS attacks against /babel

* Prevent pass-through for attachments

- This addresses a straightforward Reflected XSS vulnerability if a malicious HTML/Javascript file is attached to a post through upload

* Prevent overwriting cid on event edit

- This allowed to share an event as any other user after zeroing the cid field of an existing event
2024-02-22 06:53:52 +01:00
Michael Vogel
fc3898fe64
Updated Bluesky logo (#13926) 2024-02-21 18:23:36 +01:00
Michael Vogel
71384e6f39
Issue 13909: Filter channels by network (#13924) 2024-02-20 07:11:26 +01:00
Michael Vogel
d95c9d28a8
Issue 13922: "voted" must not be null (#13923) 2024-02-20 07:09:55 +01:00
Hypolite Petovan
bb7d25dfc9
Merge pull request #13921 from annando/content-type
Check for activity pub mime types
2024-02-19 05:57:47 -05:00
Michael Vogel
d5c0f086bd
Disallow mail addresses for registration (#13920)
* Disallow mail addresses for registration

* Order for allow/disallow has been changed
2024-02-19 09:33:20 +01:00
Michael
892e0a5623 Check for activity pub mime types 2024-02-19 07:11:56 +00:00
Michael Vogel
cb294cf411
Avoid problems with an empty domain in the blocklist (#13919)
* Avoid problems with an empty domain in the blocklist

* Test code removed
2024-02-19 07:22:19 +01:00
Michael Vogel
9ad452a19b
Merge pull request #13918 from MrPetovan/bug/fixup-13911
Move Api\Mastodon\Instance\Extended to ExtendedDescription
2024-02-19 04:05:42 +01:00
Hypolite Petovan
623a5be8a6 Clarify condition on offset in Mastodon\Search->searchStatuses 2024-02-18 18:48:37 -05:00
Hypolite Petovan
d1cd9a016e Move Api\Mastodon\Instance\Extended to ExtendedDescription
- Add reference to Mastodon documentation
2024-02-18 18:47:59 -05:00
Michael Vogel
7d5d3b3c29
Issue 13293: Endpoint /api/v1/accounts/lookup implemented (#13917) 2024-02-18 20:17:06 +01:00
Michael Vogel
bcec6c5ab2
Issue #13899: Fix error on postupdate (#13915) 2024-02-18 20:09:56 +01:00
Michael Vogel
6384265cbd
Issue #13823: Fix "Mutes" endpoint (#13916) 2024-02-18 20:07:51 +01:00
Michael Vogel
f12276eff8
New channel "quiet sharers" for posts from lesser frequent posters (#13913) 2024-02-18 15:54:21 +01:00
Michael Vogel
c6160a1c38
Fix API issues #13887, #13886, #13863, #13809, #13897 (#13911) 2024-02-18 15:52:30 +01:00
Michael Vogel
07c20da08f
Issue 13905: ostatus context added (#13912) 2024-02-18 15:46:41 +01:00
Michael Vogel
4eefd0a205
Merge pull request #13908 from MrPetovan/bug/warnings
Avoid passing null bytes in regular expression in Object\Image
2024-02-18 05:33:41 +01:00
Hypolite Petovan
78bc1359e0
Merge pull request #13907 from annando/fix-relations
Fix contact-relation follower calculation
2024-02-17 22:30:56 -05:00
Hypolite Petovan
1956c2ecfd Avoid passing null bytes in regular expression in Object\Image
- Remove capturing expression for A|B in favor of bracket syntax in regular expression since matches aren't used.
- Regular expressions have their own character escape notation including backslashes that need to be escaped in a PHP string.
- Actually address https://github.com/friendica/friendica/issues/13761#issuecomment-1949930922
2024-02-17 22:27:37 -05:00
Michael
ade2369b5d Merge remote-tracking branch 'upstream/2024.03-rc' into fix-relations 2024-02-17 21:56:56 +00:00
Michael
0d2ea97eb1 Fix comtact-relation follower calculation 2024-02-17 21:32:17 +00:00
Michael Vogel
08fa51d0bb
Fix the handling of unhandled image types and of animations (#13904)
* Fix the handling of unhandled image types and of animations

* Avoid warnings
2024-02-17 15:46:48 +01:00
Michael
7d10518e94 Revert "Fix unhandled image detection"
This reverts commit 1069cfb570.
2024-02-17 10:50:09 +00:00
Michael
1069cfb570 Fix unhandled image detection 2024-02-17 10:46:48 +00:00
Michael Vogel
14e5b06029
Image handling reworked, new image formats added (#13900)
* Image handling reworked, new image formats added

* Updated messages.po

* The dot is now part of the file extension

* Added WebP in install documentation

* Handle unhandled mime types

* Fixed animated picture detected
2024-02-17 07:45:41 +01:00
Tobias Diekershoff
1ea8a4042d bump version to 2024.03-rc 2024-02-14 08:24:41 +01:00
Michael Vogel
fad55e0948
Prevent users from following relay accounts (#13894) 2024-02-13 06:50:46 +01:00
Hypolite Petovan
262ca4131d
Merge pull request #13893 from annando/fix-relay-unsubscribe
Fixed relay detection on unsubscription
2024-02-12 23:55:01 -05:00
Michael
c7e0500529 Fixed relay detection on unsubscription 2024-02-13 04:30:38 +00:00
Hypolite Petovan
686d0b6dbb
Merge pull request #13892 from annando/no-preview-on-sensitive
Don't display preview images for links, when the post is marked as sensitive
2024-02-12 22:30:31 -05:00
Hypolite Petovan
59c27a6cbb
Merge pull request #13889 from annando/issue-13884
Issue 13884: Sanitation of links in BBCode parser
2024-02-12 15:28:04 -05:00
Michael
e2cbe0983a Don't display preview images for links, when the post is marked as sensitive 2024-02-12 06:01:07 +00:00
Michael
3b0cc45588 Link sanitation added to some more places 2024-02-12 05:40:09 +00:00
Michael
061f43788c Sanitize links before storing them 2024-02-12 05:21:13 +00:00
Michael
fe00a3893d urlencode for tags / fix smiley replacement 2024-02-12 04:46:20 +00:00
Michael
5d4f72698d Function renamed 2024-02-12 04:44:13 +00:00
Michael
96ede22abb Issue 13884: Sanitation of links in BBCode parser 2024-02-11 12:05:31 +00:00
Michael Vogel
2cc8fcc4aa
Merge pull request #13880 from MrPetovan/bug/13878-deprecate-star-list
Deprecate use of [*] BBCode tag for list items in favor of [li]
2024-02-11 03:13:28 +01:00
Hypolite Petovan
98900c33d4
Merge pull request #13881 from annando/valid-object
Ckeck for host differences of fetched activities
2024-02-10 11:13:16 -05:00
Michael
7dc9a812f6 Updated messages.po 2024-02-10 11:46:42 +00:00
Michael
7a14d5f7e4 Merge branch 'develop' of https://github.com/annando/friendica into develop 2024-02-10 11:39:47 +00:00
Michael
909d516ed4 Merge remote-tracking branch 'upstream/develop' into valid-object 2024-02-10 11:34:17 +00:00
Michael Vogel
52825cb4c4
User setting to disable blurring of sensitive pictures (#13883) 2024-02-10 09:50:49 +01:00
Michael Vogel
dbc72adaf9
Merge pull request #13882 from tobiasd/20240210-lng
update translations
2024-02-10 09:31:58 +01:00
Michael
ba5a288b2d User setting to disable blurring of sensitive pictures 2024-02-10 08:27:54 +00:00
Tobias Diekershoff
84043abbda update translations 2024-02-10 08:57:19 +01:00
Michael Vogel
f212888e90
Merge pull request #13879 from MrPetovan/bug/13877-fpostit-ssrf
Remove deprecated fpostit mod
2024-02-10 07:16:48 +01:00
Michael
50c0fd6738 Ckeck for host differences of fetched objects 2024-02-10 04:58:11 +00:00
Hypolite Petovan
5b5c9ddc74 Deprecate use of [*] BBCode tag for list items in favor of [li]
- It is conflicting with Markdown syntax
2024-02-09 20:33:42 -05:00
Hypolite Petovan
cb992f693c Remove deprecated fpostit mod
- This feature allowed unauthenticated requests to arbitrary domains.
2024-02-09 20:17:35 -05:00
Hypolite Petovan
ede41166ae
Merge pull request #13876 from annando/sensitive2
Sensitive previews are now blurred
2024-02-06 21:19:36 -05:00
Michael
caa7b6f326 "sensitive" is added to the API 2024-02-06 16:30:46 +00:00
Michael
0a6dff0618 Sensitive previews are now blurred 2024-02-06 16:15:58 +00:00
Hypolite Petovan
9b4ade4542
Merge pull request #13872 from friendica/issue-13845
Issue 13845: Support "sensitive" attribute
2024-02-06 05:54:26 -05:00
Michael
0153c2a027 Merge remote-tracking branch 'upstream/develop' into issue-13845 2024-02-06 09:47:38 +00:00
Hypolite Petovan
1a0f7c15ad
Merge pull request #13874 from annando/media-card-post
Two new search options "media:card" and "media:post"
2024-02-06 03:27:59 -05:00
Michael
d5bf306884 We now use xonstants 2024-02-06 06:34:16 +00:00
Michael
4cd2fde6f2 Two new search options "media:card" and "media:post" 2024-02-05 22:21:58 +00:00
Michael
259e7876ad Merge remote-tracking branch 'upstream/develop' into issue-13845 2024-02-05 22:17:43 +00:00
Hypolite Petovan
760c7deba3
Merge pull request #13873 from annando/libpng
Possible fixes "libpng warning: Interlace handling should be turned on when using png_read_image"
2024-02-05 15:20:23 -05:00
Michael
83306949ac Possible fixes "libpng warning: Interlace handling should be turned on when using png_read_image" 2024-02-05 18:16:47 +00:00
Michael
c0cd0dc74d "sensitive" added to fierld list 2024-02-05 12:21:57 +00:00
Michael
f7b0a0bef1 Merge remote-tracking branch 'upstream/develop' into issue-13845 2024-02-05 06:31:08 +00:00
Hypolite Petovan
a9d668cc78
Merge pull request #13871 from annando/channel-network
Issue 13844: User defined channels based on the network
2024-02-04 23:29:38 -05:00
Michael
15df9990da Issue 13845: Support "sensitive" attribute 2024-02-04 21:45:30 +00:00
Michael
91ddb406ab Merge remote-tracking branch 'upstream/develop' into channel-network 2024-02-04 16:36:25 +00:00
Hypolite Petovan
d9d42105d6
Merge pull request #13870 from annando/channel-languages
Fix: Saving of channel languages
2024-02-04 09:18:02 -05:00
Michael
7924085c94 Issue 13844: User defined channels based on the network 2024-02-04 07:14:57 +00:00
Michael
672186e549 Additional revert 2024-02-03 11:06:05 +00:00
Michael
053dfb3e2b Revert test changes / added saving of languages 2024-02-03 11:04:42 +00:00
Michael
f1efb8d277 Fix: Saving of channel languages 2024-02-03 11:01:17 +00:00
Hypolite Petovan
b6e958fdcf
Merge pull request #13869 from annando/engagement-searchindex
Unify searchindex table with engagement table
2024-02-02 12:18:11 -05:00
Michael
885b3a12b9 Search for tags and media in full text when doing full text searches 2024-02-02 16:05:24 +00:00
Michael
fc05daefb5 "media" is added to the search text 2024-02-02 10:46:20 +00:00
Michael
7faa42882b language field renamed to "language" 2024-02-02 07:05:39 +00:00
Michael
fc22a3e83f Unify searchindex table with engagement table 2024-02-01 23:08:53 +00:00
Hypolite Petovan
8ddc71188f
Merge pull request #13866 from annando/channel-reshare-privat
Channel relay reshares are now private follwers posts
2024-02-01 16:48:55 -05:00
Michael
b77a5c3eb4 Merge remote-tracking branch 'upstream/develop' into channel-reshare-privat 2024-02-01 19:41:35 +00:00
Michael Vogel
6a6e2cd2a2
Avoid duplicated post button on the contact conversation page (#13867)
* Avoid duplicated post button on the contact conversation page

* Updated messages.po
2024-02-01 19:47:43 +01:00
Michael
01c04fe2c2 messages.po updated 2024-02-01 16:10:14 +00:00
Michael
e60f3e1a99 Channel relay reshares are now private follwers posts 2024-02-01 15:59:04 +00:00
Michael Vogel
665316c14d
Issue 13859: Posts to a group in "Vier" is now possible (#13864) 2024-01-31 19:09:57 +01:00
Hypolite Petovan
fef14d96c7
Merge pull request #13861 from annando/size
Filter user defined channels by size
2024-01-30 17:52:40 -05:00
Michael
c8087a7827 Merge remote-tracking branch 'upstream/develop' into size 2024-01-30 15:24:38 +00:00
Michael Vogel
5a59dff817
Update doc/Channels.md
Co-authored-by: Hypolite Petovan <hypolite@mrpetovan.com>
2024-01-30 16:20:44 +01:00
Hypolite Petovan
e9554c32c9
Merge pull request #13862 from Raroun/fix_for_issue_13837_External_profile_image_URL_wrongly_redirect_to_default_-avatar_URL_for_all-digits_usernames
fix for issue #13837 External profile image URL wrongly redirect to default avatar URL for all-digits usernames
2024-01-30 09:43:58 -05:00
Raroun
d2f935df1d Updated messages.po 2024-01-30 15:32:27 +01:00
Raroun
ed30d888fa
Update src/Module/Register.php
Co-authored-by: Hypolite Petovan <hypolite@mrpetovan.com>
2024-01-30 15:28:49 +01:00
Raroun
606bd0be60
Check if nickname contains only US-ASCII and do not start with a digit
Update Register.php
2024-01-30 15:18:11 +01:00
Michael
d29d7c40cd Alternatives are added to the documentation 2024-01-30 11:55:36 +00:00
Michael
1e3cfca58d search term alternatives added 2024-01-30 11:14:41 +00:00
Michael
d6632bb0ea Updated messages.po 2024-01-30 10:14:03 +00:00
Michael
3fe4991fcf Filter user defined channels by size 2024-01-30 10:05:05 +00:00
Hypolite Petovan
0c583574e1
Merge pull request #13860 from annando/baseurl
Account type relay / fix missing baseurl for own contacts
2024-01-29 18:17:31 -05:00
Michael
7432e47f7a Fix code standards 2024-01-29 18:07:53 +00:00
Michael
cda1b91b77 Update searchindex on reshare 2024-01-29 12:32:21 +00:00
Michael
7c43b41f0b Searchtext functionality added 2024-01-29 11:02:13 +00:00
Michael
36313fe35b Relay data added to the search text as well 2024-01-29 11:00:39 +00:00
Michael
820674a7ad Use plural 2024-01-29 06:50:46 +00:00
Michael
9bd8d974b3 Account type relay / fix missing baseurl for own contacts 2024-01-29 06:28:43 +00:00
210 changed files with 28372 additions and 25499 deletions

View file

@ -1 +1 @@
2024.03-dev
2024.03-rc

View file

@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2024.03-dev (Yellow Archangel)
-- DB_UPDATE_VERSION 1548
-- Friendica 2024.03-rc (Yellow Archangel)
-- DB_UPDATE_VERSION 1556
-- ------------------------------------------
@ -502,6 +502,8 @@ CREATE TABLE IF NOT EXISTS `channel` (
`access-key` varchar(1) COMMENT 'Access key',
`include-tags` varchar(1023) COMMENT 'Comma separated list of tags that will be included in the channel',
`exclude-tags` varchar(1023) COMMENT 'Comma separated list of tags that aren\'t allowed in the channel',
`min-size` int unsigned COMMENT 'Minimum post size',
`max-size` int unsigned COMMENT 'Maximum post size',
`full-text-search` varchar(1023) COMMENT 'Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode',
`media-type` smallint unsigned COMMENT 'Filtered media types',
`languages` mediumtext COMMENT 'Desired languages',
@ -537,6 +539,7 @@ CREATE TABLE IF NOT EXISTS `contact-relation` (
`relation-score` smallint unsigned COMMENT 'score for interactions of relation-cid on cid',
`thread-score` smallint unsigned COMMENT 'score for interactions of cid on threads of relation-cid',
`relation-thread-score` smallint unsigned COMMENT 'score for interactions of relation-cid on threads of cid',
`post-score` smallint unsigned COMMENT 'score for the amount of posts from cid that can be seen by relation-cid',
PRIMARY KEY(`cid`,`relation-cid`),
INDEX `relation-cid` (`relation-cid`),
FOREIGN KEY (`cid`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
@ -1281,6 +1284,7 @@ CREATE TABLE IF NOT EXISTS `post-content` (
`location` varchar(255) NOT NULL DEFAULT '' COMMENT 'text location where this item originated',
`coord` varchar(255) NOT NULL DEFAULT '' COMMENT 'longitude/latitude pair representing location where this item originated',
`language` text COMMENT 'Language information about this post',
`sensitive` boolean COMMENT 'If true, this post contains sensitive content',
`app` varchar(255) NOT NULL DEFAULT '' COMMENT 'application which generated this item',
`rendered-hash` varchar(32) NOT NULL DEFAULT '' COMMENT '',
`rendered-html` mediumtext COMMENT 'item.body converted to html',
@ -1344,9 +1348,11 @@ CREATE TABLE IF NOT EXISTS `post-engagement` (
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`contact-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Person, organisation, news, community, relay',
`media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio',
`language` varchar(128) COMMENT 'Language information about this post',
`language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format',
`searchtext` mediumtext COMMENT 'Simplified text for the full text search',
`size` int unsigned COMMENT 'Body size',
`created` datetime COMMENT '',
`network` char(4) COMMENT '',
`restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network',
`comments` mediumint unsigned COMMENT 'Number of comments',
`activities` mediumint unsigned COMMENT 'Number of activities (like, dislike, ...)',
@ -1464,14 +1470,19 @@ CREATE TABLE IF NOT EXISTS `post-question-option` (
--
CREATE TABLE IF NOT EXISTS `post-searchindex` (
`uri-id` int unsigned NOT NULL COMMENT 'Id of the item-uri table entry that contains the item uri',
`network` char(4) COMMENT '',
`private` tinyint unsigned COMMENT '0=public, 1=private, 2=unlisted',
`owner-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Item owner',
`media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio',
`language` char(2) COMMENT 'Language information about this post in the ISO 639-1 format',
`searchtext` mediumtext COMMENT 'Simplified text for the full text search',
`size` int unsigned COMMENT 'Body size',
`created` datetime COMMENT '',
`restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network',
PRIMARY KEY(`uri-id`),
INDEX `owner-id` (`owner-id`),
INDEX `created` (`created`),
FULLTEXT INDEX `searchtext` (`searchtext`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Content for all posts';
--
@ -2006,7 +2017,8 @@ CREATE VIEW `application-view` AS SELECT
`application-token`.`follow` AS `follow`,
`application-token`.`push` AS `push`
FROM `application-token`
INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`;
INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`
INNER JOIN `user` ON `user`.`uid` = `application-token`.`uid` AND `user`.`verified` AND NOT `user`.`blocked` AND NOT `user`.`account_removed` AND NOT `user`.`account_expired`;
--
-- VIEW circle-member-view
@ -2098,6 +2110,38 @@ CREATE VIEW `post-timeline-view` AS SELECT
STRAIGHT_JOIN `contact` AS `owner` ON `owner`.`id` = `post-user`.`owner-id`
LEFT JOIN `contact` AS `causer` ON `causer`.`id` = `post-user`.`causer-id`;
--
-- VIEW post-searchindex-user-view
--
DROP VIEW IF EXISTS `post-searchindex-user-view`;
CREATE VIEW `post-searchindex-user-view` AS SELECT
`post-thread-user`.`uid` AS `uid`,
`post-searchindex`.`uri-id` AS `uri-id`,
`post-searchindex`.`owner-id` AS `owner-id`,
`post-searchindex`.`media-type` AS `media-type`,
`post-searchindex`.`language` AS `language`,
`post-searchindex`.`searchtext` AS `searchtext`,
`post-searchindex`.`size` AS `size`,
`post-thread-user`.`commented` AS `commented`,
`post-thread-user`.`received` AS `received`,
`post-thread-user`.`created` AS `created`,
`post-thread-user`.`network` AS `network`,
`post-searchindex`.`language` AS `restricted`,
0 AS `comments`,
0 AS `activities`
FROM `post-thread-user`
INNER JOIN `post-searchindex` ON `post-searchindex`.`uri-id` = `post-thread-user`.`uri-id`
INNER JOIN `post-user` ON `post-user`.`id` = `post-thread-user`.`post-user-id`
STRAIGHT_JOIN `contact` ON `contact`.`id` = `post-thread-user`.`contact-id`
STRAIGHT_JOIN `contact` AS `authorcontact` ON `authorcontact`.`id` = `post-thread-user`.`author-id`
STRAIGHT_JOIN `contact` AS `ownercontact` ON `ownercontact`.`id` = `post-thread-user`.`owner-id`
WHERE `post-user`.`visible` AND NOT `post-user`.`deleted`
AND (NOT `contact`.`readonly` AND NOT `contact`.`blocked` AND NOT `contact`.`pending`)
AND (`post-thread-user`.`hidden` IS NULL OR NOT `post-thread-user`.`hidden`)
AND NOT `authorcontact`.`blocked` AND NOT `ownercontact`.`blocked`
AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = `post-thread-user`.`uid` AND `cid` IN (`authorcontact`.`id`, `ownercontact`.`id`) AND (`blocked` OR `ignored`))
AND NOT EXISTS(SELECT `gsid` FROM `user-gserver` WHERE `uid` = `post-thread-user`.`uid` AND `gsid` IN (`authorcontact`.`gsid`, `ownercontact`.`gsid`) AND `ignored`);
--
-- VIEW post-user-view
--
@ -2155,6 +2199,7 @@ CREATE VIEW `post-user-view` AS SELECT
`post-content`.`plink` AS `plink`,
`post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`,
`post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`,
@ -2339,6 +2384,7 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`post-content`.`plink` AS `plink`,
`post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`,
`post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`,
@ -2509,6 +2555,7 @@ CREATE VIEW `post-view` AS SELECT
`post-content`.`plink` AS `plink`,
`post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`,
`post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`,
@ -2655,6 +2702,7 @@ CREATE VIEW `post-thread-view` AS SELECT
`post-content`.`plink` AS `plink`,
`post-content`.`location` AS `location`,
`post-content`.`coord` AS `coord`,
`post-content`.`sensitive` AS `sensitive`,
`post-content`.`app` AS `app`,
`post-content`.`object-type` AS `object-type`,
`post-content`.`object` AS `object`,

View file

@ -34,6 +34,7 @@ General
* y - for you
* f - followers
* r - sharers of sharers
* q - quiet sharers
* h - what's hot
* i - Images
* v - Videos

View file

@ -376,8 +376,8 @@ code</code></td>
&nbsp;&nbsp;[li] Second list element<br>
[/ul]<br>
[list]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listbullet" style="list-style-type: circle;">
@ -388,12 +388,12 @@ code</code></td>
</tr>
<tr>
<td>[ol]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/ol]<br>
[list=1]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listdecimal" style="list-style-type: decimal;">
@ -404,8 +404,8 @@ code</code></td>
</tr>
<tr>
<td>[list=]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listnone" style="list-style-type: none;">
@ -416,8 +416,8 @@ code</code></td>
</tr>
<tr>
<td>[list=i]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listlowerroman" style="list-style-type: lower-roman;">
@ -428,8 +428,8 @@ code</code></td>
</tr>
<tr>
<td>[list=I]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listupperroman" style="list-style-type: upper-roman;">
@ -440,8 +440,8 @@ code</code></td>
</tr>
<tr>
<td>[list=a]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listloweralpha" style="list-style-type: lower-alpha;">
@ -452,8 +452,8 @@ code</code></td>
</tr>
<tr>
<td>[list=A]<br>
&nbsp;&nbsp;[*] First list element<br>
&nbsp;&nbsp;[*] Second list element<br>
&nbsp;&nbsp;[li] First list element<br>
&nbsp;&nbsp;[li] Second list element<br>
[/list]</td>
<td>
<ul class="listupperalpha" style="list-style-type: upper-alpha;">

View file

@ -33,6 +33,7 @@ Predefined Channels
* Language: Posts in your language.
* Followers: Posts from your followers that you don't follow.
* Sharers of sharers: Posts from accounts that are followed by accounts that you follow.
* Quiet sharers: Posts from accounts that you follow but who don't post very often.
* Images: Posts with images.
* Audio: Posts with audio.
* Videos: Posts with videos.
@ -56,35 +57,43 @@ Each channel is defined by these values:
Additional keywords for the full text search
---
Additionally to the search for content, there are additional keywords that can be used in the full text search:
Additionally to the search for content, there are keywords that can be used in the full text search.
Alternatives are presented with "|".
* from - Use "from:nickname" or "from:nickname@domain.tld" to search for posts from a specific author.
* to - Use "from:nickname" or "from:nickname@domain.tld" to search for posts with the given contact as receiver.
* group - Use "from:nickname" or "from:nickname@domain.tld" to search for group post of the given group.
* group - Use "group:nickname" or "group:nickname@domain.tld" to search for group post of the given group.
* application | relay - Use "application:nickname" or "application:nickname@domain.tld" to search for posts that had been reshared by the given relay application.
* server - Use "server:hostname" to search for posts from a specific server. In the case of group postings, the search text contains both the hostname of the group server and the author's hostname.
* source - The ActivityPub type of the post source. Use this for example to include or exclude group posts or posts from services (aka bots).
* source:person - The post is created by a regular user account.
* source:organization - The post is created by an organisation.
* source:group - The post is created by or distributed via a group.
* source:service - The posts originates from a service account. This source type is often used to mark bot accounts.
* source:application - The post is created by an application. This is most likely unused in the fediverse for post creation.
* source:service | source:news - The posts originates from a service account. This source type is often used to mark bot accounts.
* source:application | source:relay - The post is created by an application. This is most likely unused in the fediverse for post creation.
* tag - Use "tag:tagname" to search for a specific tag.
* network - Use this to include or exclude some networks from your channel.
* network:apub - ActivityPub (Used by the systems in the Fediverse)
* network:dfrn - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub.
* network:dspr - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
* media - With this keyword you can search for attached media.
* media:image | media:photo | media:picture - The post contains an image
* media:video - The post contains a video
* media:audio - The post contains audio
* media:card - The post contains a link preview card
* media:post - The post links another post, means it is a quoted post
* network | net - Use this to include or exclude some networks from your channel.
* network:apub | network:activitypub - ActivityPub (Used by the systems in the Fediverse)
* network:dfrn | network:friendica - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub.
* network:dspr | network:diaspora - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails that had been imported via IMAP.
* network:stat - The OStatus protocol is mainly used by old GNU Social installations.
* network:dscs - Posts that are received by the Discourse connector.
* network:tmbl - Posts that are received by the Tumblr connector.
* network:bsky - Posts that are received by the Bluesky connector.
* network:stat | network:ostatus - The OStatus protocol is mainly used by old GNU Social installations.
* network:dscs | network:discourse - Posts that are received by the Discourse connector.
* network:tmbl | network:tumblr - Posts that are received by the Tumblr connector.
* network:bsky | network:bluesky - Posts that are received by the Bluesky connector.
* platform - Use this to include or exclude some platforms from your channel, e.g. "+platform:friendica". In the case of group postings, the search text contains both the platform of the group server and the author's platform.
* visibility - You have the choice between different visibilities. You can only see unlisted or private posts that you have the access for.
* visibility:public
* visibility:unlisted
* visibility:private
* language - Use "language:code" to search for posts with the given language in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
* language | lang - Use "language:code" to search for posts with the given language in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
Remember that you can combine these kerywords.
So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn"

View file

@ -44,7 +44,7 @@ For alternative server configurations (such as Nginx server and MariaDB database
### Optional
* PHP ImageMagick extension (php-imagick) for animated GIF support.
* PHP ImageMagick extension (php-imagick) for animated GIF and animated WebP support.
## Installation procedure

View file

@ -16,6 +16,8 @@ Fields
| access-key | Access key | varchar(1) | YES | | NULL | |
| include-tags | Comma separated list of tags that will be included in the channel | varchar(1023) | YES | | NULL | |
| exclude-tags | Comma separated list of tags that aren't allowed in the channel | varchar(1023) | YES | | NULL | |
| min-size | Minimum post size | int unsigned | YES | | NULL | |
| max-size | Maximum post size | int unsigned | YES | | NULL | |
| full-text-search | Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode | varchar(1023) | YES | | NULL | |
| media-type | Filtered media types | smallint unsigned | YES | | NULL | |
| languages | Desired languages | mediumtext | YES | | NULL | |

View file

@ -6,17 +6,18 @@ Contact relations
Fields
------
| Field | Description | Type | Null | Key | Default | Extra |
| --------------------- | -------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
| cid | contact the related contact had interacted with | int unsigned | NO | PRI | 0 | |
| relation-cid | related contact who had interacted with the contact | int unsigned | NO | PRI | 0 | |
| last-interaction | Date of the last interaction by relation-cid on cid | datetime | NO | | 0001-01-01 00:00:00 | |
| follow-updated | Date of the last update of the contact relationship | datetime | NO | | 0001-01-01 00:00:00 | |
| follows | if true, relation-cid follows cid | boolean | NO | | 0 | |
| score | score for interactions of cid on relation-cid | smallint unsigned | YES | | NULL | |
| relation-score | score for interactions of relation-cid on cid | smallint unsigned | YES | | NULL | |
| thread-score | score for interactions of cid on threads of relation-cid | smallint unsigned | YES | | NULL | |
| relation-thread-score | score for interactions of relation-cid on threads of cid | smallint unsigned | YES | | NULL | |
| Field | Description | Type | Null | Key | Default | Extra |
| --------------------- | ----------------------------------------------------------------------- | ----------------- | ---- | --- | ------------------- | ----- |
| cid | contact the related contact had interacted with | int unsigned | NO | PRI | 0 | |
| relation-cid | related contact who had interacted with the contact | int unsigned | NO | PRI | 0 | |
| last-interaction | Date of the last interaction by relation-cid on cid | datetime | NO | | 0001-01-01 00:00:00 | |
| follow-updated | Date of the last update of the contact relationship | datetime | NO | | 0001-01-01 00:00:00 | |
| follows | if true, relation-cid follows cid | boolean | NO | | 0 | |
| score | score for interactions of cid on relation-cid | smallint unsigned | YES | | NULL | |
| relation-score | score for interactions of relation-cid on cid | smallint unsigned | YES | | NULL | |
| thread-score | score for interactions of cid on threads of relation-cid | smallint unsigned | YES | | NULL | |
| relation-thread-score | score for interactions of relation-cid on threads of cid | smallint unsigned | YES | | NULL | |
| post-score | score for the amount of posts from cid that can be seen by relation-cid | smallint unsigned | YES | | NULL | |
Indexes
------------

View file

@ -17,6 +17,7 @@ Fields
| location | text location where this item originated | varchar(255) | NO | | | |
| coord | longitude/latitude pair representing location where this item originated | varchar(255) | NO | | | |
| language | Language information about this post | text | YES | | NULL | |
| sensitive | If true, this post contains sensitive content | boolean | YES | | NULL | |
| app | application which generated this item | varchar(255) | NO | | | |
| rendered-hash | | varchar(32) | NO | | | |
| rendered-html | item.body converted to html | mediumtext | YES | | NULL | |

View file

@ -12,9 +12,11 @@ Fields
| owner-id | Item owner | int unsigned | NO | | 0 | |
| contact-type | Person, organisation, news, community, relay | tinyint | NO | | 0 | |
| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | |
| language | Language information about this post | varchar(128) | YES | | NULL | |
| language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| size | Body size | int unsigned | YES | | NULL | |
| created | | datetime | YES | | NULL | |
| network | | char(4) | YES | | NULL | |
| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
| comments | Number of comments | mediumint unsigned | YES | | NULL | |
| activities | Number of activities (like, dislike, ...) | mediumint unsigned | YES | | NULL | |

View file

@ -6,13 +6,16 @@ Content for all posts
Fields
------
| Field | Description | Type | Null | Key | Default | Extra |
| ---------- | --------------------------------------------------------- | ---------------- | ---- | --- | ------- | ----- |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| network | | char(4) | YES | | NULL | |
| private | 0=public, 1=private, 2=unlisted | tinyint unsigned | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| created | | datetime | YES | | NULL | |
| Field | Description | Type | Null | Key | Default | Extra |
| ---------- | --------------------------------------------------------------------- | ------------ | ---- | --- | ------- | ----- |
| uri-id | Id of the item-uri table entry that contains the item uri | int unsigned | NO | PRI | NULL | |
| owner-id | Item owner | int unsigned | NO | | 0 | |
| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | |
| language | Language information about this post in the ISO 639-1 format | char(2) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| size | Body size | int unsigned | YES | | NULL | |
| created | | datetime | YES | | NULL | |
| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
Indexes
------------
@ -20,6 +23,7 @@ Indexes
| Name | Fields |
| ---------- | -------------------- |
| PRIMARY | uri-id |
| owner-id | owner-id |
| created | created |
| searchtext | FULLTEXT, searchtext |
@ -29,5 +33,6 @@ Foreign Keys
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| uri-id | [item-uri](help/database/db_item-uri) | id |
| owner-id | [contact](help/database/db_contact) | id |
Return to [database documentation](help/database)

View file

@ -356,8 +356,8 @@ Zeilen</code></td>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/ul]<br>
[list]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listbullet" style="list-style-type: circle;">
@ -368,12 +368,12 @@ Zeilen</code></td>
</tr>
<tr>
<td>[ol]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/ol]<br>
[list=1]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listdecimal" style="list-style-type: decimal;">
@ -384,8 +384,8 @@ Zeilen</code></td>
</tr>
<tr>
<td>[list=]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listnone" style="list-style-type: none;">
@ -396,8 +396,8 @@ Zeilen</code></td>
</tr>
<tr>
<td>[list=i]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listlowerroman" style="list-style-type: lower-roman;">
@ -408,8 +408,8 @@ Zeilen</code></td>
</tr>
<tr>
<td>[list=I]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listupperroman" style="list-style-type: upper-roman;">
@ -420,8 +420,8 @@ Zeilen</code></td>
</tr>
<tr>
<td>[list=a]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listloweralpha" style="list-style-type: lower-alpha;">
@ -432,8 +432,8 @@ Zeilen</code></td>
</tr>
<tr>
<td>[list=A]<br>
&nbsp;&nbsp;[*] Erstes Listenelement<br>
&nbsp;&nbsp;[*] Zweites Listenelement<br>
&nbsp;&nbsp;[li] Erstes Listenelement<br>
&nbsp;&nbsp;[li] Zweites Listenelement<br>
[/list]</td>
<td>
<ul class="listupperalpha" style="list-style-type: upper-alpha;">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -278,6 +278,7 @@ function item_process(array $post, array $request, bool $preview, string $return
$post['quote-uri-id'] = Item::getQuoteUriId($post['body'], $post['uid']);
$post['body'] = BBCode::removeSharedData(Item::setHashtags($post['body']));
$post['writable'] = true;
$post['sensitive'] = true;
$o = DI::conversation()->render([$post], Conversation::MODE_SEARCH, false, true);

View file

@ -132,8 +132,6 @@ function photos_post(App $a)
throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
}
$phototypes = Images::supportedTypes();
$can_post = false;
$visitor = 0;
@ -337,7 +335,7 @@ function photos_post(App $a)
if (DBA::isResult($photos)) {
$photo = $photos[0];
$ext = $phototypes[$photo['type']];
$ext = Images::getExtensionByMimeType($photo['type']);
Photo::update(
['desc' => $desc, 'album' => $albname, 'allow_cid' => $str_contact_allow, 'allow_gid' => $str_circle_allow, 'deny_cid' => $str_contact_deny, 'deny_gid' => $str_circle_deny],
['resource-id' => $resource_id, 'uid' => $page_owner_uid]
@ -590,8 +588,6 @@ function photos_content(App $a)
$profile = Profile::getByUID($user['uid']);
$phototypes = Images::supportedTypes();
$_SESSION['photo_return'] = DI::args()->getCommand();
// Parse arguments
@ -844,7 +840,7 @@ function photos_content(App $a)
foreach ($r as $rr) {
$twist = !$twist;
$ext = $phototypes[$rr['type']];
$ext = Images::getExtensionByMimeType($rr['type']);
$imgalt_e = $rr['filename'];
$desc_e = $rr['desc'];
@ -855,7 +851,7 @@ function photos_content(App $a)
'link' => 'photos/' . $user['nickname'] . '/image/' . $rr['resource-id']
. ($order_field === 'created' ? '?order=created' : ''),
'title' => DI::l10n()->t('View Photo'),
'src' => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . '.' . $ext,
'src' => 'photo/' . $rr['resource-id'] . '-' . $rr['scale'] . $ext,
'alt' => $imgalt_e,
'desc' => $desc_e,
'ext' => $ext,
@ -1013,9 +1009,9 @@ function photos_content(App $a)
}
$photo = [
'href' => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . '.' . $phototypes[$hires['type']],
'href' => 'photo/' . $hires['resource-id'] . '-' . $hires['scale'] . Images::getExtensionByMimeType($hires['type']),
'title' => DI::l10n()->t('View Full Size'),
'src' => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . '.' . $phototypes[$lores['type']] . '?_u=' . DateTimeFormat::utcNow('ymdhis'),
'src' => 'photo/' . $lores['resource-id'] . '-' . $lores['scale'] . Images::getExtensionByMimeType($lores['type']) . '?_u=' . DateTimeFormat::utcNow('ymdhis'),
'height' => $hires['height'],
'width' => $hires['width'],
'album' => $hires['album'],
@ -1043,7 +1039,7 @@ function photos_content(App $a)
$pager = new Pager(DI::l10n(), DI::args()->getQueryString());
$params = ['order' => ['id'], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
$items = Post::toArray(Post::selectForUser($link_item['uid'], Item::ITEM_FIELDLIST, $condition, $params));
$items = Post::toArray(Post::selectForUser($link_item['uid'], array_merge(Item::ITEM_FIELDLIST, ['author-alias']), $condition, $params));
if (DI::userSession()->getLocalUserId() == $link_item['uid']) {
Item::update(['unseen' => false], ['parent' => $link_item['parent']]);

View file

@ -1,8 +0,0 @@
fpostit
original author: Devlon Duthied
see his blog posting:
http://blog.duthied.com/2011/09/13/node-agnostic-friendika-bookmarklet/
original published at github https://github.com/duthied/Friendika-Bookmarklet

View file

@ -1,11 +0,0 @@
javascript: (function() {
the_url = 'http://testbubble.com/fpostit.php?url=' + encodeURIComponent(window.location.href) + '&title=' + encodeURIComponent(document.title) + '&text=' + encodeURIComponent('' (window.getSelection ? window.getSelection() : document.getSelection ? document.getSelection() : document.selection.createRange().text));
a_funct = function() {
if (!window.open(the_url, 'fpostit', 'location=yes,links=no,scrollbars=no,toolbar=no,width=600,height=300')) location.href = the_url;
};
if (/Firefox/.test(navigator.userAgent)) {
setTimeout(a_funct, 0)
} else {
a_funct()
}
})()

View file

@ -1,148 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
if (($_POST["friendica_acct_name"] != '') && ($_POST["friendica_password"] != '')) {
setcookie("username", $_POST["friendica_acct_name"], time()+60*60*24*300);
setcookie("password", $_POST["friendica_password"], time()+60*60*24*300);
}
?>
<html>
<head>
<style>
body {
font-family: arial, Helvetica,sans-serif;
margin: 0px;
}
.wrap1 {
padding: 2px 5px;
background-color: #729FCF;
margin-bottom: 10px;
}
.wrap2 {
margin-left: 10px;
font-size: 12px;
}
.logo {
margin-left: 3px;
margin-right: 5px;
float: left;
}
h2 {
color: #ffffff;
}
.error {
background-color: #FFFF66;
font-size: 12px;
margin-left: 10px;
}
</style>
</head>
<body>
<?php
if (isset($_GET['title'])) {
$title = $_GET['title'];
}
if (isset($_GET['text'])) {
$text = $_GET['text'];
}
if (isset($_GET['url'])) {
$url = $_GET['url'];
}
if ((isset($title)) && (isset($text)) && (isset($url))) {
$content = "$title\nsource:$url\n\n$text";
} else {
$content = $_POST['content'];
}
if (isset($_POST['submit'])) {
if (($_POST["friendica_acct_name"] != '') && ($_POST["friendica_password"] != '')) {
$acctname = $_POST["friendica_acct_name"];
$tmp_account_array = explode("@", $acctname);
if (isset($tmp_account_array[1])) {
$username = $tmp_account_array[0];
$hostname = $tmp_account_array[1];
}
$password = $_POST["friendica_password"];
$content = $_POST["content"];
$url = "http://" . $hostname . '/api/statuses/update';
$data = ['status' => $content];
// echo "posting to: $url<br/>";
$c = curl_init();
curl_setopt($c, CURLOPT_URL, $url);
curl_setopt($c, CURLOPT_USERPWD, "$username:$password");
curl_setopt($c, CURLOPT_POSTFIELDS, $data);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
curl_setopt($c, CURLOPT_FOLLOWLOCATION, true);
$c_result = curl_exec($c);
if(curl_errno($c)){
$error = curl_error($c);
showForm($error, $content);
}
curl_close($c);
if (!isset($error)) {
echo '<script language="javascript" type="text/javascript">window.close();</script>';
}
} else {
$error = "Missing account name and/or password...try again please";
showForm($error, $content);
}
} else {
showForm(null, $content);
}
function showForm($error, $content) {
$username_cookie = $_COOKIE['username'];
$password_cookie = $_COOKIE['password'];
echo <<<EOF
<div class='wrap1'>
<h2><img class='logo' width="32" height="32" src='friendica.svg' align='middle';/>
Friendica Bookmarklet</h2>
</div>
<div class="wrap2">
<form method="post" action="{$_SERVER['PHP_SELF']}">
Enter the email address of the Friendica Account that you want to cross-post to:(example: user@friendica.org)<br /><br />
Account ID: <input type="text" name="friendica_acct_name" value="{$username_cookie}" size="50"/><br />
Password: <input type="password" name="friendica_password" value="{$password_cookie}" size="50"/><br />
<textarea name="content" id="content" rows="6" cols="70">{$content}</textarea><br />
<input type="submit" value="PostIt!" name="submit" />&nbsp;&nbsp;<span class='error'>$error</span>
</form>
<p></p>
</div>
EOF;
}
?>
</body>
</html>

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 1920 1920"><rect width="1800" height="1800" x="60" y="60" fill="#1872a2" rx="333"/><path fill="#febf19" d="M40 371q0-136 98-234 98-97 234-97h1178q136 0 233 97 97 98 97 234v1178q0 136-97 234-97 97-233 97H372q-137 0-234-97-97-98-98-234Zm1510-258h-296v442H666v373l587-4 1 441H666v442h884q107 0 182-75 75-74 74-183V371q0-108-74-182-74-75-182-76z"/></svg>

Before

Width:  |  Height:  |  Size: 428 B

View file

@ -64,7 +64,7 @@ class App
{
const PLATFORM = 'Friendica';
const CODENAME = 'Yellow Archangel';
const VERSION = '2024.03-dev';
const VERSION = '2024.03-rc';
// Allow themes to control internal parameters
// by changing App values in theme.php

View file

@ -150,7 +150,7 @@ HELP;
if ($valid) {
$this->out('3', false);
$image = new Image($imgdata, Images::getMimeTypeByData($imgdata));
$image = new Image($imgdata);
if (!$image->isValid()) {
$this->out(' ' . $this->l10n->t('invalid image for id %s', $resourceid) . ' ', false);
$valid = false;

View file

@ -104,12 +104,17 @@ HELP;
$actor = $this->getArgument(1);
$apcontact = APContact::getByURL($actor);
if (empty($apcontact) || !in_array($apcontact['type'], ['Application', 'Service'])) {
$this->out($actor . ' is no relay actor');
if (empty($apcontact)) {
$this->out($actor . ' wasn\'t found');
return 1;
}
if ($mode == 'add') {
if (!APContact::isRelay($apcontact)) {
$this->out($actor . ' is no relay actor');
return 1;
}
if (Transmitter::sendRelayFollow($actor)) {
$this->out('Successfully added ' . $actor);
} else {

View file

@ -80,13 +80,18 @@ class Avatar
return $fields;
}
if (!$fetchResult->isSuccess()) {
Logger::debug('Fetching was unsuccessful', ['avatar' => $avatar]);
return $fields;
}
$img_str = $fetchResult->getBodyString();
if (empty($img_str)) {
Logger::debug('Avatar is invalid', ['avatar' => $avatar]);
return $fields;
}
$image = new Image($img_str, Images::getMimeTypeByData($img_str));
$image = new Image($img_str, $fetchResult->getContentType(), $avatar);
if (!$image->isValid()) {
Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]);
return $fields;
@ -145,7 +150,7 @@ class Avatar
return '';
}
$path = $filename . $size . '.' . $image->getExt();
$path = $filename . $size . $image->getExt();
$basepath = self::basePath();
if (empty($basepath)) {

View file

@ -362,6 +362,7 @@ class Conversation
$tpl = Renderer::getMarkupTemplate('jot.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$post' => $this->l10n->t($x['content'] ? 'Post to group' : 'Post'),
'$new_post' => $this->l10n->t('New Post'),
'$return_path' => $this->args->getQueryString(),
'$action' => 'item',

View file

@ -28,6 +28,7 @@ class Channel extends Timeline
const DISCOVER = 'discover';
const FOLLOWERS = 'followers';
const SHARERSOFSHARERS = 'sharersofsharers';
const QUIETSHARERS = 'quietsharers';
const IMAGE = 'image';
const VIDEO = 'video';
const AUDIO = 'audio';

View file

@ -30,6 +30,8 @@ namespace Friendica\Content\Conversation\Entity;
* @property-read int $uid User of the channel
* @property-read string $includeTags The tags to include in the channel
* @property-read string $excludeTags The tags to exclude in the channel
* @property-read int $minSize Minimum content size
* @property-read int $maxSize Maximum content size
* @property-read string $fullTextSearch full text search pattern
* @property-read int $mediaType Media types that are included in the channel
* @property-read array $languages Channel languages
@ -57,6 +59,10 @@ class Timeline extends \Friendica\BaseEntity
protected $includeTags;
/** @var string */
protected $excludeTags;
/** @var int */
protected $minSize;
/** @var int */
protected $maxSize;
/** @var string */
protected $fullTextSearch;
/** @var int */
@ -68,7 +74,7 @@ class Timeline extends \Friendica\BaseEntity
/** @var bool */
protected $valid;
public function __construct(string $code = null, string $label = null, string $description = null, string $accessKey = null, string $path = null, int $uid = null, string $includeTags = null, string $excludeTags = null, string $fullTextSearch = null, int $mediaType = null, int $circle = null, array $languages = null, bool $publish = null, bool $valid = null)
public function __construct(string $code = null, string $label = null, string $description = null, string $accessKey = null, string $path = null, int $uid = null, string $includeTags = null, string $excludeTags = null, string $fullTextSearch = null, int $mediaType = null, int $circle = null, array $languages = null, bool $publish = null, bool $valid = null, int $minSize = null, int $maxSize = null)
{
$this->code = $code;
$this->label = $label;
@ -78,6 +84,8 @@ class Timeline extends \Friendica\BaseEntity
$this->uid = $uid;
$this->includeTags = $includeTags;
$this->excludeTags = $excludeTags;
$this->minSize = $minSize;
$this->maxSize = $maxSize;
$this->fullTextSearch = $fullTextSearch;
$this->mediaType = $mediaType;
$this->circle = $circle;

View file

@ -45,6 +45,7 @@ final class Channel extends Timeline
new ChannelEntity(ChannelEntity::LANGUAGE, $native, $this->l10n->t('Posts in %s', $native), 'g'),
new ChannelEntity(ChannelEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'),
new ChannelEntity(ChannelEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'),
new ChannelEntity(ChannelEntity::QUIETSHARERS, $this->l10n->t('Quiet sharers'), $this->l10n->t('Posts from accounts that you follow but who don\'t post very often'), 'q'),
new ChannelEntity(ChannelEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'),
new ChannelEntity(ChannelEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'),
new ChannelEntity(ChannelEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'),
@ -55,6 +56,6 @@ final class Channel extends Timeline
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::DISCOVER, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE]);
return in_array($selectedTab, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::DISCOVER, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::QUIETSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE]);
}
}

View file

@ -52,6 +52,8 @@ final class UserDefinedChannel extends Timeline implements ICanCreateFromTableRo
$row['languages'] ?? null,
$row['publish'] ?? null,
$row['valid'] ?? null,
$row['min-size'] ?? null,
$row['max-size'] ?? null,
);
}
}

View file

@ -130,9 +130,11 @@ class UserDefinedChannel extends \Friendica\BaseRepository
'circle' => $Channel->circle,
'include-tags' => $Channel->includeTags,
'exclude-tags' => $Channel->excludeTags,
'min-size' => $Channel->minSize,
'max-size' => $Channel->maxSize,
'full-text-search' => $Channel->fullTextSearch,
'media-type' => $Channel->mediaType,
'languages' => serialize($Channel->languages),
'languages' => !empty($Channel->languages) ? serialize($Channel->languages) : null,
'publish' => $Channel->publish,
'valid' => $this->isValid($Channel->fullTextSearch),
];

View file

@ -695,7 +695,7 @@ class Item
$item['body'] = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']);
}
$shared_content = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], $item['plink'], $item['created'], $item['guid'], $item['uri']);
$shared_content = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], $item['plink'] ?? $item['uri'], $item['created'], $item['guid'], $item['uri']);
if (!empty($item['title'])) {
$shared_content .= '[h3]' . $item['title'] . "[/h3]\n";
@ -1105,4 +1105,35 @@ class Item
Tag::store($toUriId, $receiver['type'], $receiver['name'], $receiver['url']);
}
}
/**
* Check if the item is too old
*
* @param string $created
* @param integer $uid
* @return boolean item is too old
*/
public function isTooOld(string $created, int $uid = 0): bool
{
// check for create date and expire time
$expire_interval = DI::config()->get('system', 'dbclean-expire-days', 0);
if ($uid) {
$user = DBA::selectFirst('user', ['expire'], ['uid' => $uid]);
if (DBA::isResult($user) && ($user['expire'] > 0) && (($user['expire'] < $expire_interval) || ($expire_interval == 0))) {
$expire_interval = $user['expire'];
}
}
if (($expire_interval > 0) && !empty($created)) {
$expire_date = time() - ($expire_interval * 86400);
$created_date = strtotime($created);
if ($created_date < $expire_date) {
Logger::notice('Item created before expiration interval.', ['created' => date('c', $created_date), 'expired' => date('c', $expire_date)]);
return true;
}
}
return false;
}
}

View file

@ -22,10 +22,9 @@
namespace Friendica\Content;
use DOMDocument;
use DOMNode;
use DOMText;
use DOMXPath;
use Exception;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Hook;
use Friendica\Core\Renderer;
@ -49,32 +48,15 @@ use Friendica\Util\Strings;
*/
class OEmbed
{
/**
* Callback for fetching URL, checking allowance and returning formatted HTML
*
* @param array $matches
* @return string Formatted HTML
*/
public static function replaceCallback(array $matches): string
{
$embedurl = $matches[1];
$j = self::fetchURL($embedurl, !self::isAllowedURL($embedurl));
$s = self::formatObject($j);
return $s;
}
/**
* Get data from an URL to embed its content.
*
* @param string $embedurl The URL from which the data should be fetched.
* @param bool $no_rich_type If set to true rich type content won't be fetched.
* @param bool $use_parseurl Use the "ParseUrl" functionality to add additional data
* @param string $embedurl The URL from which the data should be fetched.
*
* @return \Friendica\Object\OEmbed
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function fetchURL(string $embedurl, bool $no_rich_type = false, bool $use_parseurl = true): \Friendica\Object\OEmbed
private static function fetchURL(string $embedurl): \Friendica\Object\OEmbed
{
$embedurl = trim($embedurl, '\'"');
@ -119,7 +101,7 @@ class OEmbed
$href = str_replace(['http://www.youtube.com/', 'http://player.vimeo.com/'],
['https://www.youtube.com/', 'https://player.vimeo.com/'], $href);
$result = DI::httpClient()->fetchFull($href . '&maxwidth=' . $a->getThemeInfoValue('videowidth'));
if ($result->getReturnCode() === 200) {
if ($result->isSuccess()) {
$json_string = $result->getBodyString();
break;
}
@ -157,57 +139,55 @@ class OEmbed
}
// Improve the OEmbed data with data from OpenGraph, Twitter cards and other sources
if ($use_parseurl) {
$data = ParseUrl::getSiteinfoCached($embedurl, false);
$data = ParseUrl::getSiteinfoCached($embedurl);
if (($oembed->type == 'error') && empty($data['title']) && empty($data['text'])) {
return $oembed;
}
if (($oembed->type == 'error') && empty($data['title']) && empty($data['text'])) {
return $oembed;
}
if ($no_rich_type || ($oembed->type == 'error')) {
$oembed->html = '';
$oembed->type = $data['type'];
if (!self::isAllowedURL($embedurl) || ($oembed->type == 'error')) {
$oembed->html = '';
$oembed->type = $data['type'];
if ($oembed->type == 'photo') {
if (!empty($data['images'])) {
$oembed->url = $data['images'][0]['src'];
$oembed->width = $data['images'][0]['width'];
$oembed->height = $data['images'][0]['height'];
} else {
$oembed->type = 'link';
}
if ($oembed->type == 'photo') {
if (!empty($data['images'])) {
$oembed->url = $data['images'][0]['src'];
$oembed->width = $data['images'][0]['width'];
$oembed->height = $data['images'][0]['height'];
} else {
$oembed->type = 'link';
}
}
}
if (!empty($data['title'])) {
$oembed->title = $data['title'];
}
if (!empty($data['title'])) {
$oembed->title = $data['title'];
}
if (!empty($data['text'])) {
$oembed->description = $data['text'];
}
if (!empty($data['text'])) {
$oembed->description = $data['text'];
}
if (!empty($data['publisher_name'])) {
$oembed->provider_name = $data['publisher_name'];
}
if (!empty($data['publisher_name'])) {
$oembed->provider_name = $data['publisher_name'];
}
if (!empty($data['publisher_url'])) {
$oembed->provider_url = $data['publisher_url'];
}
if (!empty($data['publisher_url'])) {
$oembed->provider_url = $data['publisher_url'];
}
if (!empty($data['author_name'])) {
$oembed->author_name = $data['author_name'];
}
if (!empty($data['author_name'])) {
$oembed->author_name = $data['author_name'];
}
if (!empty($data['author_url'])) {
$oembed->author_url = $data['author_url'];
}
if (!empty($data['author_url'])) {
$oembed->author_url = $data['author_url'];
}
if (!empty($data['images']) && ($oembed->type != 'photo')) {
$oembed->thumbnail_url = $data['images'][0]['src'];
$oembed->thumbnail_width = $data['images'][0]['width'];
$oembed->thumbnail_height = $data['images'][0]['height'];
}
if (!empty($data['images']) && ($oembed->type != 'photo')) {
$oembed->thumbnail_url = $data['images'][0]['src'];
$oembed->thumbnail_width = $data['images'][0]['width'];
$oembed->thumbnail_height = $data['images'][0]['height'];
}
Hook::callAll('oembed_fetch_url', $embedurl, $oembed);
@ -219,9 +199,10 @@ class OEmbed
* Returns a formatted string from OEmbed object
*
* @param \Friendica\Object\OEmbed $oembed
* @param int $uriid
* @return string
*/
private static function formatObject(\Friendica\Object\OEmbed $oembed): string
private static function formatObject(\Friendica\Object\OEmbed $oembed, int $uriid): string
{
$ret = '<div class="oembed ' . $oembed->type . '">';
@ -241,22 +222,22 @@ class OEmbed
'$escapedhtml' => base64_encode($oembed->html),
'$tw' => $tw,
'$th' => $th,
'$turl' => $oembed->thumbnail_url,
'$turl' => BBCode::proxyUrl($oembed->thumbnail_url, BBCode::INTERNAL, $uriid, Proxy::SIZE_SMALL),
]);
} else {
$ret = $oembed->html;
$ret .= Proxy::proxifyHtml($oembed->html, $uriid);
}
break;
case 'photo':
$ret .= '<img width="' . $oembed->width . '" src="' . Proxy::proxifyUrl($oembed->url) . '">';
$ret .= '<img width="' . $oembed->width . '" src="' . BBCode::proxyUrl($oembed->url, BBCode::INTERNAL, $uriid, Proxy::SIZE_MEDIUM) . '">';
break;
case 'link':
break;
case 'rich':
$ret .= Proxy::proxifyHtml($oembed->html);
$ret .= Proxy::proxifyHtml($oembed->html, $uriid);
break;
}
@ -294,12 +275,21 @@ class OEmbed
$ret .= '<a href="' . $oembed->embed_url . '" rel="oembed">' . $oembed->embed_url . '</a>';
}
$ret .= "</h4>";
if ($oembed->type == 'link') {
if (!empty($oembed->thumbnail_url)) {
$ret .= '<img width="' . $oembed->width . '" src="' . BBCode::proxyUrl($oembed->thumbnail_url, BBCode::INTERNAL, $uriid, Proxy::SIZE_MEDIUM) . '">';
}
if (!empty($oembed->description)) {
$ret .= '<p>' . $oembed->description . '</p>';
}
}
} elseif (!strpos($oembed->html, $oembed->embed_url)) {
// add <a> for html2bbcode conversion
$ret .= '<a href="' . $oembed->embed_url . '" rel="oembed">' . $oembed->title . '</a>';
}
$ret .= '</div>';
$test = Proxy::proxifyHtml($ret, $uriid);
return str_replace("\n", "", $ret);
}
@ -308,51 +298,19 @@ class OEmbed
* Converts BBCode to HTML code
*
* @param string $text
* @param int $uriid
* @return string
*/
public static function BBCode2HTML(string $text): string
public static function BBCode2HTML(string $text, int $uriid): string
{
if (DI::config()->get('system', 'no_oembed')) {
return preg_replace("/\[embed\](.+?)\[\/embed\]/is", "<!-- oembed $1 --><i>" . DI::l10n()->t('Embedding disabled') . " : $1</i><!-- /oembed $1 -->", $text);
}
return preg_replace_callback("/\[embed\](.+?)\[\/embed\]/is", [self::class, 'replaceCallback'], $text);
}
/**
* Find <span class='oembed'>..<a href='url' rel='oembed'>..</a></span>
* and replace it with [embed]url[/embed]
*
* @param string $text
* @return string
*/
public static function HTML2BBCode(string $text): string
{
// start parser only if 'oembed' is in text
if (strpos($text, 'oembed')) {
// convert non ascii chars to html entities
$html_text = mb_convert_encoding($text, 'HTML-ENTITIES', mb_detect_encoding($text));
// If it doesn't parse at all, just return the text.
$dom = new DOMDocument();
if (!@$dom->loadHTML($html_text)) {
return $text;
}
$xpath = new DOMXPath($dom);
$xattr = self::buildXPath('class', 'oembed');
$entries = $xpath->query("//div[$xattr]");
$xattr = "@rel='oembed'"; //oe_build_xpath("rel","oembed");
foreach ($entries as $e) {
$href = $xpath->evaluate("a[$xattr]/@href", $e)->item(0)->nodeValue;
if (!is_null($href)) {
$e->parentNode->replaceChild(new DOMText('[embed]' . $href . '[/embed]'), $e);
}
}
return self::getInnerHTML($dom->getElementsByTagName('body')->item(0));
} else {
if (!preg_match_all("/\[embed\](.+?)\[\/embed\]/is", $text, $matches, PREG_SET_ORDER)) {
return $text;
}
foreach ($matches as $match) {
$data = self::fetchURL($match[1]);
$text = str_replace($match[0], self::formatObject($data, $uriid), $text);
}
return $text;
}
/**
@ -373,26 +331,25 @@ class OEmbed
return false;
}
$str_allowed = DI::config()->get('system', 'allowed_oembed', '');
if (empty($str_allowed)) {
$allowed = DI::config()->get('system', 'allowed_oembed', '');
if (empty($allowed)) {
return false;
}
$allowed = explode(',', $str_allowed);
return Network::isDomainAllowed($domain, $allowed);
return Network::isDomainMatch($domain, explode(',', $allowed));
}
/**
* Returns a formatted HTML code from given URL and sets optional title
*
* @param string $url URL to fetch
* @param string $title Optional title (default: what comes from OEmbed object)
* @param string $title title (default: what comes from OEmbed object)
* @param int $uriid
* @return string Formatted HTML
*/
public static function getHTML(string $url, string $title = ''): string
public static function getHTML(string $url, string $title, int $uriid): string
{
$o = self::fetchURL($url, !self::isAllowedURL($url));
$o = self::fetchURL($url);
if (!is_object($o) || property_exists($o, 'type') && $o->type == 'error') {
throw new Exception('OEmbed failed for URL: ' . $url);
@ -402,74 +359,8 @@ class OEmbed
$o->title = $title;
}
$html = self::formatObject($o);
$html = self::formatObject($o, $uriid);
return $html;
}
/**
* Generates the iframe HTML for an oembed attachment.
*
* Width and height are given by the remote, and are regularly too small for
* the generated iframe.
*
* The width is entirely discarded for the actual width of the post, while fixed
* height is used as a starting point before the inevitable resizing.
*
* Since the iframe is automatically resized on load, there are no need for ugly
* and impractical scrollbars.
*
* @todo This function is currently unused until someone™ adds support for a separate OEmbed domain
*
* @param string $src Original remote URL to embed
* @param string $width
* @param string $height
* @return string Formatted HTML
*
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @see oembed_format_object()
*/
private static function iframe(string $src, string $width, string $height): string
{
if (!$height || strstr($height, '%')) {
$height = '200';
}
$width = '100%';
$src = DI::baseUrl() . '/oembed/' . Strings::base64UrlEncode($src);
return '<iframe onload="resizeIframe(this);" class="embed_rich" height="' . $height . '" width="' . $width . '" src="' . $src . '" allowfullscreen scrolling="no" frameborder="no">' . DI::l10n()->t('Embedded content') . '</iframe>';
}
/**
* Generates attribute search XPath string
*
* Generates an XPath query to select elements whose provided attribute contains
* the provided value in a space-separated list.
*
* @param string $attr Name of the attribute to search
* @param string $value Value to search in a space-separated list
* @return string
*/
private static function buildXPath(string $attr, $value): string
{
// https://www.westhoffswelt.de/blog/2009/6/9/select-html-elements-with-more-than-one-css-class-using-xpath
return "contains(normalize-space(@$attr), ' $value ') or substring(normalize-space(@$attr), 1, string-length('$value') + 1) = '$value ' or substring(normalize-space(@$attr), string-length(@$attr) - string-length('$value')) = ' $value' or @$attr = '$value'";
}
/**
* Returns the inner XML string of a provided DOMNode
*
* @param DOMNode $node
* @return string
*/
private static function getInnerHTML(DOMNode $node): string
{
$innerHTML = '';
$children = $node->childNodes;
foreach ($children as $child) {
$innerHTML .= $child->ownerDocument->saveXML($child);
}
return $innerHTML;
}
}

View file

@ -170,7 +170,7 @@ class PageInfo
foreach ($data['keywords'] as $keyword) {
/// @TODO make a positive list of allowed characters
$hashtag = str_replace([' ', '+', '/', '.', '#', '@', "'", '"', '', '`', '(', ')', '„', '“'], '', $keyword);
$hashtags .= '#[url=' . DI::baseUrl() . '/search?tag=' . $hashtag . ']' . $hashtag . '[/url] ';
$hashtags .= '#[url=' . DI::baseUrl() . '/search?tag=' . urlencode($hashtag) . ']' . $hashtag . '[/url] ';
}
}

View file

@ -177,15 +177,20 @@ class PostMedia extends BaseEntity
/**
* Get preview path for given media id relative to the base URL
*
* @param string $size One of the Proxy::SIZE_* constants
* @param string $size One of the Proxy::SIZE_* constants
* @param bool $vlurred If "true", the preview will be blurred
* @return string preview link
*/
public function getPreviewPath(string $size = ''): string
public function getPreviewPath(string $size = '', bool $blurred = false): string
{
return '/photo/preview/' .
$path = '/photo/preview/' .
(Proxy::getPixelsFromSize($size) ? Proxy::getPixelsFromSize($size) . '/' : '') .
$this->id;
if ($blurred) {
$path .= '?' . http_build_query(['blur' => true]);
}
return $path;
}
/**

View file

@ -40,7 +40,9 @@ use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Util\Images;
use Friendica\Util\Map;
use Friendica\Util\Network;
use Friendica\Util\ParseUrl;
use Friendica\Util\Proxy;
use Friendica\Util\Strings;
@ -124,7 +126,7 @@ class BBCode
break;
case 'publisher_url':
$data['provider_url'] = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
$data['provider_url'] = Network::sanitizeUrl(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
break;
case 'author_name':
@ -135,7 +137,7 @@ class BBCode
break;
case 'author_url':
$data['author_url'] = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
$data['author_url'] = Network::sanitizeUrl(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
if ($data['provider_url'] == $data['author_url']) {
$data['author_url'] = '';
}
@ -308,7 +310,7 @@ class BBCode
return trim($text);
}
private static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string
public static function proxyUrl(string $image, int $simplehtml = self::INTERNAL, int $uriid = 0, string $size = ''): string
{
// Only send proxied pictures to API and for internal display
if (!in_array($simplehtml, [self::INTERNAL, self::MASTODON_API, self::TWITTER_API])) {
@ -434,6 +436,8 @@ class BBCode
return $text;
}
$data['url'] = Network::sanitizeUrl($data['url']);
if (isset($data['title'])) {
$data['title'] = strip_tags($data['title']);
$data['title'] = str_replace(['http://', 'https://'], '', $data['title']);
@ -449,7 +453,7 @@ class BBCode
$return = '';
try {
if ($tryoembed && OEmbed::isAllowedURL($data['url'])) {
$return = OEmbed::getHTML($data['url'], $data['title']);
$return = OEmbed::getHTML($data['url'], $data['title'], $uriid);
} else {
throw new Exception('OEmbed is disabled for this attachment.');
}
@ -485,6 +489,7 @@ class BBCode
}
if (!empty($data['provider_url']) && !empty($data['provider_name'])) {
$data['provider_url'] = Network::sanitizeUrl($data['provider_url']);
if (!empty($data['author_name'])) {
$return .= sprintf('<sup><a href="%s" target="_blank" rel="noopener noreferrer">%s (%s)</a></sup>', $data['provider_url'], $data['author_name'], $data['provider_name']);
} else {
@ -1023,12 +1028,12 @@ class BBCode
if (is_null($text)) {
$curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]);
if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getHeader('Content-Type')[0] ?? '';
$mimetype = $curlResult->getContentType() ?? '';
} else {
$mimetype = '';
}
if (substr($mimetype, 0, 6) == 'image/') {
if (Images::isSupportedMimeType($mimetype)) {
$text = '[url=' . $match[1] . ']' . $match[1] . '[/url]';
} else {
$text = '[url=' . $match[2] . ']' . $match[2] . '[/url]';
@ -1064,6 +1069,21 @@ class BBCode
return $text;
}
/**
* Callback: Sanitize links from given $match array
*
* @param array $match Array with link match
* @return string BBCode
*/
private static function sanitizeLinksCallback(array $match): string
{
if (count($match) == 3) {
return '[' . $match[1] . ']' . Network::sanitizeUrl($match[2]) . '[/' . $match[1] . ']';
} else {
return '[' . $match[1] . '=' . Network::sanitizeUrl($match[2]) . ']' . $match[3] . '[/' . $match[1] . ']';
}
}
/**
* Callback: Expands links from given $match array
*
@ -1106,13 +1126,13 @@ class BBCode
$curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]);
if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getHeader('Content-Type')[0] ?? '';
$mimetype = $curlResult->getContentType() ?? '';
} else {
$mimetype = '';
}
// if its a link to a picture then embed this picture
if (substr($mimetype, 0, 6) == 'image/') {
if (Images::isSupportedMimeType($mimetype)) {
$text = '[img]' . $match[1] . '[/img]';
} else {
if (!empty($match[3])) {
@ -1338,12 +1358,12 @@ class BBCode
* $match[1] = $url
* $match[2] = $title or absent
*/
$try_oembed_callback = function (array $match) {
$try_oembed_callback = function (array $match) use ($uriid) {
$url = $match[1];
$title = $match[2] ?? '';
try {
$return = OEmbed::getHTML($url, $title);
$return = OEmbed::getHTML($url, $title, $uriid);
} catch (Exception $ex) {
$return = $match[0];
}
@ -1455,7 +1475,7 @@ class BBCode
// Replace non graphical smilies for external posts
if (!$nosmile) {
$text = self::performWithEscapedTags($text, ['img'], function ($text) use ($simple_html, $for_plaintext) {
$text = self::performWithEscapedTags($text, ['url', 'img', 'audio', 'video', 'youtube', 'vimeo', 'share', 'attachment', 'iframe', 'bookmark'], function ($text) use ($simple_html, $for_plaintext) {
return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext);
});
}
@ -1549,9 +1569,6 @@ class BBCode
// Check for centered text
$text = preg_replace("(\[center\](.*?)\[\/center\])ism", '<div style="text-align:center;">$1</div>', $text);
// Check for list text
$text = str_replace("[*]", "<li>", $text);
// Check for block-level custom CSS
$text = preg_replace('#(?<=^|\n)\[style=(.*?)](.*?)\[/style](?:\n|$)#ism', '<div style="$1">$2</div>', $text);
@ -1591,6 +1608,10 @@ class BBCode
$text = preg_replace("/\[li\](.*?)\[\/li\]/ism", '<li>$1</li>', $text);
}
// Check for list text
$text = str_replace("[*]", "<li>", $text);
$text = str_replace("[li]", "<li>", $text);
$text = preg_replace("/\[th\](.*?)\[\/th\]/sm", '<th>$1</th>', $text);
$text = preg_replace("/\[td\](.*?)\[\/td\]/sm", '<td>$1</td>', $text);
$text = preg_replace("/\[tr\](.*?)\[\/tr\]/sm", '<tr>$1</tr>', $text);
@ -1716,6 +1737,9 @@ class BBCode
// Simplify "video" element
$text = preg_replace('(\[video[^\]]*?\ssrc\s?=\s?([^\s\]]+)[^\]]*?\].*?\[/video\])ism', '[video]$1[/video]', $text);
$text = preg_replace_callback("/\[(video)\](.*?)\[\/video\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace_callback("/\[(audio)\](.*?)\[\/audio\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
if ($simple_html == self::NPF) {
$text = preg_replace(
"/\[video\](.*?)\[\/video\]/ism",
@ -1758,12 +1782,13 @@ class BBCode
}
// Backward compatibility, [iframe] support has been removed in version 2020.12
$text = preg_replace_callback("/\[(iframe)\](.*?)\[\/iframe\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace("/\[iframe\](.*?)\[\/iframe\]/ism", '<a href="$1">$1</a>', $text);
$text = self::normalizeVideoLinks($text);
// Youtube extensions
if ($try_oembed) {
if ($try_oembed && OEmbed::isAllowedURL('https://www.youtube.com/embed/')) {
$text = preg_replace("/\[youtube\]([A-Za-z0-9\-_=]+)(.*?)\[\/youtube\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://www.youtube.com/embed/$1" frameborder="0" ></iframe>', $text);
} else {
$text = preg_replace(
@ -1774,7 +1799,7 @@ class BBCode
}
// Vimeo extensions
if ($try_oembed) {
if ($try_oembed && OEmbed::isAllowedURL('https://player.vimeo.com/video')) {
$text = preg_replace("/\[vimeo\]([0-9]+)(.*?)\[\/vimeo\]/ism", '<iframe width="' . $a->getThemeInfoValue('videowidth') . '" height="' . $a->getThemeInfoValue('videoheight') . '" src="https://player.vimeo.com/video/$1" frameborder="0" ></iframe>', $text);
} else {
$text = preg_replace(
@ -1785,7 +1810,7 @@ class BBCode
}
// oembed tag
$text = OEmbed::BBCode2HTML($text);
$text = OEmbed::BBCode2HTML($text, $uriid);
// Avoid triple linefeeds through oembed
$text = str_replace("<br style='clear:left'></span><br><br>", "<br style='clear:left'></span><br>", $text);
@ -1810,6 +1835,9 @@ class BBCode
$text = '<span style="font-size: xx-large; line-height: normal;">' . $text . '</span>';
}
$text = preg_replace_callback("/\[(url)\](.*?)\[\/url\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace_callback("/\[(url)\=(.*?)\](.*?)\[\/url\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
// Handle mentions and hashtag links
if ($simple_html == self::DIASPORA) {
// The ! is converted to @ since Diaspora only understands the @
@ -1912,11 +1940,11 @@ class BBCode
self::performWithEscapedTags($text, ['url', 'share'], function ($text) use ($simple_html) {
$text = preg_replace_callback("/(?:#\[url\=[^\[\]]*\]|\[url\=[^\[\]]*\]#)(.*?)\[\/url\]/ism", function ($matches) use ($simple_html) {
if ($simple_html == self::ACTIVITYPUB) {
return '<a href="' . DI::baseUrl() . '/search?tag=' . rawurlencode($matches[1])
return '<a href="' . DI::baseUrl() . '/search?tag=' . urlencode($matches[1])
. '" data-tag="' . XML::escape($matches[1]) . '" rel="tag ugc">#'
. XML::escape($matches[1]) . '</a>';
} else {
return '#<a href="' . DI::baseUrl() . '/search?tag=' . rawurlencode($matches[1])
return '#<a href="' . DI::baseUrl() . '/search?tag=' . urlencode($matches[1])
. '" class="tag" rel="tag" title="' . XML::escape($matches[1]) . '">'
. XML::escape($matches[1]) . '</a>';
}
@ -1943,6 +1971,7 @@ class BBCode
$text = preg_replace('/acct:([^@]+)@((?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63})/', '<a href="' . DI::baseUrl() . '/acctlink?addr=$1@$2" target="extlink">acct:$1@$2</a>', $text);
// Perform MAIL Search
$text = preg_replace_callback("/\[(mail)\](.*?)\[\/mail\]/ism", [self::class, 'sanitizeLinksCallback'], $text);
$text = preg_replace("/\[mail\](.*?)\[\/mail\]/", '<a href="mailto:$1">$1</a>', $text);
$text = preg_replace("/\[mail\=(.*?)\](.*?)\[\/mail\]/", '<a href="mailto:$1">$2</a>', $text);
@ -2028,13 +2057,7 @@ class BBCode
);
// Default iframe allowed domains/path
$allowedIframeDomains = [
DI::baseUrl()->getHost()
. (DI::baseUrl()->getPath() ? '/' . DI::baseUrl()->getPath() : '')
. '/oembed/', # The path part has to change with the source in Content\Oembed::iframe
'www.youtube.com/embed/',
'player.vimeo.com/video/',
];
$allowedIframeDomains = DI::config()->get('system', 'no_oembed_rich_content') ? [] : ['www.youtube.com/embed/', 'player.vimeo.com/video/'];
$allowedIframeDomains = array_merge(
$allowedIframeDomains,
@ -2303,7 +2326,7 @@ class BBCode
case '#':
default:
return $match[1] . '[url=' . DI::baseUrl() . '/search?tag=' . $match[2] . ']' . $match[2] . '[/url]';
return $match[1] . '[url=' . DI::baseUrl() . '/search?tag=' . urlencode($match[2]) . ']' . $match[2] . '[/url]';
}
},
$body

View file

@ -103,6 +103,7 @@ class Widget
{
// Always hide content from these networks
$networks = [Protocol::PHANTOM, Protocol::FACEBOOK, Protocol::APPNET, Protocol::TWITTER, Protocol::ZOT];
Addon::loadAddons();
if (!Addon::isEnabled("discourse")) {
$networks[] = Protocol::DISCOURSE;
@ -535,6 +536,7 @@ class Widget
['ref' => 'organisation', 'name' => DI::l10n()->t('Organisations')],
['ref' => 'news', 'name' => DI::l10n()->t('News')],
['ref' => 'community', 'name' => DI::l10n()->t('Groups')],
['ref' => 'relay', 'name' => DI::l10n()->t('Relays')],
];
return self::filter(

View file

@ -28,7 +28,6 @@ use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Util\Network;
use Friendica\Util\Strings;
/**
@ -42,9 +41,11 @@ class VCard
* Get HTML for vcard block
*
* @template widget/vcard.tpl
* @param array $contact
* @param bool $hide_mention
* @return string
*/
public static function getHTML(array $contact): string
public static function getHTML(array $contact, bool $hide_mention = false): string
{
if (!isset($contact['network']) || !isset($contact['id'])) {
Logger::warning('Incomplete contact', ['contact' => $contact ?? []]);
@ -99,10 +100,12 @@ class VCard
}
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_link = 'compose/0?body=!' . $contact['addr'];
if (!$hide_mention) {
$mention_label = DI::l10n()->t('Post to group');
$mention_link = 'compose/0?body=!' . $contact['addr'];
}
$showgroup_link = 'network/group/' . $id;
} else {
} elseif (!$hide_mention) {
$mention_label = DI::l10n()->t('Mention');
$mention_link = 'compose/0?body=@' . $contact['addr'];
}

View file

@ -632,23 +632,10 @@ class Installer
*/
public function checkImagick()
{
$imagick = false;
$gif = false;
if (class_exists('Imagick')) {
$imagick = true;
$supported = Images::supportedTypes();
if (array_key_exists('image/gif', $supported)) {
$gif = true;
}
}
if (!$imagick) {
$this->addCheck(DI::l10n()->t('ImageMagick PHP extension is not installed'), $imagick, false, "");
if (!class_exists('Imagick')) {
$this->addCheck(DI::l10n()->t('ImageMagick PHP extension is not installed'), false, false, "");
} else {
$this->addCheck(DI::l10n()->t('ImageMagick PHP extension is installed'), $imagick, false, "");
if ($imagick) {
$this->addCheck(DI::l10n()->t('ImageMagick supports GIF'), $gif, false, "");
}
$this->addCheck(DI::l10n()->t('ImageMagick PHP extension is installed'), true, false, "");
}
// Imagick is not required

View file

@ -245,6 +245,7 @@ class Search
'Group' => Contact::TYPE_COMMUNITY,
'Organization' => Contact::TYPE_ORGANISATION,
'News' => Contact::TYPE_NEWS,
'Relay' => Contact::TYPE_RELAY,
];
return [

View file

@ -473,7 +473,7 @@ class System
return false;
}
return max($load_arr[0], $load_arr[1]);
return round(max($load_arr[0], $load_arr[1]), 2);
}
/**

View file

@ -52,7 +52,7 @@ class PostUpdate
// Needed for the helper function to read from the legacy term table
const OBJECT_TYPE_POST = 1;
const VERSION = 1547;
const VERSION = 1550;
/**
* Calls the post update functions
@ -128,7 +128,7 @@ class PostUpdate
if (!self::update1544()) {
return false;
}
if (!self::update1547()) {
if (!self::update1550()) {
return false;
}
return true;
@ -1369,14 +1369,24 @@ class PostUpdate
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function update1547()
private static function update1550()
{
// Was the script completed?
if (DI::keyValue()->get('post_update_version') >= 1547) {
if (DI::keyValue()->get('post_update_version') >= 1550) {
return true;
}
$id = (int)(DI::keyValue()->get('post_update_version_1547_id') ?? 0);
$engagements = DBA::select('post-engagement', ['uri-id'], ["`language` IS NULL"], ['order' => ['uri-id' => true], 'limit' => 1000]);
while ($engagement = DBA::fetch($engagements)) {
$item = Post::selectFirst([], ['uri-id' => $engagement['uri-id']]);
if (empty($item)) {
continue;
}
Post\Engagement::storeFromItem($item);
}
DBA::close($engagements);
$id = (int)(DI::keyValue()->get('post_update_version_1550_id') ?? 0);
if ($id == 0) {
$post = Post::selectFirstPost(['uri-id'], [], ['order' => ['uri-id' => true]]);
$id = (int)($post['uri-id'] ?? 0);
@ -1393,7 +1403,7 @@ class PostUpdate
DBA::mergeConditions($condition, ["`created` > ?", $limit]);
}
$posts = Post::selectPosts(['uri-id', 'network', 'private', 'created'], $condition, ['order' => ['uri-id' => true], 'limit' => 1000]);
$posts = Post::selectPosts(['uri-id', 'created'], $condition, ['order' => ['uri-id' => true], 'limit' => 1000]);
if (DBA::errorNo() != 0) {
Logger::error('Database error', ['no' => DBA::errorNo(), 'message' => DBA::errorMessage()]);
@ -1402,17 +1412,17 @@ class PostUpdate
while ($post = Post::fetch($posts)) {
$id = $post['uri-id'];
Post\SearchIndex::insert($post['uri-id'], $post['network'], $post['private'], $post['created'], true);
Post\SearchIndex::insert($post['uri-id'], $post['created'], true);
++$rows;
}
DBA::close($posts);
DI::keyValue()->set('post_update_version_1547_id', $id);
DI::keyValue()->set('post_update_version_1550_id', $id);
Logger::info('Processed', ['rows' => $rows, 'last' => $id]);
if ($rows <= 100) {
DI::keyValue()->set('post_update_version', 1547);
DI::keyValue()->set('post_update_version', 1550);
Logger::info('Done');
return true;
}

View file

@ -23,6 +23,7 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory;
use Friendica\Database\Database;
use Friendica\Model\Subscription;
use Friendica\Network\HTTPException\UnprocessableEntityException;
use Psr\Log\LoggerInterface;
@ -56,6 +57,8 @@ class Application extends BaseFactory
$application['client_secret'],
$application['id'],
$application['redirect_uri'],
$application['website']);
$application['website'],
Subscription::getPublicVapidKey(),
);
}
}

View file

@ -84,7 +84,7 @@ class Attachment extends BaseFactory
$type = 'audio';
} elseif (($filetype == 'video') || ($attachment['type'] == Post\Media::VIDEO)) {
$type = 'video';
} elseif ($attachment['mimetype'] == 'image/gif') {
} elseif ($attachment['mimetype'] == image_type_to_mime_type(IMAGETYPE_GIF)) {
$type = 'gifv';
} elseif (($filetype == 'image') || ($attachment['type'] == Post\Media::IMAGE)) {
$type = 'image';
@ -95,12 +95,12 @@ class Attachment extends BaseFactory
$remote = $attachment['url'];
if ($type == 'image') {
$url = Post\Media::getPreviewUrlForId($attachment['id']);
$preview = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_SMALL);
$preview = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM);
} else {
$url = $attachment['url'];
if (!empty($attachment['preview'])) {
$preview = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_SMALL);
$preview = Post\Media::getPreviewUrlForId($attachment['id'], Proxy::SIZE_MEDIUM);
} else {
$preview = '';
}
@ -130,14 +130,13 @@ class Attachment extends BaseFactory
'blurhash' => $photo['blurhash'],
];
$photoTypes = Images::supportedTypes();
$ext = $photoTypes[$photo['type']];
$ext = Images::getExtensionByMimeType($photo['type']);
$url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-0.' . $ext;
$url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-0' . $ext;
$preview = Photo::selectFirst(['scale'], ["`resource-id` = ? AND `uid` = ? AND `scale` > ?", $photo['resource-id'], $photo['uid'], 0], ['order' => ['scale']]);
if (!empty($preview)) {
$preview_url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-' . $preview['scale'] . '.' . $ext;
$preview_url = $this->baseUrl . '/photo/' . $photo['resource-id'] . '-' . $preview['scale'] . $ext;
} else {
$preview_url = '';
}

View file

@ -67,10 +67,12 @@ class Poll extends BaseFactory
if (empty($uid)) {
$ownvotes = null;
$voted = null;
} else {
$ownvotes = [];
$voted = false;
}
return new \Friendica\Object\Api\Mastodon\Poll($question, $options, $expired, $votes, $ownvotes);
return new \Friendica\Object\Api\Mastodon\Poll($question, $options, $expired, $votes, $ownvotes, $voted);
}
}

View file

@ -32,7 +32,6 @@ use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\Tag as TagModel;
use Friendica\Model\Verb;
use Friendica\Network\HTTPException;
use Friendica\Object\Api\Mastodon\Status\FriendicaDeliveryData;
@ -60,8 +59,6 @@ class Status extends BaseFactory
private $mstdnAttachmentFactory;
/** @var Emoji */
private $mstdnEmojiFactory;
/** @var Error */
private $mstdnErrorFactory;
/** @var Poll */
private $mstdnPollFactory;
/** @var ContentItem */
@ -78,7 +75,6 @@ class Status extends BaseFactory
Card $mstdnCardFactory,
Attachment $mstdnAttachmentFactory,
Emoji $mstdnEmojiFactory,
Error $mstdnErrorFactory,
Poll $mstdnPollFactory,
ContentItem $contentItem,
ACLFormatter $aclFormatter
@ -91,7 +87,6 @@ class Status extends BaseFactory
$this->mstdnCardFactory = $mstdnCardFactory;
$this->mstdnAttachmentFactory = $mstdnAttachmentFactory;
$this->mstdnEmojiFactory = $mstdnEmojiFactory;
$this->mstdnErrorFactory = $mstdnErrorFactory;
$this->mstdnPollFactory = $mstdnPollFactory;
$this->contentItem = $contentItem;
$this->aclFormatter = $aclFormatter;
@ -112,7 +107,7 @@ class Status extends BaseFactory
{
$fields = ['uri-id', 'uid', 'author-id', 'causer-id', 'author-uri-id', 'author-link', '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',
'delivery_queue_count', 'delivery_queue_done','delivery_queue_failed', 'allow_cid', 'deny_cid', 'allow_gid', 'deny_gid'];
'delivery_queue_count', 'delivery_queue_done','delivery_queue_failed', 'allow_cid', 'deny_cid', 'allow_gid', 'deny_gid', 'sensitive'];
$item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (!$item) {
$mail = DBA::selectFirst('mail', ['id'], ['uri-id' => $uriId, 'uid' => $uid]);
@ -217,7 +212,7 @@ class Status extends BaseFactory
$item['featured']
);
$sensitive = $this->dba->exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw', 'type' => TagModel::HASHTAG]);
$sensitive = (bool)$item['sensitive'];
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: ContactSelector::networkToName($item['network'], $item['author-link']));
$mentions = $this->mstdnMentionFactory->createFromUriId($uriId)->getArrayCopy();

View file

@ -24,10 +24,9 @@ namespace Friendica\Factory\Api\Twitter;
use Friendica\BaseFactory;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Network\HTTPException;
use Friendica\Factory\Api\Twitter\Status;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Network\HTTPException;
use Psr\Log\LoggerInterface;
class User extends BaseFactory
@ -85,9 +84,17 @@ class User extends BaseFactory
* @param bool $include_user_entities
*
* @return \Friendica\Object\Api\Twitter\User
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\NotFoundException If the $uid doesn't exist
* @throws \ImagickException
*/
public function createFromUserId(int $uid, bool $skip_status = true, bool $include_user_entities = true): \Friendica\Object\Api\Twitter\User
{
return $this->createFromContactId(Contact::getPublicIdByUserId($uid), $uid, $skip_status, $include_user_entities);
$cid = Contact::getPublicIdByUserId($uid);
if (!$cid) {
throw new HTTPException\NotFoundException();
}
return $this->createFromContactId($cid, $uid, $skip_status, $include_user_entities);
}
}

View file

@ -208,6 +208,9 @@ class APContact
if (!$failed && ($curlResult->getReturnCode() == 410)) {
$data = ['@context' => ActivityPub::CONTEXT, 'id' => $url, 'type' => 'Tombstone'];
} elseif (!$failed && !HTTPSignature::isValidContentType($curlResult->getContentType())) {
Logger::debug('Unexpected content type', ['content-type' => $curlResult->getContentType(), 'url' => $url]);
$failed = true;
}
} catch (\Exception $exception) {
Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]);

View file

@ -755,7 +755,7 @@ class Contact
$user = DBA::selectFirst(
'user',
['uid', 'username', 'nickname', 'pubkey', 'prvkey'],
['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
['uid' => $uid, 'account_removed' => false, 'account_expired' => false]
);
if (!DBA::isResult($user)) {
return false;
@ -784,6 +784,7 @@ class Contact
'name-date' => DateTimeFormat::utcNow(),
'uri-date' => DateTimeFormat::utcNow(),
'avatar-date' => DateTimeFormat::utcNow(),
'baseurl' => DI::baseUrl(),
'closeness' => 0
];
@ -819,7 +820,7 @@ class Contact
$fields = [
'id', 'uri-id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve',
'xmpp', 'matrix', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable',
'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network'
'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network', 'baseurl', 'gsid'
];
$self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]);
if (!DBA::isResult($self)) {
@ -841,7 +842,6 @@ class Contact
return false;
}
$file_suffix = 'jpg';
$url = DI::baseUrl() . '/profile/' . $user['nickname'];
$fields = [
@ -874,17 +874,11 @@ class Contact
$fields['avatar-date'] = DateTimeFormat::utcNow();
}
// Creating the path to the avatar, beginning with the file suffix
$types = Images::supportedTypes();
if (isset($types[$avatar['type']])) {
$file_suffix = $types[$avatar['type']];
}
// We are adding a timestamp value so that other systems won't use cached content
$timestamp = strtotime($fields['avatar-date']);
$prefix = DI::baseUrl() . '/photo/' . $avatar['resource-id'] . '-';
$suffix = '.' . $file_suffix . '?ts=' . $timestamp;
$suffix = Images::getExtensionByMimeType($avatar['type']) . '?ts=' . $timestamp;
$fields['photo'] = $prefix . '4' . $suffix;
$fields['thumb'] = $prefix . '5' . $suffix;
@ -902,6 +896,8 @@ class Contact
$fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP;
$fields['unsearchable'] = !$profile['net-publish'];
$fields['manually-approve'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]);
$fields['baseurl'] = DI::baseUrl();
$fields['gsid'] = GServer::getID($fields['baseurl'], true);
$update = false;
@ -1747,6 +1743,10 @@ class Contact
$account_type = DI::l10n()->t("Group");
break;
case self::TYPE_RELAY:
$account_type = DI::l10n()->t("Relay");
break;
default:
$account_type = "";
break;
@ -2306,8 +2306,8 @@ class Contact
$fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]);
$img_str = $fetchResult->getBodyString();
if (!empty($img_str)) {
$image = new Image($img_str, Images::getMimeTypeByData($img_str));
if ($fetchResult->isSuccess() && !empty($img_str)) {
$image = new Image($img_str, $fetchResult->getContentType(), $avatar);
if ($image->isValid()) {
$update_fields['blurhash'] = $image->getBlurHash();
} else {
@ -3099,6 +3099,13 @@ class Contact
$ret['url'] = str_replace('?absolute=true', '', $ret['url']);
}
if (($protocol == Protocol::ACTIVITYPUB) && ($uid != 0)) {
if (APContact::isRelay(APContact::getByURL($ret['url']))) {
$result['message'] = DI::l10n()->t('This seems to be a relay account. They can\'t be followed by users.');
return $result;
}
}
// do we have enough information?
if (empty($protocol) || ($protocol == Protocol::PHANTOM) || (empty($ret['url']) && empty($ret['addr']))) {
$result['message'] .= DI::l10n()->t('The profile address specified does not provide adequate information.') . '<br />';

View file

@ -22,6 +22,7 @@
namespace Friendica\Model\Contact;
use Exception;
use Friendica\Content\Widget;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
@ -78,14 +79,14 @@ class Relation
*/
public static function discoverByUser(int $uid)
{
$contact = Contact::selectFirst(['id', 'url', 'network'], ['uid' => $uid, 'self' => true]);
$contact = Contact::selectFirst(['id', 'url', 'network'], ['id' => Contact::getPublicIdByUserId($uid)]);
if (empty($contact)) {
Logger::warning('Self contact for user not found', ['uid' => $uid]);
return;
}
$followers = self::getContacts($uid, [Contact::FOLLOWER, Contact::FRIEND]);
$followings = self::getContacts($uid, [Contact::SHARING, Contact::FRIEND]);
$followers = self::getContacts($uid, [Contact::FOLLOWER, Contact::FRIEND], false);
$followings = self::getContacts($uid, [Contact::SHARING, Contact::FRIEND], false);
self::updateFollowersFollowings($contact, $followers, $followings);
}
@ -207,10 +208,11 @@ class Relation
* Fetch contact url list from the given local user
*
* @param integer $uid
* @param array $rel
* @param array $rel
* @param bool $only_ap
* @return array contact list
*/
private static function getContacts(int $uid, array $rel): array
private static function getContacts(int $uid, array $rel, bool $only_ap = true): array
{
$list = [];
$profile = Profile::getByUID($uid);
@ -219,15 +221,22 @@ class Relation
}
$condition = [
'rel' => $rel,
'uid' => $uid,
'self' => false,
'rel' => $rel,
'uid' => $uid,
'self' => false,
'deleted' => false,
'hidden' => false,
'hidden' => false,
'archive' => false,
'pending' => false,
'blocked' => false,
'failed' => false,
];
$condition = DBA::mergeConditions($condition, ["`url` IN (SELECT `url` FROM `apcontact`)"]);
if ($only_ap) {
$condition = DBA::mergeConditions($condition, ["`url` IN (SELECT `url` FROM `apcontact`)"]);
} else {
$networks = Widget::unavailableNetworks();
$condition = DBA::mergeConditions($condition, array_merge(["NOT `network` IN (" . substr(str_repeat("?, ", count($networks)), 0, -2) . ")"], $networks));
}
$contacts = DBA::select('contact', ['url'], $condition);
while ($contact = DBA::fetch($contacts)) {
$list[] = $contact['url'];
@ -870,6 +879,20 @@ class Relation
DBA::update('contact-relation', ['thread-score' => $score], ['relation-cid' => $contact_id, 'cid' => $interaction['author-id']]);
}
DBA::close($interactions);
$total = DBA::fetchFirst("SELECT count(*) AS `posts` FROM `post-thread-user` WHERE EXISTS(SELECT `cid` FROM `contact-relation` WHERE `cid` = `post-thread-user`.`author-id` AND `relation-cid` = ? AND `follows`) AND `uid` = ? AND `created` > ?",
$contact_id, $uid, DateTimeFormat::utc('now - ' . $days . ' day'));
Logger::debug('Calculate post-score', ['uid' => $uid, 'total' => $total['posts']]);
$posts = DBA::p("SELECT `author-id`, count(*) AS `posts` FROM `post-thread-user` WHERE EXISTS(SELECT `cid` FROM `contact-relation` WHERE `cid` = `post-thread-user`.`author-id` AND `relation-cid` = ? AND `follows`) AND `uid` = ? AND `created` > ? GROUP BY `author-id`",
$contact_id, $uid, DateTimeFormat::utc('now - ' . $days . ' day'));
while ($post = DBA::fetch($posts)) {
$score = min((int)(($post['posts'] / $total['posts']) * 65535), 65535);
DBA::update('contact-relation', ['post-score' => $score], ['relation-cid' => $contact_id, 'cid' => $post['author-id']]);
}
DBA::close($posts);
Logger::debug('Calculation - end', ['uid' => $uid]);
}
}

View file

@ -925,9 +925,6 @@ class Event
$end_short = '';
}
// Format the event location.
$location = self::locationToArray($item['event-location']);
// Construct the profile link (magic-auth).
$author = [
'uid' => 0,
@ -964,7 +961,7 @@ class Event
'$show_map_label' => DI::l10n()->t('Show map'),
'$hide_map_label' => DI::l10n()->t('Hide map'),
'$map_btn_label' => DI::l10n()->t('Show map'),
'$location' => $location
'$location' => self::locationToTemplateVars($item['event-location']),
]);
return $return;
@ -984,7 +981,7 @@ class Event
* 'coordinates' => Latitude and longitude (e.g. '48.864716,2.349014').<br>
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function locationToArray(string $s = ''): array
private static function locationToTemplateVars(string $s = ''): array
{
if ($s == '') {
return [];

View file

@ -34,10 +34,11 @@ use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post\Category;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\HTTPException\ServiceUnavailableException;
use Friendica\Protocol\Activity;
@ -45,6 +46,7 @@ use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Delivery;
use Friendica\Protocol\Diaspora;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPSignature;
use Friendica\Util\Map;
use Friendica\Util\Network;
use Friendica\Util\Proxy;
@ -99,7 +101,7 @@ class Item
'uid', 'id', 'parent', 'guid', 'network', 'gravity',
'uri-id', 'uri', 'thr-parent-id', 'thr-parent', 'parent-uri-id', 'parent-uri', 'conversation',
'commented', 'created', 'edited', 'received', 'verb', 'object-type', 'postopts', 'plink',
'wall', 'private', 'starred', 'origin', 'parent-origin', 'title', 'body', 'language',
'wall', 'private', 'starred', 'origin', 'parent-origin', 'title', 'body', 'language', 'sensitive',
'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object',
'quote-uri', 'quote-uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'mention', 'global',
'author-id', 'author-link', 'author-alias', 'author-name', 'author-avatar', 'author-network', 'author-updated', 'author-gsid', 'author-baseurl', 'author-addr', 'author-uri-id',
@ -119,7 +121,7 @@ class Item
const DELIVER_FIELDLIST = [
'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid',
'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target',
'private', 'title', 'body', 'raw-body', 'language', 'location', 'coord', 'app',
'private', 'title', 'body', 'raw-body', 'language', 'location', 'coord', 'app', 'sensitive',
'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity',
'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid',
'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid',
@ -689,38 +691,6 @@ class Item
return true;
}
/**
* Check if the item array is too old
*
* @param array $item Item record
* @return boolean item is too old
*/
public static function isTooOld(array $item): bool
{
// check for create date and expire time
$expire_interval = DI::config()->get('system', 'dbclean-expire-days', 0);
$user = DBA::selectFirst('user', ['expire'], ['uid' => $item['uid']]);
if (DBA::isResult($user) && ($user['expire'] > 0) && (($user['expire'] < $expire_interval) || ($expire_interval == 0))) {
$expire_interval = $user['expire'];
}
if (($expire_interval > 0) && !empty($item['created'])) {
$expire_date = time() - ($expire_interval * 86400);
$created_date = strtotime($item['created']);
if ($created_date < $expire_date) {
Logger::notice('Item created before expiration interval.', [
'created' => date('c', $created_date),
'expired' => date('c', $expire_date),
'$item' => $item
]);
return true;
}
}
return false;
}
/**
* Return the id of the given item array if it has been stored before
*
@ -1032,7 +1002,7 @@ class Item
if (
!empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY]) &&
empty($item['origin']) && self::isTooOld($item)
empty($item['origin']) && DI::contentItem()->isTooOld($item['created'], $item['uid'])
) {
Logger::info('Item is too old', ['item' => $item]);
return 0;
@ -1280,6 +1250,15 @@ class Item
}
}
// Store tags from the body if this hadn't been handled previously in the protocol classes
if (!Tag::existsForPost($item['uri-id'])) {
Tag::storeFromBody($item['uri-id'], $item['body']);
}
if (in_array($item['gravity'], [self::GRAVITY_PARENT, self::GRAVITY_COMMENT]) && (!isset($item['sensitive']) || is_null($item['sensitive']))) {
$item['sensitive'] = Tag::existsTagForPost($item['uri-id'], 'nsfw');
}
$item['language'] = self::getLanguage($item);
$inserted = Post::insert($item['uri-id'], $item);
@ -1319,11 +1298,6 @@ class Item
Post\DeliveryData::insert($item['uri-id'], $delivery_data);
}
// Store tags from the body if this hadn't been handled previously in the protocol classes
if (!Tag::existsForPost($item['uri-id'])) {
Tag::storeFromBody($item['uri-id'], $item['body']);
}
$condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid']];
if (Post::exists($condition)) {
Logger::notice('Item is already inserted - aborting', $condition);
@ -1448,9 +1422,11 @@ class Item
}
$engagement_uri_id = Post\Engagement::storeFromItem($posted_item);
if (in_array($posted_item['gravity'], [self::GRAVITY_PARENT, self::GRAVITY_COMMENT])) {
Post\SearchIndex::insert($posted_item['uri-id'], $posted_item['network'], $posted_item['private'], $posted_item['created']);
Post\SearchIndex::insert($posted_item['uri-id'], $posted_item['created']);
} elseif ($posted_item['verb'] == Activity::ANNOUNCE) {
Post\SearchIndex::update($posted_item['thr-parent-id']);
}
if (($posted_item['gravity'] == self::GRAVITY_ACTIVITY) && ($posted_item['verb'] == Activity::ANNOUNCE) && ($posted_item['parent-uri-id'] == $posted_item['thr-parent-id'])) {
@ -1499,7 +1475,11 @@ class Item
];
if (!Post::exists($condition)) {
Logger::debug('Reshare post', ['uid' => $uid, 'uri-id' => $uri_id]);
self::performActivity($item['id'], 'announce', $uid);
$allow_cid = '';
$allow_gid = '<' . Circle::FOLLOWERS . '>';
$deny_cid = '';
$deny_gid = '';
self::performActivity($item['id'], 'announce', $uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid);
} else {
Logger::debug('Reshare already exists', ['uid' => $uid, 'uri-id' => $uri_id]);
}
@ -1616,7 +1596,7 @@ class Item
}
$languages = $item['language'] ? array_keys(json_decode($item['language'], true)) : [];
foreach (Tag::getUIDListByURIId($item['uri-id']) as $uid => $tags) {
if (!empty($languages)) {
$keep = false;
@ -2481,7 +2461,7 @@ class Item
}
$basetag = str_replace('_', ' ', substr($tag, 1));
$newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . $basetag . ']' . $basetag . '[/url]';
$newtag = '#[url=' . DI::baseUrl() . '/search?tag=' . urlencode($basetag) . ']' . $basetag . '[/url]';
$body = str_replace($tag, $newtag, $body);
}
@ -3320,9 +3300,9 @@ class Item
public static function prepareBody(array &$item, bool $attach = false, bool $is_preview = false, bool $only_cache = false): string
{
$a = DI::app();
$uid = DI::userSession()->getLocalUserId();
Hook::callAll('prepare_body_init', $item);
// In order to provide theme developers more possibilities, event items
// are treated differently.
if ($item['object-type'] === Activity\ObjectType::EVENT && isset($item['event-id'])) {
@ -3335,6 +3315,7 @@ class Item
$item['tags'] = $tags['tags'];
$item['hashtags'] = $tags['hashtags'];
$item['mentions'] = $tags['mentions'];
$sensitive = $item['sensitive'] && !DI::pConfig()->get($uid, 'system', 'display_sensitive', false);
if (!$is_preview) {
$item['body'] = preg_replace("#\s*\[attachment .*?].*?\[/attachment]\s*#ism", "\n", $item['body']);
@ -3402,11 +3383,11 @@ class Item
$shared_links = array_merge($shared_links, $sharedSplitAttachments['visual']->column('url'));
$shared_links = array_merge($shared_links, $sharedSplitAttachments['link']->column('url'));
$shared_links = array_merge($shared_links, $sharedSplitAttachments['additional']->column('url'));
$item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body']);
$item['body'] = self::replaceVisualAttachments($sharedSplitAttachments['visual'], $item['body'], $sensitive);
}
$itemSplitAttachments = DI::postMediaRepository()->splitAttachments($item['uri-id'], $shared_links, $item['has-media'] ?? false);
$item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? '');
$item['body'] = self::replaceVisualAttachments($itemSplitAttachments['visual'], $item['body'] ?? '', $sensitive);
self::putInCache($item);
$item['body'] = $body;
@ -3427,7 +3408,7 @@ class Item
$filter_reasons[] = DI::l10n()->t('Content from %s is collapsed', $item['author-name']);
}
if (!empty($item['content-warning']) && (!DI::userSession()->getLocalUserId() || !DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'disable_cw', false))) {
if (!empty($item['content-warning']) && (!$uid || !DI::pConfig()->get($uid, 'system', 'disable_cw', false))) {
$filter_reasons[] = DI::l10n()->t('Content warning: %s', $item['content-warning']);
}
@ -3461,9 +3442,9 @@ class Item
}
if (!empty($sharedSplitAttachments)) {
$s = self::addGallery($s, $sharedSplitAttachments['visual']);
$s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true);
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links);
$s = self::addGallery($s, $sharedSplitAttachments['visual'], $sensitive);
$s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true, $sensitive);
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links, $sensitive);
$s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true);
$body = BBCode::removeSharedData($body);
}
@ -3474,9 +3455,9 @@ class Item
$s = substr($s, 0, $pos);
}
$s = self::addGallery($s, $itemSplitAttachments['visual']);
$s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false);
$s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links);
$s = self::addGallery($s, $itemSplitAttachments['visual'], $sensitive);
$s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false, $sensitive);
$s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links, $sensitive);
$s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false);
$s = self::addQuestions($item, $s);
@ -3510,9 +3491,10 @@ class Item
*
* @param string $s
* @param PostMedias $PostMedias
* @param bool $sensitive
* @return string
*/
private static function addGallery(string $s, PostMedias $PostMedias): string
private static function addGallery(string $s, PostMedias $PostMedias, bool $sensitive): string
{
foreach ($PostMedias as $PostMedia) {
if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) {
@ -3522,9 +3504,10 @@ class Item
if ($PostMedia->hasDimensions()) {
$pattern = '#<a href="' . preg_quote($PostMedia->url) . '">(.*?)"></a>#';
$s = preg_replace_callback($pattern, function () use ($PostMedia) {
$s = preg_replace_callback($pattern, function () use ($PostMedia, $sensitive) {
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
'$image' => $PostMedia,
'$sensitive' => $sensitive,
'$allocated_height' => $PostMedia->getAllocatedHeight(),
'$allocated_max_width' => ($PostMedia->previewWidth ?? $PostMedia->width) . 'px',
]);
@ -3593,9 +3576,10 @@ class Item
*
* @param PostMedias $PostMedias
* @param string $body
* @param bool $sensitive
* @return string modified body
*/
private static function replaceVisualAttachments(PostMedias $PostMedias, string $body): string
private static function replaceVisualAttachments(PostMedias $PostMedias, string $body, bool $sensitive): string
{
DI::profiler()->startRecording('rendering');
@ -3604,7 +3588,7 @@ class Item
if (DI::baseUrl()->isLocalUri($PostMedia->preview)) {
continue;
}
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE, $sensitive);
$search = ['[img=' . $PostMedia->preview . ']', ']' . $PostMedia->preview . '[/img]'];
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
@ -3613,7 +3597,7 @@ class Item
if (DI::baseUrl()->isLocalUri($PostMedia->url)) {
continue;
}
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE);
$proxy = DI::baseUrl() . $PostMedia->getPreviewPath(Proxy::SIZE_LARGE, $sensitive);
$search = ['[img=' . $PostMedia->url . ']', ']' . $PostMedia->url . '[/img]'];
$replace = ['[img=' . $proxy . ']', ']' . $proxy . '[/img]'];
@ -3631,10 +3615,11 @@ class Item
* @param array $item
* @param string $content
* @param bool $shared
* @param bool $sensitive
* @return string modified content
* @throws ServiceUnavailableException
*/
private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared): string
private static function addVisualAttachments(PostMedias $PostMedias, array $item, string $content, bool $shared, bool $sensitive): string
{
DI::profiler()->startRecording('rendering');
$leading = '';
@ -3649,7 +3634,7 @@ class Item
if ($PostMedia->mimetype->type == 'image' || $PostMedia->preview) {
$preview_size = Proxy::SIZE_MEDIUM;
$preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size);
$preview_url = DI::baseUrl() . $PostMedia->getPreviewPath($preview_size, $sensitive);
} else {
$preview_size = 0;
$preview_url = '';
@ -3660,6 +3645,13 @@ class Item
}
if ($PostMedia->mimetype->type == 'video') {
if (($PostMedia->height ?? 0) > ($PostMedia->width ?? 0)) {
$height = min(DI::config()->get('system', 'max_video_height') ?: '100%', $PostMedia->height);
$width = 'auto';
} else {
$height = 'auto';
$width = '100%';
}
/// @todo Move the template to /content as well
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('video_top.tpl'), [
'$video' => [
@ -3668,6 +3660,8 @@ class Item
'name' => $PostMedia->name ?: $PostMedia->url,
'preview' => $preview_url,
'mime' => (string)$PostMedia->mimetype,
'height' => $height,
'width' => $width,
],
]);
if (($item['post-type'] ?? null) == Item::PT_VIDEO) {
@ -3734,11 +3728,12 @@ class Item
* @param string $content
* @param bool $shared
* @param array $ignore_links A list of URLs to ignore
* @param bool $sensitive
* @return string modified content
* @throws InternalServerErrorException
* @throws ServiceUnavailableException
*/
private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links): string
private static function addLinkAttachment(int $uriid, array $attachments, string $body, string $content, bool $shared, array $ignore_links, bool $sensitive): string
{
DI::profiler()->startRecording('rendering');
// Don't show a preview when there is a visual attachment (audio or video)
@ -3781,9 +3776,9 @@ class Item
if ($preview && $attachment->preview) {
if ($attachment->previewWidth >= 500) {
$data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
$data['image'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM, $sensitive);
} else {
$data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM);
$data['preview'] = DI::baseUrl() . $attachment->getPreviewPath(Proxy::SIZE_MEDIUM, $sensitive);
}
}
@ -3807,6 +3802,12 @@ class Item
} elseif (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $body, $match)) {
$data = BBCode::getAttachmentData($match[1]);
}
if ($sensitive) {
$data['image'] = '';
$data['preview'] = '';
}
DI::profiler()->stopRecording();
if (isset($data['url']) && !in_array(strtolower($data['url']), $ignore_links)) {
@ -4095,9 +4096,12 @@ class Item
return is_numeric($hookData['item_id']) ? $hookData['item_id'] : 0;
}
$fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', $completion, $uid);
$curlResult = DI::httpClient()->head($uri, [HttpClientOptions::ACCEPT_CONTENT => HttpClientAccept::JSON_AS]);
if (HTTPSignature::isValidContentType($curlResult->getContentType())) {
$fetched_uri = ActivityPub\Processor::fetchMissingActivity($uri, [], '', $completion, $uid);
}
if ($fetched_uri) {
if (!empty($fetched_uri)) {
$item_id = self::searchByLink($fetched_uri, $uid);
} else {
$item_id = Diaspora::fetchByURL($uri);

View file

@ -363,6 +363,7 @@ class Photo
$photo['backend-class'] = SystemResource::NAME;
$photo['backend-ref'] = $filename;
$photo['type'] = $mimetype;
$photo['filename'] = basename($filename);
$photo['cacheable'] = false;
return $photo;
@ -394,6 +395,7 @@ class Photo
$photo['backend-class'] = ExternalResource::NAME;
$photo['backend-ref'] = json_encode(['url' => $url, 'uid' => $uid]);
$photo['type'] = $mimetype;
$photo['filename'] = basename(parse_url($url, PHP_URL_PATH));
$photo['cacheable'] = true;
$photo['blurhash'] = $blurhash;
$photo['width'] = $width;
@ -608,9 +610,7 @@ class Photo
return false;
}
$type = Images::getMimeTypeByData($img_str, $image_url, $type);
$image = new Image($img_str, $type);
$image = new Image($img_str, $type, $image_url);
if ($image->isValid()) {
$image->scaleToSquare(300);
@ -619,9 +619,9 @@ class Photo
if ($maximagesize && ($filesize > $maximagesize)) {
Logger::info('Avatar exceeds image limit', ['uid' => $uid, 'cid' => $cid, 'maximagesize' => $maximagesize, 'size' => $filesize, 'type' => $image->getType()]);
if ($image->getType() == 'image/gif') {
if ($image->getImageType() == IMAGETYPE_GIF) {
$image->toStatic();
$image = new Image($image->asString(), 'image/png');
$image = new Image($image->asString(), image_type_to_mime_type(IMAGETYPE_PNG));
$filesize = strlen($image->asString());
Logger::info('Converted gif to a static png', ['uid' => $uid, 'cid' => $cid, 'size' => $filesize, 'type' => $image->getType()]);
@ -662,9 +662,9 @@ class Photo
$suffix = '?ts=' . time();
$image_url = DI::baseUrl() . '/photo/' . $resource_id . '-4.' . $image->getExt() . $suffix;
$thumb = DI::baseUrl() . '/photo/' . $resource_id . '-5.' . $image->getExt() . $suffix;
$micro = DI::baseUrl() . '/photo/' . $resource_id . '-6.' . $image->getExt() . $suffix;
$image_url = DI::baseUrl() . '/photo/' . $resource_id . '-4' . $image->getExt() . $suffix;
$thumb = DI::baseUrl() . '/photo/' . $resource_id . '-5' . $image->getExt() . $suffix;
$micro = DI::baseUrl() . '/photo/' . $resource_id . '-6' . $image->getExt() . $suffix;
} else {
$photo_failure = true;
}
@ -1060,9 +1060,7 @@ class Photo
return [];
}
$type = Images::getMimeTypeByData($img_str, $image_url, $type);
$image = new Image($img_str, $type);
$image = new Image($img_str, $type, $image_url);
$image = self::fitImageSize($image);
if (empty($image)) {
@ -1132,12 +1130,10 @@ class Photo
return [];
}
$filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
Logger::info('File upload', ['src' => $src, 'filename' => $filename, 'size' => $filesize, 'type' => $filetype]);
$imagedata = @file_get_contents($src);
$image = new Image($imagedata, $filetype);
$image = new Image($imagedata, $filetype, $filename);
if (!$image->isValid()) {
Logger::notice('Image is unvalid', ['files' => $files]);
return [];

View file

@ -110,9 +110,9 @@ class Content
{
$search = Post\Engagement::escapeKeywords($search);
if ($uid != 0) {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) and (private = ? OR `uri-id` in (SELECT `uri-id` FROM `post-user` where `uid` = ?))", $search, Item::PUBLIC, $uid];
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?))", $search, $uid];
} else {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) and private = ?", $search, Item::PUBLIC];
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND NOT `restricted`", $search];
}
if (!empty($last_uriid)) {
@ -139,9 +139,9 @@ class Content
{
$search = Post\Engagement::escapeKeywords($search);
if ($uid != 0) {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) and (private = ? OR `uri-id` in (SELECT `uri-id` FROM `post-user` where `uid` = ?))", $search, Item::PUBLIC, $uid];
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?))", $search, $uid];
} else {
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) and private = ?", $search, Item::PUBLIC];
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND NOT `restricted", $search];
}
return DBA::count('post-searchindex', $condition);
}

View file

@ -43,7 +43,7 @@ class Counts
Activity::EMOJIREACT, Activity::ANNOUNCE, Activity::VIEW, Activity::READ])) {
return true;
}
$condition = ['thr-parent-id' => $uri_id, 'vid' => $vid, 'deleted' => false];
if ($body == $verb) {
@ -52,7 +52,7 @@ class Counts
} elseif ($verb == Activity::POST) {
$condition['gravity'] = Item::GRAVITY_COMMENT;
$body = '';
} elseif (($verb != Activity::POST) && (mb_strlen($body) == 1) && Smilies::isEmojiPost($body)) {
} elseif ($body && mb_strlen($body) == 1 && Smilies::isEmojiPost($body)) {
$condition['body'] = $body;
} else {
$body = '';

View file

@ -22,6 +22,7 @@
namespace Friendica\Model\Post;
use Friendica\Content\Text\BBCode;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
@ -38,7 +39,19 @@ use Friendica\Util\DateTimeFormat;
class Engagement
{
const KEYWORDS = ['source', 'server', 'from', 'to', 'group', 'tag', 'network', 'platform', 'visibility', 'language'];
const KEYWORDS = ['source', 'server', 'from', 'to', 'group', 'application', 'tag', 'network', 'platform', 'visibility', 'language', 'media'];
const SHORTCUTS = ['lang' => 'language', 'net' => 'network', 'relay' => 'application'];
const ALTERNATIVES = ['source:news' => 'source:service', 'source:relay' => 'source:application',
'media:picture' => 'media:image', 'media:photo' => 'media:image',
'network:activitypub' => 'network:apub', 'network:friendica' => 'network:dfrn',
'network:diaspora' => 'network:dspr', 'network:ostatus' => 'network:stat',
'network:discourse' => 'network:dscs', 'network:tumblr' => 'network:tmbl', 'network:bluesky' => 'network:bsky'];
const MEDIA_NONE = 0;
const MEDIA_IMAGE = 1;
const MEDIA_VIDEO = 2;
const MEDIA_AUDIO = 4;
const MEDIA_CARD = 8;
const MEDIA_POST = 16;
/**
* Store engagement data from an item array
@ -53,8 +66,10 @@ class Engagement
return 0;
}
$parent = Post::selectFirst(['uri-id', 'created', 'author-id', 'owner-id', 'uid', 'private', 'contact-contact-type', 'language', 'network',
'title', 'content-warning', 'body', 'author-contact-type', 'author-nick', 'author-addr', 'author-gsid', 'owner-contact-type', 'owner-nick', 'owner-addr', 'owner-gsid'],
$parent = Post::selectFirst(['uri-id', 'created', 'uid', 'private', 'quote-uri-id',
'contact-contact-type', 'network', 'title', 'content-warning', 'body', 'language',
'author-id', 'author-contact-type', 'author-nick', 'author-addr', 'author-gsid',
'owner-id', 'owner-contact-type', 'owner-nick', 'owner-addr', 'owner-gsid'],
['uri-id' => $item['parent-uri-id']]);
if ($parent['created'] < self::getCreationDateLimit(false)) {
@ -82,16 +97,16 @@ class Engagement
}
}
$mediatype = self::getMediaType($item['parent-uri-id']);
$mediatype = self::getMediaType($item['parent-uri-id'], $parent['quote-uri-id']);
if (!$store) {
$store = !empty($mediatype);
}
$searchtext = self::getSearchTextForItem($parent);
$searchtext = self::getSearchTextForItem($parent, $mediatype);
$language = !empty($parent['language']) ? (array_key_first(json_decode($parent['language'], true)) ?? L10n::UNDETERMINED_LANGUAGE) : L10n::UNDETERMINED_LANGUAGE;
if (!$store) {
$language = !empty($parent['language']) ? (array_key_first(json_decode($parent['language'], true)) ?? '') : '';
$store = DI::userDefinedChannel()->match($searchtext, $language);
$store = DI::userDefinedChannel()->match($searchtext, $language);
}
$engagement = [
@ -99,9 +114,11 @@ class Engagement
'owner-id' => $parent['owner-id'],
'contact-type' => $parent['contact-contact-type'],
'media-type' => $mediatype,
'language' => $parent['language'],
'language' => $language,
'searchtext' => $searchtext,
'size' => self::getContentSize($parent),
'created' => $parent['created'],
'network' => $parent['network'],
'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]),
'activities' => DBA::count('post', [
@ -125,6 +142,18 @@ class Engagement
return ($ret && !$exists) ? $engagement['uri-id'] : 0;
}
public static function getContentSize(array $item): int
{
$body = ' ' . $item['title'] . ' ' . $item['content-warning'] . ' ' . $item['body'];
$body = BBCode::removeAttachment($body);
$body = BBCode::removeSharedData($body);
$body = preg_replace('/[^@!#]\[url\=.*?\].*?\[\/url\]/ism', '', $body);
$body = BBCode::removeLinks($body);
$msg = BBCode::toPlaintext($body, false);
return mb_strlen($msg);
}
public static function getSearchTextForActivity(string $content, int $author_id, array $tags, array $receivers): string
{
$author = Contact::getById($author_id);
@ -146,6 +175,7 @@ class Engagement
'owner-nick' => $author['nick'],
'owner-addr' => $author['addr'],
'owner-gsid' => $author['gsid'],
'quote-uri-id' => 0,
];
foreach ($receivers as $receiver) {
@ -154,7 +184,7 @@ class Engagement
}
}
return self::getSearchText($item, $receivers, $tags);
return self::getSearchText($item, $receivers, $tags, 0);
}
public static function getSearchTextForUriId(int $uri_id, bool $refresh = false): string
@ -167,28 +197,29 @@ class Engagement
}
$post = Post::selectFirstPost(['uri-id', 'network', 'title', 'content-warning', 'body', 'private',
'author-id', 'author-contact-type', 'author-nick', 'author-addr', 'author-gsid',
'author-id', 'author-contact-type', 'author-nick', 'author-addr', 'author-gsid', 'quote-uri-id',
'owner-id', 'owner-contact-type', 'owner-nick', 'owner-addr', 'owner-gsid'], ['uri-id' => $uri_id]);
if (empty($post['uri-id'])) {
return '';
}
return self::getSearchTextForItem($post);
$mediatype = self::getMediaType($uri_id, $post['quote-uri-id']);
return self::getSearchTextForItem($post, $mediatype);
}
private static function getSearchTextForItem(array $item): string
private static function getSearchTextForItem(array $item, int $mediatype): string
{
$receivers = array_column(Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION, Tag::AUDIENCE]), 'url');
$tags = array_column(Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]), 'name');
return self::getSearchText($item, $receivers, $tags);
return self::getSearchText($item, $receivers, $tags, $mediatype);
}
private static function getSearchText(array $item, array $receivers, array $tags): string
private static function getSearchText(array $item, array $receivers, array $tags, int $mediatype): string
{
$body = '[nosmile]network_' . $item['network'];
if (!empty($item['author-gsid'])) {
$gserver = DBA::selectFirst('gserver', ['platform', 'nurl'], ['id' => $item['author-gsid']]);
$platform = preg_replace( '/[\W]/', '', $gserver['platform'] ?? '');
$platform = preg_replace('/[\W]/', '', $gserver['platform'] ?? '');
if (!empty($platform)) {
$body .= ' platform_' . $platform;
}
@ -197,7 +228,7 @@ class Engagement
if (($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) && !empty($item['owner-gsid']) && ($item['owner-gsid'] != ($item['author-gsid'] ?? 0))) {
$gserver = DBA::selectFirst('gserver', ['platform', 'nurl'], ['id' => $item['owner-gsid']]);
$platform = preg_replace( '/[\W]/', '', $gserver['platform'] ?? '');
$platform = preg_replace('/[\W]/', '', $gserver['platform'] ?? '');
if (!empty($platform) && !strpos($body, 'platform_' . $platform)) {
$body .= ' platform_' . $platform;
}
@ -230,6 +261,8 @@ class Engagement
if ($item['author-contact-type'] == Contact::TYPE_COMMUNITY) {
$body .= ' group_' . $item['author-nick'] . ' group_' . $item['author-addr'];
} elseif ($item['author-contact-type'] == Contact::TYPE_RELAY) {
$body .= ' application_' . $item['author-nick'] . ' application_' . $item['author-addr'];
} elseif (in_array($item['author-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' from_' . $item['author-nick'] . ' from_' . $item['author-addr'];
}
@ -242,7 +275,12 @@ class Engagement
}
}
$body = self::addResharers($body, $item['uri-id']);
foreach ($receivers as $receiver) {
if (empty($receiver)) {
continue;
}
$contact = Contact::getByURL($receiver, false, ['nick', 'addr', 'contact-type']);
if (empty($contact)) {
continue;
@ -264,22 +302,71 @@ class Engagement
$body .= ' language_' . array_key_first($languages);
}
if ($mediatype & self::MEDIA_IMAGE) {
$body .= ' media_image';
}
if ($mediatype & self::MEDIA_VIDEO) {
$body .= ' media_video';
}
if ($mediatype & self::MEDIA_AUDIO) {
$body .= ' media_audio';
}
if ($mediatype & self::MEDIA_CARD) {
$body .= ' media_card';
}
if ($mediatype & self::MEDIA_POST) {
$body .= ' media_post';
}
$body .= ' ' . $item['title'] . ' ' . $item['content-warning'] . ' ' . $item['body'];
return BBCode::toSearchText($body, $item['uri-id']);
}
private static function getMediaType(int $uri_id): int
private static function addResharers(string $text, int $uri_id): string
{
$result = Post::selectPosts(['author-addr', 'author-nick', 'author-contact-type'],
['thr-parent-id' => $uri_id, 'gravity' => Item::GRAVITY_ACTIVITY, 'verb' => Activity::ANNOUNCE, 'author-contact-type' => [Contact::TYPE_RELAY, Contact::TYPE_COMMUNITY]]);
while ($reshare = Post::fetch($result)) {
switch ($reshare['author-contact-type']) {
case Contact::TYPE_RELAY:
$prefix = ' application_';
break;
case Contact::TYPE_COMMUNITY:
$prefix = ' group_';
break;
}
$nick = $prefix . $reshare['author-nick'];
$addr = $prefix . $reshare['author-addr'];
if (stripos($text, $addr) === false) {
$text .= $nick . $addr;
}
}
DBA::close($result);
return $text;
}
public static function getMediaType(int $uri_id, int $quote_uri_id = null): int
{
$media = Post\Media::getByURIId($uri_id);
$type = 0;
$type = !empty($quote_uri_id) ? self::MEDIA_POST : self::MEDIA_NONE;
foreach ($media as $entry) {
if ($entry['type'] == Post\Media::IMAGE) {
$type = $type | 1;
$type = $type | self::MEDIA_IMAGE;
} elseif ($entry['type'] == Post\Media::VIDEO) {
$type = $type | 2;
$type = $type | self::MEDIA_VIDEO;
} elseif ($entry['type'] == Post\Media::AUDIO) {
$type = $type | 4;
$type = $type | self::MEDIA_AUDIO;
} elseif ($entry['type'] == Post\Media::HTML) {
$type = $type | self::MEDIA_CARD;
} elseif ($entry['type'] == Post\Media::ACTIVITY) {
$type = $type | self::MEDIA_POST;
}
}
return $type;
@ -318,7 +405,15 @@ class Engagement
public static function escapeKeywords(string $fullTextSearch): string
{
foreach (Engagement::KEYWORDS as $keyword) {
foreach (SELF::SHORTCUTS as $search => $replace) {
$fullTextSearch = preg_replace('~' . $search . ':(.[\w\*@\.-]+)~', $replace . ':$1', $fullTextSearch);
}
foreach (SELF::ALTERNATIVES as $search => $replace) {
$fullTextSearch = str_replace($search, $replace, $fullTextSearch);
}
foreach (self::KEYWORDS as $keyword) {
$fullTextSearch = preg_replace('~(' . $keyword . '):(.[\w\*@\.-]+)~', '"$1_$2"', $fullTextSearch);
}
return $fullTextSearch;

View file

@ -31,6 +31,7 @@ use Friendica\Util\HTTPSignature;
use Friendica\Util\Images;
use Friendica\Util\Proxy;
use Friendica\Object\Image;
use Friendica\Util\Network;
/**
* Class Link
@ -77,7 +78,7 @@ class Link
} else {
$fields = self::fetchMimeType($url);
$fields['uri-id'] = $uriId;
$fields['url'] = $url;
$fields['url'] = Network::sanitizeUrl($url);
DBA::insert('post-link', $fields, Database::INSERT_IGNORE);
$id = DBA::lastInsertId();
@ -133,15 +134,23 @@ class Link
Logger::notice('Error fetching url', ['url' => $url, 'exception' => $exception]);
return [];
}
$fields = ['mimetype' => $curlResult->getHeader('Content-Type')[0]];
$img_str = $curlResult->getBodyString();
$image = new Image($img_str, Images::getMimeTypeByData($img_str));
if ($image->isValid()) {
$fields['mimetype'] = $image->getType();
$fields['width'] = $image->getWidth();
$fields['height'] = $image->getHeight();
$fields['blurhash'] = $image->getBlurHash();
if (!$curlResult->isSuccess()) {
Logger::notice('Fetching unsuccessful', ['url' => $url]);
return [];
}
$fields = ['mimetype' => $curlResult->getContentType()];
if (Images::isSupportedMimeType($fields['mimetype'])) {
$img_str = $curlResult->getBodyString();
$image = new Image($img_str, $fields['mimetype'], $url);
if ($image->isValid()) {
$fields['mimetype'] = $image->getType();
$fields['width'] = $image->getWidth();
$fields['height'] = $image->getHeight();
$fields['blurhash'] = $image->getBlurHash();
}
}
return $fields;

View file

@ -96,6 +96,7 @@ class Media
return false;
}
$media['url'] = Network::sanitizeUrl($media['url']);
$media = self::unsetEmptyFields($media);
$media = DI::dbaDefinition()->truncateFieldsForTable('post-media', $media);
@ -195,7 +196,7 @@ class Media
if ($curlResult->isSuccess()) {
if (empty($media['mimetype'])) {
$media['mimetype'] = $curlResult->getHeader('Content-Type')[0] ?? '';
$media['mimetype'] = $curlResult->getContentType() ?? '';
}
if (empty($media['size'])) {
$media['size'] = (int)($curlResult->getHeader('Content-Length')[0] ?? 0);
@ -364,7 +365,7 @@ class Media
*/
private static function addPage(array $media): array
{
$data = ParseUrl::getSiteinfoCached($media['url'], false);
$data = ParseUrl::getSiteinfoCached($media['url']);
$media['preview'] = $data['images'][0]['src'] ?? null;
$media['preview-height'] = $data['images'][0]['height'] ?? null;
$media['preview-width'] = $data['images'][0]['width'] ?? null;

View file

@ -21,10 +21,13 @@
namespace Friendica\Model\Post;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Util\DateTimeFormat;
@ -34,24 +37,30 @@ class SearchIndex
* Insert a post-searchindex entry
*
* @param int $uri_id
* @param string $network
* @param int $private
* @param string $created
* @param bool $refresh
*/
public static function insert(int $uri_id, string $network, int $private, string $created, bool $refresh = false)
public static function insert(int $uri_id, string $created, bool $refresh = false)
{
$limit = self::searchAgeDateLimit();
if (!empty($limit) && (strtotime($created) < strtotime($limit))) {
return;
}
$item = Post::selectFirstPost(['created', 'owner-id', 'private', 'language', 'network', 'title', 'content-warning', 'body', 'quote-uri-id'], ['uri-id' => $uri_id]);
if (empty($item)) {
return;
}
$search = [
'uri-id' => $uri_id,
'network' => $network,
'private' => $private,
'created' => $created,
'owner-id' => $item['owner-id'],
'media-type' => Engagement::getMediaType($uri_id, $item['quote-uri-id']),
'language' => substr(!empty($item['language']) ? (array_key_first(json_decode($item['language'], true)) ?? L10n::UNDETERMINED_LANGUAGE) : L10n::UNDETERMINED_LANGUAGE, 0, 2),
'searchtext' => Post\Engagement::getSearchTextForUriId($uri_id, $refresh),
'size' => Engagement::getContentSize($item),
'created' => $item['created'],
'restricted' => !in_array($item['network'], Protocol::FEDERATED) || ($item['private'] != Item::PUBLIC),
];
return DBA::insert('post-searchindex', $search, Database::INSERT_UPDATE);
}
@ -63,7 +72,7 @@ class SearchIndex
*/
public static function update(int $uri_id)
{
$searchtext = Post\Engagement::getSearchTextForUriId($uri_id, true);
$searchtext = Post\Engagement::getSearchTextForUriId($uri_id);
return DBA::update('post-searchindex', ['searchtext' => $searchtext], ['uri-id' => $uri_id]);
}

View file

@ -378,6 +378,18 @@ class Tag
return DBA::exists('post-tag', ['uri-id' => $uriId, 'type' => [self::HASHTAG, self::MENTION, self::EXCLUSIVE_MENTION, self::IMPLICIT_MENTION]]);
}
/**
* Check for a given hashtag on a given post
*
* @param integer $uriId
* @param string $tag
* @return boolean
*/
public static function existsTagForPost(int $uriId, string $tag): bool
{
return DBA::exists('tag-view', ['uri-id' => $uriId, 'type' => self::HASHTAG, 'name' => $tag]);
}
/**
* Remove tag/mention
*
@ -546,7 +558,7 @@ class Tag
);
while ($tag = DBA::fetch($taglist)) {
if ($tag['url'] == '') {
$tag['url'] = $searchpath . rawurlencode($tag['name']);
$tag['url'] = $searchpath . urlencode($tag['name']);
}
$orig_tag = $tag['url'];

View file

@ -127,6 +127,9 @@ class User
case 'community':
return self::ACCOUNT_TYPE_COMMUNITY;
case 'relay':
return self::ACCOUNT_TYPE_RELAY;
}
return null;
}
@ -477,7 +480,7 @@ class User
// Check for correct url and normalised nurl
$url = DI::baseUrl() . '/profile/' . $owner['nickname'];
$repair = empty($owner['network']) || ($owner['url'] != $url) || ($owner['nurl'] != Strings::normaliseLink($owner['url']));
$repair = empty($owner['baseurl']) || empty($owner['network']) || ($owner['url'] != $url) || ($owner['nurl'] != Strings::normaliseLink($owner['url']));
if (!$repair) {
// Check if "addr" is present and correct
@ -823,26 +826,30 @@ class User
/**
* Update the day of the last activity of the given user
*
* @param integer $uid
* @param array $user
* @param bool $refresh_login
* @return void
*/
public static function updateLastActivity(int $uid)
public static function updateLastActivity(array $user, bool $refresh_login)
{
if (!$uid) {
return;
}
$user = self::getById($uid, ['last-activity']);
if (empty($user)) {
return;
}
$current_day = DateTimeFormat::utcNow('Y-m-d');
if (($user['last-activity'] == $current_day) && (!$refresh_login || DateTimeFormat::utc($user['login_date'], 'z-H') == DateTimeFormat::utcNow('z-H'))) {
return;
}
if ($user['last-activity'] != $current_day) {
self::update(['last-activity' => $current_day], $uid);
// Set the last activity for all identities of the user
DBA::update('user', ['last-activity' => $current_day], ['parent-uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
$fields = ['last-activity' => $current_day];
if ($refresh_login) {
$fields['login_date'] = DateTimeFormat::utcNow();
}
Logger::debug('Set last activity for user', ['uid' => $user['uid'], 'fields' => $fields]);
self::update($fields, $user['uid']);
// Set the last activity for all identities of the user
DBA::update('user', $fields, ['parent-uid' => $user['uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!empty($user['parent-uid'])) {
self::update($fields, $user['parent-uid']);
DBA::update('user', $fields, ['parent-uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
}
}
@ -1400,9 +1407,7 @@ class User
$type = '';
}
$type = Images::getMimeTypeByData($img_str, $photo, $type);
$image = new Image($img_str, $type);
$image = new Image($img_str, $type, $photo);
if ($image->isValid()) {
$image->scaleToSquare(300);

View file

@ -30,6 +30,8 @@ class PhpInfo extends BaseAdmin
{
self::checkAdminAccess();
self::checkFormSecurityTokenForbiddenOnError('phpinfo', 't');
phpinfo();
System::exit();
}

View file

@ -80,6 +80,7 @@ class Site extends BaseAdmin
$allowed_sites = (!empty($_POST['allowed_sites']) ? trim($_POST['allowed_sites']) : '');
$allowed_email = (!empty($_POST['allowed_email']) ? trim($_POST['allowed_email']) : '');
$disallowed_email = (!empty($_POST['disallowed_email']) ? trim($_POST['disallowed_email']) : '');
$forbidden_nicknames = (!empty($_POST['forbidden_nicknames']) ? strtolower(trim($_POST['forbidden_nicknames'])) : '');
$system_actor_name = (!empty($_POST['system_actor_name']) ? trim($_POST['system_actor_name']) : '');
$no_oembed_rich_content = !empty($_POST['no_oembed_rich_content']);
@ -255,6 +256,7 @@ class Site extends BaseAdmin
$transactionConfig->set('config', 'register_text' , $register_text);
$transactionConfig->set('system', 'allowed_sites' , $allowed_sites);
$transactionConfig->set('system', 'allowed_email' , $allowed_email);
$transactionConfig->set('system', 'disallowed_email' , $disallowed_email);
$transactionConfig->set('system', 'forbidden_nicknames' , $forbidden_nicknames);
$transactionConfig->set('system', 'system_actor_name' , $system_actor_name);
$transactionConfig->set('system', 'no_oembed_rich_content' , $no_oembed_rich_content);
@ -505,6 +507,7 @@ class Site extends BaseAdmin
'$abandon_days' => ['abandon_days', DI::l10n()->t('Accounts abandoned after x days'), DI::config()->get('system', 'account_abandon_days'), DI::l10n()->t('Will not waste system resources polling external sites for abandonded accounts. Enter 0 for no time limit.')],
'$allowed_sites' => ['allowed_sites', DI::l10n()->t('Allowed friend domains'), DI::config()->get('system', 'allowed_sites'), DI::l10n()->t('Comma separated list of domains which are allowed to establish friendships with this site. Wildcards are accepted. Empty to allow any domains')],
'$allowed_email' => ['allowed_email', DI::l10n()->t('Allowed email domains'), DI::config()->get('system', 'allowed_email'), DI::l10n()->t('Comma separated list of domains which are allowed in email addresses for registrations to this site. Wildcards are accepted. Empty to allow any domains')],
'$disallowed_email' => ['disallowed_email', DI::l10n()->t('Disallowed email domains'), DI::config()->get('system', 'disallowed_email'), DI::l10n()->t('Comma separated list of domains which are rejected as email addresses for registrations to this site. Wildcards are accepted.')],
'$no_oembed_rich_content' => ['no_oembed_rich_content', DI::l10n()->t('No OEmbed rich content'), DI::config()->get('system', 'no_oembed_rich_content'), DI::l10n()->t('Don\'t show the rich content (e.g. embedded PDF), except from the domains listed below.')],
'$allowed_oembed' => ['allowed_oembed', DI::l10n()->t('Trusted third-party domains'), DI::config()->get('system', 'allowed_oembed'), DI::l10n()->t('Comma separated list of domains from which content is allowed to be embedded in posts like with OEmbed. All sub-domains of the listed domains are allowed as well.')],
'$block_public' => ['block_public', DI::l10n()->t('Block public'), DI::config()->get('system', 'block_public'), DI::l10n()->t('Check to block public access to all otherwise public personal pages on this site unless you are currently logged in.')],

View file

@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\Protocol;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/#lookup
*/
class Lookup extends BaseApi
{
/**
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
protected function rawContent(array $request = [])
{
$this->checkAllowedScope(self::SCOPE_READ);
$uid = self::getCurrentUserID();
$request = $this->getRequest([
'acct' => '', // The username or Webfinger address to lookup.
], $request);
if (empty($request['acct'])) {
$this->logAndJsonError(404, $this->errorFactory->RecordNotFound());
}
$contact = Contact::getByURL($request['acct'], null, ['id', 'network', 'failed', 'blocked']);
if (empty($contact) || ($contact['network'] == Protocol::PHANTOM) || $contact['failed'] || $contact['blocked']) {
$this->logAndJsonError(404, $this->errorFactory->RecordNotFound());
}
$this->jsonExit(DI::mstdnAccount()->createFromContactId($contact['id'], $uid));
}
}

View file

@ -21,7 +21,6 @@
namespace Friendica\Module\Api\Mastodon\Apps;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\BaseApi;

View file

@ -21,7 +21,6 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;

View file

@ -38,7 +38,7 @@ use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/**
* @see https://docs.joinmastodon.org/api/rest/instances/
* @see https://docs.joinmastodon.org/methods/instance/
*/
class Instance extends BaseApi
{
@ -95,7 +95,7 @@ class Instance extends BaseApi
return new InstanceV2Entity\Configuration(
$statuses_config,
new InstanceV2Entity\MediaAttachmentsConfig(array_keys(Images::supportedTypes()), $image_size_limit, $image_matrix_limit),
new InstanceV2Entity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit),
new InstanceV2Entity\Polls(),
new InstanceV2Entity\Accounts(),
);

View file

@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Instance;
use DateTime;
use Friendica\App;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Model\User;
use Friendica\Module\Api\ApiResponse;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
use Friendica\Object\Api\Mastodon;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
/**
* @see https://docs.joinmastodon.org/methods/instance/#extended_description
*/
class ExtendedDescription extends BaseApi
{
private IManageConfigValues $config;
public function __construct(\Friendica\Factory\Api\Mastodon\Error $errorFactory, App $app, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, ApiResponse $response, IManageConfigValues $config, array $server, array $parameters = [])
{
parent::__construct($errorFactory, $app, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->config = $config;
}
/**
* @throws HTTPException\InternalServerErrorException
*/
protected function rawContent(array $request = [])
{
$account = User::getSystemAccount();
$this->jsonExit(new Mastodon\ExtendedDescription(new DateTime($account['updated']), $this->config->get('config', 'info')));
}
}

View file

@ -22,7 +22,6 @@
namespace Friendica\Module\Api\Mastodon\Instance;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\GServer;
use Friendica\Module\BaseApi;

View file

@ -21,10 +21,7 @@
namespace Friendica\Module\Api\Mastodon\Instance;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;

View file

@ -131,7 +131,7 @@ class InstanceV2 extends BaseApi
return new InstanceEntity\Configuration(
$statuses_config,
new InstanceEntity\MediaAttachmentsConfig(array_keys(Images::supportedTypes()), $image_size_limit, $image_matrix_limit),
new InstanceEntity\MediaAttachmentsConfig(Images::supportedMimeTypes(), $image_size_limit, $image_matrix_limit),
new InstanceEntity\Polls(),
new InstanceEntity\Accounts(),
);

View file

@ -21,7 +21,6 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
@ -39,15 +38,6 @@ class Mutes extends BaseApi
$this->checkAllowedScope(self::SCOPE_READ);
$uid = self::getCurrentUserID();
if (empty($this->parameters['id'])) {
$this->logAndJsonError(422, $this->errorFactory->UnprocessableEntity());
}
$id = $this->parameters['id'];
if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) {
$this->logAndJsonError(404, $this->errorFactory->RecordNotFound());
}
$request = $this->getRequest([
'max_id' => 0, // Return results older than this id
'since_id' => 0, // Return results newer than this id
@ -57,7 +47,7 @@ class Mutes extends BaseApi
$params = ['order' => ['cid' => true], 'limit' => $request['limit']];
$condition = ['cid' => $id, 'ignored' => true, 'uid' => $uid];
$condition = ['ignored' => true, 'uid' => $uid];
if (!empty($request['max_id'])) {
$condition = DBA::mergeConditions($condition, ["`cid` < ?", $request['max_id']]);
@ -74,6 +64,7 @@ class Mutes extends BaseApi
}
$followers = DBA::select('user-contact', ['cid'], $condition, $params);
$accounts = [];
while ($follower = DBA::fetch($followers)) {
self::setBoundaries($follower['cid']);
$accounts[] = DI::mstdnAccount()->createFromContactId($follower['cid'], $uid);

View file

@ -21,7 +21,6 @@
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;

View file

@ -137,6 +137,10 @@ class Search extends BaseApi
private static function searchStatuses(int $uid, string $q, string $account_id, int $max_id, int $min_id, int $limit, int $offset)
{
if (Network::isValidHttpUrl($q)) {
// Unique post search, any offset greater than 0 should return empty result
if ($offset > 0) {
return [];
}
$q = Network::convertToIdn($q);
// If the user-specific search failed, we search and probe a public post
$item_id = Item::fetchByLink($q, $uid) ?: Item::fetchByLink($q);
@ -154,10 +158,14 @@ class Search extends BaseApi
$table = 'tag-search-view';
} else {
$q = Post\Engagement::escapeKeywords($q);
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) and (private = ? OR `uri-id` in (SELECT `uri-id` FROM `post-user` where `uid` = ?))", $q, Item::PUBLIC, $uid];
$condition = ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE) AND (NOT `restricted` OR `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `uid` = ?))", $q, $uid];
$table = 'post-searchindex';
}
if (!empty($account_id)) {
$condition = DBA::mergeConditions($condition, ["`author-id` = ?", $account_id]);
}
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
}

View file

@ -55,6 +55,7 @@ class Statuses extends BaseApi
'status' => '', // Text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.
'media_ids' => [], // Array of Attachment ids to be attached as media. If provided, status becomes optional, and poll cannot be used.
'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply
'sensitive' => false, // Mark status and attached media as sensitive? Defaults to false.
'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.
'language' => '', // ISO 639 language code for this status.
'media_attributes' => [],
@ -83,6 +84,7 @@ class Statuses extends BaseApi
$item['gravity'] = $post['gravity'];
$item['verb'] = $post['verb'];
$item['app'] = $this->getApp();
$item['sensitive'] = $request['sensitive'];
if (!empty($request['language'])) {
$item['language'] = json_encode([$request['language'] => 1]);
@ -179,7 +181,7 @@ class Statuses extends BaseApi
'poll' => [], // Poll data. If provided, media_ids cannot be used, and poll[expires_in] must be provided.
'in_reply_to_id' => 0, // ID of the status being replied to, if status is a reply
'quote_id' => 0, // ID of the message to quote
'sensitive' => false, // Mark status and attached media as sensitive?
'sensitive' => false, // Mark status and attached media as sensitive? Defaults to false.
'spoiler_text' => '', // Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.
'visibility' => '', // Visibility of the posted status. One of: "public", "unlisted", "private" or "direct".
'scheduled_at' => '', // ISO 8601 Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future.
@ -198,6 +200,7 @@ class Statuses extends BaseApi
$item['title'] = '';
$item['body'] = $this->formatStatus($request['status'], $uid);
$item['app'] = $this->getApp();
$item['sensitive'] = $request['sensitive'];
$item['visibility'] = $request['visibility'];
switch ($request['visibility']) {
@ -400,11 +403,10 @@ class Statuses extends BaseApi
Photo::setPermissionForResource($media[0]['resource-id'], $item['uid'], $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
$phototypes = Images::supportedTypes();
$ext = $phototypes[$media[0]['type']];
$ext = Images::getExtensionByMimeType($media[0]['type']);
$attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'],
'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext,
'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . $ext,
'size' => $media[0]['datasize'],
'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
'description' => $media[0]['desc'] ?? '',
@ -412,7 +414,7 @@ class Statuses extends BaseApi
'height' => $media[0]['height']];
if (count($media) > 1) {
$attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext;
$attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . $ext;
$attachment['preview-width'] = $media[1]['width'];
$attachment['preview-height'] = $media[1]['height'];
}

View file

@ -129,7 +129,11 @@ class Context extends BaseApi
$display_quotes = self::appSupportsQuotes();
foreach (array_slice($ancestors, 0, $request['limit']) as $ancestor) {
$statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid, $display_quotes);
try {
$statuses['ancestors'][] = DI::mstdnStatus()->createFromUriId($ancestor, $uid, $display_quotes);
} catch (\Throwable $th) {
$this->logger->info('Post not fetchable', ['uri-id' => $ancestor, 'uid' => $uid, 'error' => $th]);
}
}
$descendants = array_diff(self::getChildren($id, $children), $deleted);
@ -137,7 +141,11 @@ class Context extends BaseApi
asort($descendants);
foreach (array_slice($descendants, 0, $request['limit']) as $descendant) {
$statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid, $display_quotes);
try {
$statuses['descendants'][] = DI::mstdnStatus()->createFromUriId($descendant, $uid, $display_quotes);
} catch (\Throwable $th) {
$this->logger->info('Post not fetchable', ['uri-id' => $descendant, 'uid' => $uid, 'error' => $th]);
}
}
$this->jsonExit($statuses);

View file

@ -155,13 +155,12 @@ class Update extends BaseApi
Photo::setPermissionForResource($media[0]['resource-id'], $uid, $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
$phototypes = Images::supportedTypes();
$ext = $phototypes[$media[0]['type']];
$ext = Images::getExtensionByMimeType($media[0]['type']);
$attachment = [
'type' => Post\Media::IMAGE,
'mimetype' => $media[0]['type'],
'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext,
'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . $ext,
'size' => $media[0]['datasize'],
'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
'description' => $media[0]['desc'] ?? '',
@ -170,7 +169,7 @@ class Update extends BaseApi
];
if (count($media) > 1) {
$attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext;
$attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . $ext;
$attachment['preview-width'] = $media[1]['width'];
$attachment['preview-height'] = $media[1]['height'];
}

View file

@ -65,11 +65,7 @@ class Attach extends BaseModule
// error in Chrome for filenames with commas in them
header('Content-type: ' . $item['filetype']);
header('Content-length: ' . $item['filesize']);
if (isset($_GET['attachment']) && $_GET['attachment'] === '0') {
header('Content-disposition: filename="' . $item['filename'] . '"');
} else {
header('Content-disposition: attachment; filename="' . $item['filename'] . '"');
}
header('Content-disposition: attachment; filename="' . $item['filename'] . '"');
echo $data;
System::exit();

View file

@ -104,7 +104,7 @@ abstract class BaseAdmin extends BaseModule
'logsview' => ['admin/logs/view' , DI::l10n()->t('View Logs') , 'viewlogs'],
]],
'diagnostics' => [DI::l10n()->t('Diagnostics'), [
'phpinfo' => ['admin/phpinfo' , DI::l10n()->t('PHP Info') , 'phpinfo'],
'phpinfo' => ['admin/phpinfo?t=' . self::getFormSecurityToken('phpinfo'), DI::l10n()->t('PHP Info') , 'phpinfo'],
'probe' => ['probe' , DI::l10n()->t('probe address') , 'probe'],
'webfinger' => ['webfinger' , DI::l10n()->t('check webfinger') , 'webfinger'],
'babel' => ['babel' , DI::l10n()->t('Babel') , 'babel'],

View file

@ -142,7 +142,8 @@ class API extends BaseModule
{
$eventId = !empty($request['event_id']) ? intval($request['event_id']) : 0;
$uid = (int)$this->session->getLocalUserId();
$cid = !empty($request['cid']) ? intval($request['cid']) : 0;
// No overwriting event.cid on edit
$cid = !empty($request['cid']) && !$eventId ? intval($request['cid']) : 0;
$strStartDateTime = Strings::escapeHtml($request['start_text'] ?? '');
$strFinishDateTime = Strings::escapeHtml($request['finish_text'] ?? '');

View file

@ -27,11 +27,13 @@ use Friendica\Contact\LocalRelationship\Repository\LocalRelationship;
use Friendica\Content\Conversation;
use Friendica\Content\Nav;
use Friendica\Content\Widget;
use Friendica\Core\ACL;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Core\Theme;
use Friendica\Model;
use Friendica\Model\Contact as ModelContact;
use Friendica\Module\Contact;
use Friendica\Module\Response;
use Friendica\Module\Security\Login;
@ -105,12 +107,17 @@ class Conversations extends BaseModule
$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'] .= Widget\VCard::getHTML($contact);
$this->page['aside'] .= Widget\VCard::getHTML($contact, true);
Nav::setSelected('contact');
// We need the editor here to be able to reshare an item.
$o = $this->conversation->statusEditor([], 0, true);
$options = [
'lockstate' => ACL::getLockstateForUserId($this->userSession->getLocalUserId()) ? 'lock' : 'unlock',
'acl' => ACL::getFullSelectorHTML($this->page, $this->userSession->getLocalUserId(), true, []),
'bang' => '',
'content' => ($contact['contact-type'] == ModelContact::TYPE_COMMUNITY ? '!' : '@') . ($contact['addr'] ?: $contact['url']),
];
$o = $this->conversation->statusEditor($options);
$o .= Contact::getTabsHTML($contact, Contact::TAB_CONVERSATIONS);
$o .= Model\Contact::getThreadsFromId($contact['id'], $this->userSession->getLocalUserId(), 0, 0, $request['last_created'] ?? '');

View file

@ -128,7 +128,7 @@ class Channel extends Timeline
}
if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$items = $this->getChannelItems();
$items = $this->getChannelItems($request);
$order = 'created';
} else {
$items = $this->getCommunityItems();

View file

@ -65,8 +65,6 @@ class Network extends Timeline
/** @var int */
protected $circleId;
/** @var string */
protected $network;
/** @var string */
protected $dateFrom;
/** @var string */
protected $dateTo;
@ -74,8 +72,6 @@ class Network extends Timeline
protected $star;
/** @var int */
protected $mention;
/** @var string */
protected $order;
/** @var App */
protected $app;
@ -162,8 +158,6 @@ class Network extends Timeline
Nav::setSelected($this->args->get(0));
$content = '';
$default_permissions = [];
if ($this->circleId) {
$default_permissions['allow_gid'] = [$this->circleId];
@ -195,7 +189,7 @@ class Network extends Timeline
'lockstate' => $this->circleId || $this->network || ACL::getLockstateForUserId($this->session->getLocalUserId()) ? 'lock' : 'unlock',
'acl' => ACL::getFullSelectorHTML($this->page, $this->session->getLocalUserId(), true, $default_permissions),
'bang' => (($this->circleId || $this->network) ? '!' : ''),
'content' => $content,
'content' => '',
];
$o .= $this->conversation->statusEditor($x);
@ -217,7 +211,7 @@ class Network extends Timeline
try {
if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$items = $this->getChannelItems();
$items = $this->getChannelItems($request);
} elseif ($this->community->isTimeline($this->selectedTab)) {
$items = $this->getCommunityItems();
} else {
@ -362,24 +356,7 @@ class Network extends Timeline
$this->dateFrom = $this->parameters['from'] ?? '';
$this->dateTo = $this->parameters['to'] ?? '';
switch ($this->order) {
case 'received':
$this->maxId = $request['last_received'] ?? $this->maxId;
$this->minId = $request['first_received'] ?? $this->minId;
break;
case 'created':
$this->maxId = $request['last_created'] ?? $this->maxId;
$this->minId = $request['first_created'] ?? $this->minId;
break;
case 'uriid':
$this->maxId = $request['last_uriid'] ?? $this->maxId;
$this->minId = $request['first_uriid'] ?? $this->minId;
break;
default:
$this->order = 'commented';
$this->maxId = $request['last_commented'] ?? $this->maxId;
$this->minId = $request['first_commented'] ?? $this->minId;
}
$this->setMaxMinByOrder($request);
}
protected function getItems()

View file

@ -26,6 +26,7 @@ use Friendica\App\Mode;
use Friendica\BaseModule;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Content\Conversation\Entity\UserDefinedChannel as UserDefinedChannelEntity;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Cache\Enum\Duration;
@ -41,7 +42,6 @@ use Friendica\Database\DBA;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\Post\Engagement;
use Friendica\Model\Verb;
use Friendica\Module\Response;
use Friendica\Protocol\Activity;
use Friendica\Util\DateTimeFormat;
@ -72,6 +72,10 @@ class Timeline extends BaseModule
protected $update;
/** @var bool */
protected $raw;
/** @var string */
protected $order;
/** @var string */
protected $network;
/** @var App\Mode $mode */
protected $mode;
@ -138,6 +142,8 @@ class Timeline extends BaseModule
$this->itemUriId = 0;
}
$this->order = 'created';
$this->minId = $request['min_id'] ?? null;
$this->maxId = $request['max_id'] ?? null;
@ -147,6 +153,28 @@ class Timeline extends BaseModule
$this->raw = !empty($request['mode']) && ($request['mode'] == 'raw');
}
protected function setMaxMinByOrder(array $request)
{
switch ($this->order) {
case 'received':
$this->maxId = $request['last_received'] ?? $this->maxId;
$this->minId = $request['first_received'] ?? $this->minId;
break;
case 'created':
$this->maxId = $request['last_created'] ?? $this->maxId;
$this->minId = $request['first_created'] ?? $this->minId;
break;
case 'uriid':
$this->maxId = $request['last_uriid'] ?? $this->maxId;
$this->minId = $request['first_uriid'] ?? $this->minId;
break;
default:
$this->order = 'commented';
$this->maxId = $request['last_commented'] ?? $this->maxId;
$this->minId = $request['first_commented'] ?? $this->minId;
}
}
protected function getNoSharerWidget(string $base): string
{
$path = $this->selectedTab;
@ -204,9 +232,9 @@ class Timeline extends BaseModule
* @return array
* @throws \Exception
*/
protected function getChannelItems()
protected function getChannelItems(array $request)
{
$items = $this->getRawChannelItems();
$items = $this->getRawChannelItems($request);
$contacts = $this->database->selectToArray('user-contact', ['cid'], ['channel-frequency' => Contact\User::FREQUENCY_REDUCED, 'cid' => array_column($items, 'owner-id')]);
$reduced = array_column($contacts, 'cid');
@ -220,8 +248,8 @@ class Timeline extends BaseModule
while (count($selected_items) < $this->itemsPerPage && ++$count < 50 && count($items) > 0) {
$maxposts = round((count($items) / $this->itemsPerPage) * $maxpostperauthor);
$minId = $items[array_key_first($items)]['created'];
$maxId = $items[array_key_last($items)]['created'];
$minId = $items[array_key_first($items)][$this->order];
$maxId = $items[array_key_last($items)][$this->order];
foreach ($items as $item) {
if (!in_array($item['owner-id'], $reduced)) {
@ -252,7 +280,7 @@ class Timeline extends BaseModule
}
if (count($selected_items) < $this->itemsPerPage) {
$items = $this->getRawChannelItems();
$items = $this->getRawChannelItems($request);
}
}
} else {
@ -271,10 +299,12 @@ class Timeline extends BaseModule
* @return array
* @throws \Exception
*/
private function getRawChannelItems()
private function getRawChannelItems(array $request)
{
$uid = $this->session->getLocalUserId();
$table = 'post-engagement';
if ($this->selectedTab == ChannelEntity::WHATSHOT) {
if (!is_null($this->accountType)) {
$condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` = ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $this->accountType];
@ -317,6 +347,13 @@ class Timeline extends BaseModule
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->getMedianRelationThreadScore($cid, 4), $cid
];
} elseif ($this->selectedTab == ChannelEntity::QUIETSHARERS) {
$cid = Contact::getPublicIdByUserId($uid);
$condition = [
"`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `post-score` <= ?)",
$cid, $this->getMedianPostScore($cid, 2)
];
} elseif ($this->selectedTab == ChannelEntity::IMAGE) {
$condition = ["`media-type` & ?", 1];
} elseif ($this->selectedTab == ChannelEntity::VIDEO) {
@ -324,48 +361,62 @@ class Timeline extends BaseModule
} elseif ($this->selectedTab == ChannelEntity::AUDIO) {
$condition = ["`media-type` & ?", 4];
} elseif ($this->selectedTab == ChannelEntity::LANGUAGE) {
$condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", User::getLanguageCode($uid)];
} elseif (is_numeric($this->selectedTab)) {
$condition = $this->getUserChannelConditions($this->selectedTab, $uid);
$condition = ["`language` = ?", User::getLanguageCode($uid)];
} elseif (is_numeric($this->selectedTab) && !empty($channel = $this->channelRepository->selectById($this->selectedTab, $uid))) {
$condition = $this->getUserChannelConditions($channel, $uid);
if (in_array($channel->circle, [-3, -4, -5])) {
$table = 'post-searchindex-user-view';
$condition = DBA::mergeConditions($condition, ['uid' => $uid]);
$orders = ['-3' => 'created', '-4' => 'received', '-5' => 'commented'];
$this->order = $orders[$channel->circle];
}
}
$this->setMaxMinByOrder($request);
if (!empty($this->network)) {
$condition = DBA::mergeConditions($condition, ['network' => $this->network]);
}
if (($this->selectedTab != ChannelEntity::LANGUAGE) && !is_numeric($this->selectedTab)) {
$condition = $this->addLanguageCondition($uid, $condition);
}
$condition = DBA::mergeConditions($condition, ["(NOT `restricted` OR EXISTS(SELECT `id` FROM `post-user` WHERE `uid` = ? AND `uri-id` = `post-engagement`.`uri-id`))", $uid]);
$condition = DBA::mergeConditions($condition, ["(NOT `restricted` OR EXISTS(SELECT `id` FROM `post-user` WHERE `uid` = ? AND `uri-id` = `$table`.`uri-id`))", $uid]);
$condition = DBA::mergeConditions($condition, ["NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed` OR `is-blocked` OR `channel-frequency` = ?))", $uid, Contact\User::FREQUENCY_NEVER]);
$condition = DBA::mergeConditions($condition, ["NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `$table`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed` OR `is-blocked` OR `channel-frequency` = ?))", $uid, Contact\User::FREQUENCY_NEVER]);
if (($this->selectedTab != ChannelEntity::WHATSHOT) && !is_null($this->accountType)) {
$condition = DBA::mergeConditions($condition, ['contact-type' => $this->accountType]);
}
$params = ['order' => ['created' => true], 'limit' => $this->itemsPerPage];
$params = ['order' => [$this->order => true], 'limit' => $this->itemsPerPage];
if (!empty($this->itemUriId)) {
$condition = DBA::mergeConditions($condition, ['uri-id' => $this->itemUriId]);
} else {
if ($this->noSharer) {
$condition = DBA::mergeConditions($condition, ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-engagement`.`uri-id`)", $this->session->getLocalUserId()]);
$condition = DBA::mergeConditions($condition, ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `$table`.`uri-id`)", $this->session->getLocalUserId()]);
}
if (isset($this->maxId)) {
$condition = DBA::mergeConditions($condition, ["`created` < ?", $this->maxId]);
$condition = DBA::mergeConditions($condition, ["`$this->order` < ?", $this->maxId]);
}
if (isset($this->minId)) {
$condition = DBA::mergeConditions($condition, ["`created` > ?", $this->minId]);
$condition = DBA::mergeConditions($condition, ["`$this->order` > ?", $this->minId]);
// Previous page case: we want the items closest to min_id but for that we need to reverse the query order
if (!isset($this->maxId)) {
$params['order']['created'] = false;
$params['order'][$this->order] = false;
}
}
}
$items = [];
$result = $this->database->select('post-engagement', ['uri-id', 'created', 'owner-id', 'comments', 'activities'], $condition, $params);
$fields = ['uri-id', 'owner-id', 'comments', 'activities'];
$fields[] = $this->order;
$result = $this->database->select($table, $fields, $condition, $params);
if ($this->database->errorNo()) {
throw new \Exception($this->database->errorMessage(), $this->database->errorNo());
}
@ -390,13 +441,8 @@ class Timeline extends BaseModule
return $items;
}
private function getUserChannelConditions(int $id, int $uid): array
private function getUserChannelConditions(UserDefinedChannelEntity $channel, int $uid): array
{
$channel = $this->channelRepository->selectById($id, $uid);
if (empty($channel)) {
return [];
}
$condition = [];
if (!empty($channel->circle)) {
@ -404,47 +450,127 @@ class Timeline extends BaseModule
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?))", $uid, Contact::SHARING, Contact::FRIEND];
} elseif ($channel->circle == -2) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER];
} elseif ($channel->circle == -3) {
$condition = ["EXISTS(SELECT `uri-id` FROM `post-thread-user` WHERE `uid` = ? AND `post-thread-user`.`uri-id` = `post-engagement`.`uri-id`)", $uid];
} 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]);
}
}
if (!empty($channel->fullTextSearch)) {
$condition = DBA::mergeConditions($condition, ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE)", Engagement::escapeKeywords($channel->fullTextSearch)]);
if (!empty($channel->includeTags)) {
$additional = self:: addIncludeTags($channel->includeTags);
} else {
$additional = '';
}
if (!empty($channel->excludeTags)) {
foreach (explode(',', mb_strtolower($channel->excludeTags)) as $tag) {
$additional .= ' -tag:' . $tag;
}
}
if (!empty($channel->mediaType)) {
$additional .= self::addMediaTerms($channel->mediaType);
}
$additional .= self::addLanguageSearchTerms($uid, $channel->languages);
if ($additional) {
$searchterms = '+(' . trim($channel->fullTextSearch) . ')' . $additional;
} else {
$searchterms = $channel->fullTextSearch;
}
$condition = DBA::mergeConditions($condition, ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE)", Engagement::escapeKeywords($searchterms)]);
} else {
if (!empty($channel->includeTags)) {
$search = explode(',', mb_strtolower($channel->includeTags));
$placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
$condition = DBA::mergeConditions($condition, array_merge(["`uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
}
if (!empty($channel->excludeTags)) {
$search = explode(',', mb_strtolower($channel->excludeTags));
$placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
$condition = DBA::mergeConditions($condition, array_merge(["NOT `uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
}
if (!empty($channel->mediaType)) {
$condition = DBA::mergeConditions($condition, ["`media-type` & ?", $channel->mediaType]);
}
// For "addLanguageCondition" to work, the condition must not be empty
$condition = $this->addLanguageCondition($uid, $condition ?: ["true"], $channel->languages);
}
if (!empty($channel->includeTags)) {
$search = explode(',', mb_strtolower($channel->includeTags));
$placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
$condition = DBA::mergeConditions($condition, array_merge(["`uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
if (!is_null($channel->minSize)) {
$condition = DBA::mergeConditions($condition, ["`size` >= ?", $channel->minSize]);
}
if (!empty($channel->excludeTags)) {
$search = explode(',', mb_strtolower($channel->excludeTags));
$placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
$condition = DBA::mergeConditions($condition, array_merge(["NOT `uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
if (!is_null($channel->maxSize)) {
$condition = DBA::mergeConditions($condition, ["`size` <= ?", $channel->maxSize]);
}
if (!empty($channel->mediaType)) {
$condition = DBA::mergeConditions($condition, ["`media-type` & ?", $channel->mediaType]);
}
// For "addLanguageCondition" to work, the condition must not be empty
$condition = $this->addLanguageCondition($uid, $condition ?: ["true"], $channel->languages);
return $condition;
}
private function addIncludeTags(string $includeTags): string
{
$tagterms = '';
foreach (explode(',', mb_strtolower($includeTags)) as $tag) {
$tagterms .= ' tag:' . $tag;
}
if ($tagterms) {
return ' +(' . trim($tagterms) . ')';
} else {
return '';
}
}
private function addMediaTerms(int $mediaType): string
{
$mediaterms = '';
if ($mediaType & 1) {
$mediaterms .= ' media:image';
}
if ($mediaType & 2) {
$mediaterms .= ' media:video';
}
if ($mediaType & 4) {
$mediaterms .= ' media:audio';
}
if ($mediaterms) {
return ' +(' . trim($mediaterms) . ')';
} else {
return '';
}
}
private function addLanguageSearchTerms(int $uid, $languages = null): string
{
$langterms = '';
foreach ($languages ?: User::getWantedLanguages($uid) as $language) {
$langterms .= ' language:' . $language;
}
if ($langterms) {
return ' +(' . trim($langterms) . ')';
} else {
return '';
}
}
private function addLanguageCondition(int $uid, array $condition, $languages = null): array
{
$conditions = [];
$languages = $languages ?: User::getWantedLanguages($uid);
foreach ($languages as $language) {
$conditions[] = "JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?";
foreach ($languages ?: User::getWantedLanguages($uid) as $language) {
$conditions[] = "`language` = ?";
$condition[] = $language;
}
if (!empty($conditions)) {
$condition[0] .= " AND (" . implode(' OR ', $conditions) . ")";
}
@ -521,6 +647,28 @@ class Timeline extends BaseModule
return $score;
}
private function getMedianPostScore(int $cid, int $divider): int
{
$cache_key = 'Channel:getPostScore:' . $cid . ':' . $divider;
$score = $this->cache->get($cache_key);
if (!empty($score)) {
return $score;
}
$condition = ["`relation-cid` = ? AND `post-score` > ?", $cid, 0];
$limit = $this->database->count('contact-relation', $condition) / $divider;
$relation = $this->database->selectToArray('contact-relation', ['post-score'], $condition, ['order' => ['post-score' => true], 'limit' => [$limit, 1]]);
$score = $relation[0]['post-score'] ?? 0;
if (empty($score)) {
return 0;
}
$this->cache->set($cache_key, $score, Duration::HALF_HOUR);
$this->logger->debug('Calculated median score', ['cid' => $cid, 'divider' => $divider, 'median' => $score]);
return $score;
}
/**
* Computes the displayed items.
*
@ -601,6 +749,8 @@ class Timeline extends BaseModule
*/
private function selectItems()
{
$this->order = 'received';
if ($this->selectedTab == 'local') {
$condition = ["`wall` AND `origin` AND `private` = ?", Item::PUBLIC];
} elseif ($this->selectedTab == 'global') {

View file

@ -43,10 +43,11 @@ class Babel extends BaseModule
}
$results = [];
if (!empty($_REQUEST['text'])) {
switch (($_REQUEST['type'] ?? '') ?: 'bbcode') {
if (!empty($request['text'])) {
self::checkFormSecurityTokenForbiddenOnError('babel');
switch (($request['type'] ?? '') ?: 'bbcode') {
case 'bbcode':
$bbcode = $_REQUEST['text'];
$bbcode = $request['text'];
$results[] = [
'title' => DI::l10n()->t('Source input'),
'content' => visible_whitespace($bbcode)
@ -136,7 +137,7 @@ class Babel extends BaseModule
];
break;
case 'diaspora':
$diaspora = trim($_REQUEST['text']);
$diaspora = trim($request['text']);
$results[] = [
'title' => DI::l10n()->t('Source input (Diaspora format)'),
'content' => visible_whitespace($diaspora),
@ -144,7 +145,7 @@ class Babel extends BaseModule
$markdown = XML::unescape($diaspora);
case 'markdown':
$markdown = $markdown ?? trim($_REQUEST['text']);
$markdown = $markdown ?? trim($request['text']);
$results[] = [
'title' => DI::l10n()->t('Source input (Markdown)'),
@ -169,7 +170,7 @@ class Babel extends BaseModule
];
break;
case 'html' :
$html = trim($_REQUEST['text']);
$html = trim($request['text']);
$results[] = [
'title' => DI::l10n()->t('Raw HTML input'),
'content' => visible_whitespace($html),
@ -239,7 +240,7 @@ class Babel extends BaseModule
];
break;
case 'twitter':
$json = trim($_REQUEST['text']);
$json = trim($request['text']);
if (file_exists('addon/twitter/twitter.php')) {
require_once 'addon/twitter/twitter.php';
@ -302,13 +303,14 @@ class Babel extends BaseModule
$tpl = Renderer::getMarkupTemplate('babel.tpl');
$o = Renderer::replaceMacros($tpl, [
'$title' => DI::l10n()->t('Babel Diagnostic'),
'$text' => ['text', DI::l10n()->t('Source text'), $_REQUEST['text'] ?? '', ''],
'$type_bbcode' => ['type', DI::l10n()->t('BBCode'), 'bbcode', '', (($_REQUEST['type'] ?? '') ?: 'bbcode') == 'bbcode'],
'$type_diaspora' => ['type', DI::l10n()->t('Diaspora'), 'diaspora', '', (($_REQUEST['type'] ?? '') ?: 'bbcode') == 'diaspora'],
'$type_markdown' => ['type', DI::l10n()->t('Markdown'), 'markdown', '', (($_REQUEST['type'] ?? '') ?: 'bbcode') == 'markdown'],
'$type_html' => ['type', DI::l10n()->t('HTML'), 'html', '', (($_REQUEST['type'] ?? '') ?: 'bbcode') == 'html'],
'$form_security_token' => self::getFormSecurityToken('babel'),
'$text' => ['text', DI::l10n()->t('Source text'), $request['text'] ?? '', ''],
'$type_bbcode' => ['type', DI::l10n()->t('BBCode'), 'bbcode', '', (($request['type'] ?? '') ?: 'bbcode') == 'bbcode'],
'$type_diaspora' => ['type', DI::l10n()->t('Diaspora'), 'diaspora', '', (($request['type'] ?? '') ?: 'bbcode') == 'diaspora'],
'$type_markdown' => ['type', DI::l10n()->t('Markdown'), 'markdown', '', (($request['type'] ?? '') ?: 'bbcode') == 'markdown'],
'$type_html' => ['type', DI::l10n()->t('HTML'), 'html', '', (($request['type'] ?? '') ?: 'bbcode') == 'html'],
'$flag_twitter' => file_exists('addon/twitter/twitter.php'),
'$type_twitter' => ['type', DI::l10n()->t('Twitter Source / Tweet URL (requires API key)'), 'twitter', '', (($_REQUEST['type'] ?? '') ?: 'bbcode') == 'twitter'],
'$type_twitter' => ['type', DI::l10n()->t('Twitter Source / Tweet URL (requires API key)'), 'twitter', '', (($request['type'] ?? '') ?: 'bbcode') == 'twitter'],
'$results' => $results,
'$submit' => DI::l10n()->t('Submit'),
]);

View file

@ -99,8 +99,7 @@ class Browser extends BaseModule
protected function map_files(array $record): array
{
$types = Images::supportedTypes();
$ext = $types[$record['type']];
$ext = Images::getExtensionByMimeType($record['type']);
$filename_e = $record['filename'];
// Take the largest picture that is smaller or equal 640 pixels
@ -118,7 +117,7 @@ class Browser extends BaseModule
return [
sprintf('%s/photos/%s/image/%s', $this->baseUrl, $this->app->getLoggedInUserNickname(), $record['resource-id']),
$filename_e,
sprintf('%s/photo/%s-%s.%s', $this->baseUrl, $record['resource-id'], $scale, $ext),
sprintf('%s/photo/%s-%s%s', $this->baseUrl, $record['resource-id'], $scale, $ext),
$record['desc'],
];
}

View file

@ -135,8 +135,6 @@ class Upload extends \Friendica\BaseModule
$this->return(401, $this->t('Invalid request.'), true);
}
$filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
$this->logger->info('File upload:', [
'src' => $src,
'filename' => $filename,
@ -145,7 +143,7 @@ class Upload extends \Friendica\BaseModule
]);
$imagedata = @file_get_contents($src);
$image = new Image($imagedata, $filetype);
$image = new Image($imagedata, $filetype, $filename);
if (!$image->isValid()) {
@unlink($src);

View file

@ -57,11 +57,11 @@ class Index extends BaseModeration
// Edit the entries from blocklist
$blocklist = [];
foreach ($request['domain'] as $id => $domain) {
foreach ((array)$request['domain'] as $id => $domain) {
// Trimming whitespaces as well as any lingering slashes
$domain = trim($domain);
$reason = trim($request['reason'][$id]);
if (empty($request['delete'][$id])) {
if (empty($request['delete'][$id]) && !empty($domain)) {
$blocklist[] = [
'domain' => $domain,
'reason' => $reason

View file

@ -21,10 +21,7 @@
namespace Friendica\Module\OAuth;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
use Friendica\Module\Special\HTTPException;
use Psr\Http\Message\ResponseInterface;

View file

@ -22,15 +22,12 @@
namespace Friendica\Module\OAuth;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\User;
use Friendica\Module\BaseApi;
use Friendica\Module\Special\HTTPException;
use Friendica\Security\OAuth;
use Friendica\Util\DateTimeFormat;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\ResponseInterface;
/**

View file

@ -1,74 +0,0 @@
<?php
/**
* @copyright Copyright (C) 2010-2024, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Content;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Util\Strings;
/**
* Oembed module
*
* Displays stored embed content based on a base64 hash of a remote URL
*
* Example: /oembed/aHR0cHM6Ly9...
*
* @author Hypolite Petovan <hypolite@mrpetovan.com>
*/
class Oembed extends BaseModule
{
protected function content(array $request = []): string
{
// Unused form: /oembed/b2h?url=...
if (DI::args()->getArgv()[1] == 'b2h') {
$url = ["", trim(hex2bin($_REQUEST['url']))];
echo Content\OEmbed::replaceCallback($url);
System::exit();
}
// Unused form: /oembed/h2b?text=...
if (DI::args()->getArgv()[1] == 'h2b') {
$text = trim(hex2bin($_REQUEST['text']));
echo Content\OEmbed::HTML2BBCode($text);
System::exit();
}
// @TODO: Replace with parameter from router
if (DI::args()->getArgc() == 2) {
echo '<html><body>';
$url = Strings::base64UrlDecode(DI::args()->getArgv()[1]);
$j = Content\OEmbed::fetchURL($url);
// workaround for media.ccc.de (and any other endpoint that return size 0)
if (substr($j->html, 0, 7) == "<iframe" && strstr($j->html, 'width="0"')) {
$j->html = '<style>html,body{margin:0;padding:0;} iframe{width:100%;height:100%;}</style>' . $j->html;
$j->html = str_replace('width="0"', '', $j->html);
$j->html = str_replace('height="0"', '', $j->html);
}
echo $j->html;
echo '</body></html>';
}
System::exit();
}
}

View file

@ -100,7 +100,6 @@ class Photo extends BaseApi
$id = $account['id'];
}
// Contact Id Fallback, to remove after version 2021.12
if (isset($this->parameters['contact_id'])) {
$id = intval($this->parameters['contact_id']);
}
@ -115,12 +114,6 @@ class Photo extends BaseApi
$id = $user['uid'];
}
// User Id Fallback, to remove after version 2021.12
if (!empty($this->parameters['uid_ext'])) {
$id = intval(pathinfo($this->parameters['uid_ext'], PATHINFO_FILENAME));
}
// Please refactor this for the love of everything that's good
if (isset($this->parameters['id'])) {
$id = $this->parameters['id'];
}
@ -166,13 +159,17 @@ class Photo extends BaseApi
$stamp = microtime(true);
$imgdata = MPhoto::getImageDataForPhoto($photo);
if (empty($request['blur']) || empty($photo['blurhash'])) {
$imgdata = MPhoto::getImageDataForPhoto($photo);
$mimetype = $photo['type'];
}
if (empty($imgdata) && empty($photo['blurhash'])) {
throw new HTTPException\NotFoundException();
} elseif (empty($imgdata) && !empty($photo['blurhash'])) {
$image = New Image('', 'image/png');
$image = New Image('', image_type_to_mime_type(IMAGETYPE_WEBP));
$image->getFromBlurHash($photo['blurhash'], $photo['width'], $photo['height']);
$imgdata = $image->asString();
$imgdata = $image->asString();
$mimetype = $image->getType();
}
// The mimetype for an external or system resource can only be known reliably after it had been fetched
@ -197,20 +194,23 @@ class Photo extends BaseApi
}
if (!empty($request['static'])) {
$img = new Image($imgdata, $photo['type']);
$img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->toStatic();
$imgdata = $img->asString();
$imgdata = $img->asString();
$mimetype = $img->getType();
}
// if customsize is set and image is not a gif, resize it
if ($photo['type'] !== 'image/gif' && $customsize > 0 && $customsize <= Proxy::PIXEL_THUMB && $square_resize) {
$img = new Image($imgdata, $photo['type']);
// if customsize is set and image resizing is supported for this image, resize it
if (Images::canResize($photo['type']) && $customsize > 0 && $customsize <= Proxy::PIXEL_THUMB && $square_resize) {
$img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->scaleToSquare($customsize);
$imgdata = $img->asString();
} elseif ($photo['type'] !== 'image/gif' && $customsize > 0) {
$img = new Image($imgdata, $photo['type']);
$imgdata = $img->asString();
$mimetype = $img->getType();
} elseif (Images::canResize($photo['type']) && $customsize > 0) {
$img = new Image($imgdata, $photo['type'], $photo['filename']);
$img->scaleDown($customsize);
$imgdata = $img->asString();
$imgdata = $img->asString();
$mimetype = $img->getType();
}
if (function_exists('header_remove')) {
@ -218,7 +218,7 @@ class Photo extends BaseApi
header_remove('pragma');
}
header('Content-type: ' . $photo['type']);
header('Content-type: ' . $mimetype);
$stamp = microtime(true);
if (!$cacheable) {
@ -389,7 +389,7 @@ class Photo extends BaseApi
}
}
if (empty($mimetext) && !empty($contact['blurhash'])) {
$image = New Image('', 'image/png');
$image = New Image('', image_type_to_mime_type(IMAGETYPE_WEBP));
$image->getFromBlurHash($contact['blurhash'], $customsize, $customsize);
return MPhoto::createPhotoForImageData($image->asString());
} elseif (empty($mimetext)) {

View file

@ -184,8 +184,6 @@ class Photos extends \Friendica\Module\BaseProfile
return;
}
$type = Images::getMimeTypeBySource($src, $filename, $type);
$this->logger->info('photos: upload: received file: ' . $filename . ' as ' . $src . ' ('. $type . ') ' . $filesize . ' bytes');
$maximagesize = Strings::getBytesFromShorthand($this->config->get('system', 'maximagesize'));
@ -210,7 +208,7 @@ class Photos extends \Friendica\Module\BaseProfile
$imagedata = @file_get_contents($src);
$image = new Image($imagedata, $type);
$image = new Image($imagedata, $type, $filename);
if (!$image->isValid()) {
$this->logger->notice('unable to process image');
@ -341,14 +339,12 @@ class Photos extends \Friendica\Module\BaseProfile
$pager->getItemsPerPage()
));
$phototypes = Images::supportedTypes();
$photos = array_map(function ($photo) use ($phototypes) {
$photos = array_map(function ($photo){
return [
'id' => $photo['id'],
'link' => 'photos/' . $this->owner['nickname'] . '/image/' . $photo['resource-id'],
'title' => $this->t('View Photo'),
'src' => 'photo/' . $photo['resource-id'] . '-' . ((($photo['scale']) == 6) ? 4 : $photo['scale']) . '.' . $phototypes[$photo['type']],
'src' => 'photo/' . $photo['resource-id'] . '-' . ((($photo['scale']) == 6) ? 4 : $photo['scale']) . Images::getExtensionByMimeType($photo['type']),
'alt' => $photo['filename'],
'album' => [
'link' => 'photos/' . $this->owner['nickname'] . '/album/' . bin2hex($photo['album']),

View file

@ -226,7 +226,7 @@ class Profile extends BaseProfile
// Separator is defined in Module\Settings\Profile\Index::cleanKeywords
foreach (explode(', ', $profile['pub_keywords']) as $tag_label) {
$tags[] = [
'url' => '/search?tag=' . $tag_label,
'url' => '/search?tag=' . urlencode($tag_label),
'label' => Tag::TAG_CHARACTER[Tag::HASHTAG] . $tag_label,
];
}

View file

@ -99,17 +99,15 @@ class Proxy extends BaseModule
Logger::debug('Got picture', ['Content-Type' => $fetchResult->getHeader('Content-Type'), 'uid' => DI::userSession()->getLocalUserId(), 'image' => $request['url']]);
$mime = Images::getMimeTypeByData($img_str);
$image = new Image($img_str, $mime);
$image = new Image($img_str, $fetchResult->getContentType(), $request['url']);
if (!$image->isValid()) {
Logger::notice('The image is invalid', ['image' => $request['url'], 'mime' => $mime]);
Logger::notice('The image is invalid', ['image' => $request['url'], 'mime' => $fetchResult->getContentType()]);
self::responseError();
// stop.
}
// reduce quality - if it isn't a GIF
if ($image->getType() != 'image/gif') {
// reduce quality - if it is supported for this image type
if (Images::canResize($image->getType())) {
$image->scaleDown($request['size']);
}

View file

@ -284,7 +284,19 @@ class Register extends BaseModule
$regdata = ['email' => $arr['email'], 'nickname' => $arr['nickname'], 'username' => $arr['username']];
DI::baseUrl()->redirect('register?' . http_build_query($regdata));
}
//Check if nickname contains only US-ASCII and do not start with a digit
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $arr['nickname'])) {
if (is_numeric(substr($arr['nickname'], 0, 1))) {
DI::sysmsg()->addNotice(DI::l10n()->t("Nickname cannot start with a digit."));
} else {
DI::sysmsg()->addNotice(DI::l10n()->t("Nickname can only contain US-ASCII characters."));
}
$regdata = ['email' => $arr['email'], 'nickname' => $arr['nickname'], 'username' => $arr['username']];
DI::baseUrl()->redirect('register?' . http_build_query($regdata));
return;
}
$arr['blocked'] = $blocked;
$arr['verified'] = $verified;
$arr['language'] = L10n::detectLanguage($_SERVER, $_GET, DI::config()->get('system', 'language'));

View file

@ -83,6 +83,8 @@ class Channels extends BaseSettings
'circle' => (int)$request['new_circle'],
'include-tags' => Strings::cleanTags($request['new_include_tags']),
'exclude-tags' => Strings::cleanTags($request['new_exclude_tags']),
'min-size' => $request['new_min_size'] != '' ? (int)$request['new_min_size'] : null,
'max-size' => $request['new_max_size'] != '' ? (int)$request['new_max_size'] : null,
'full-text-search' => $request['new_text_search'],
'media-type' => ($request['new_image'] ? 1 : 0) | ($request['new_video'] ? 2 : 0) | ($request['new_audio'] ? 4 : 0),
'languages' => $request['new_languages'],
@ -99,7 +101,7 @@ class Channels extends BaseSettings
continue;
}
if (!array_diff((array)$request['languages'][$id], $channel_languages)) {
if (!array_diff((array)$request['languages'][$id], $channel_languages) && (count((array)$request['languages'][$id]) == count($channel_languages))) {
$request['languages'][$id] = null;
}
@ -112,6 +114,8 @@ class Channels extends BaseSettings
'circle' => (int)$request['circle'][$id],
'include-tags' => Strings::cleanTags($request['include_tags'][$id]),
'exclude-tags' => Strings::cleanTags($request['exclude_tags'][$id]),
'min-size' => $request['min_size'][$id] != '' ? (int)$request['min_size'][$id] : null,
'max-size' => $request['max_size'][$id] != '' ? (int)$request['max_size'][$id] : null,
'full-text-search' => $request['text_search'][$id],
'media-type' => ($request['image'][$id] ? 1 : 0) | ($request['video'][$id] ? 2 : 0) | ($request['audio'][$id] ? 4 : 0),
'languages' => $request['languages'][$id],
@ -143,7 +147,9 @@ class Channels extends BaseSettings
$intro = $this->t('This page can be used to define your own channels.');
$circles = [
0 => $this->l10n->t('Global Community'),
-3 => $this->l10n->t('Network'),
-5 => $this->l10n->t('Latest Activity'),
-4 => $this->l10n->t('Latest Posts'),
-3 => $this->l10n->t('Latest Creation'),
-1 => $this->l10n->t('Following'),
-2 => $this->l10n->t('Followers'),
];
@ -181,6 +187,8 @@ class Channels extends BaseSettings
'circle' => ["circle[$channel->code]", $this->t('Circle/Channel'), $channel->circle, '', $circles],
'include_tags' => ["include_tags[$channel->code]", $this->t("Include Tags"), str_replace(',', ', ', $channel->includeTags)],
'exclude_tags' => ["exclude_tags[$channel->code]", $this->t("Exclude Tags"), str_replace(',', ', ', $channel->excludeTags)],
'min_size' => ["min_size[$channel->code]", $this->t("Minimum Size"), $channel->minSize],
'max_size' => ["max_size[$channel->code]", $this->t("Maximum Size"), $channel->maxSize],
'text_search' => ["text_search[$channel->code]", $this->t("Full Text Search"), $channel->fullTextSearch],
'image' => ["image[$channel->code]", $this->t("Images"), $channel->mediaType & 1],
'video' => ["video[$channel->code]", $this->t("Videos"), $channel->mediaType & 2],
@ -200,6 +208,8 @@ class Channels extends BaseSettings
'circle' => ['new_circle', $this->t('Circle/Channel'), 0, $this->t('Select a circle or channel, that your channel should be based on.'), $circles],
'include_tags' => ["new_include_tags", $this->t("Include Tags"), '', $this->t('Comma separated list of tags. A post will be used when it contains any of the listed tags.')],
'exclude_tags' => ["new_exclude_tags", $this->t("Exclude Tags"), '', $this->t('Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel.')],
'min_size' => ["new_min_size", $this->t("Minimum Size"), '', $this->t('Minimum post size. Leave empty for no minimum size. The size is calculated without links, attached posts, mentions or hashtags.')],
'max_size' => ["new_max_size", $this->t("Maximum Size"), '', $this->t('Maximum post size. Leave empty for no maximum size. The size is calculated without links, attached posts, mentions or hashtags.')],
'text_search' => ["new_text_search", $this->t("Full Text Search"), '', $this->t('Search terms for the body, supports the "boolean mode" operators from MariaDB. See the help for a complete list of operators and additional keywords: %s', '<a href="help/Channels">help/Channels</a>')],
'image' => ['new_image', $this->t("Images"), false, $this->t("Check to display images in the channel.")],
'video' => ["new_video", $this->t("Videos"), false, $this->t("Check to display videos in the channel.")],

View file

@ -106,6 +106,7 @@ class Display extends BaseSettings
$enable_smart_threading = (bool)$request['enable_smart_threading'];
$enable_dislike = (bool)$request['enable_dislike'];
$display_resharer = (bool)$request['display_resharer'];
$display_sensitive = (bool)$request['display_sensitive'];
$stay_local = (bool)$request['stay_local'];
$show_page_drop = (bool)$request['show_page_drop'];
$display_eventlist = (bool)$request['display_eventlist'];
@ -157,6 +158,7 @@ class Display extends BaseSettings
$this->pConfig->set($uid, 'system', 'no_smart_threading' , !$enable_smart_threading);
$this->pConfig->set($uid, 'system', 'hide_dislike' , !$enable_dislike);
$this->pConfig->set($uid, 'system', 'display_resharer' , $display_resharer);
$this->pConfig->set($uid, 'system', 'display_sensitive' , $display_sensitive);
$this->pConfig->set($uid, 'system', 'stay_local' , $stay_local);
$this->pConfig->set($uid, 'system', 'show_page_drop' , $show_page_drop);
$this->pConfig->set($uid, 'system', 'display_eventlist' , $display_eventlist);
@ -251,6 +253,7 @@ class Display extends BaseSettings
$enable_smart_threading = !$this->pConfig->get($uid, 'system', 'no_smart_threading', false);
$enable_dislike = !$this->pConfig->get($uid, 'system', 'hide_dislike', false);
$display_resharer = $this->pConfig->get($uid, 'system', 'display_resharer', false);
$display_sensitive = $this->pConfig->get($uid, 'system', 'display_sensitive', false);
$stay_local = $this->pConfig->get($uid, 'system', 'stay_local', false);
$show_page_drop = $this->pConfig->get($uid, 'system', 'show_page_drop', true);
$display_eventlist = $this->pConfig->get($uid, 'system', 'display_eventlist', true);
@ -330,6 +333,7 @@ class Display extends BaseSettings
'$enable_smart_threading' => ['enable_smart_threading' , $this->t('Enable Smart Threading'), $enable_smart_threading, $this->t('Enable the automatic suppression of extraneous thread indentation.')],
'$enable_dislike' => ['enable_dislike' , $this->t('Display the Dislike feature'), $enable_dislike, $this->t('Display the Dislike button and dislike reactions on posts and comments.')],
'$display_resharer' => ['display_resharer' , $this->t('Display the resharer'), $display_resharer, $this->t('Display the first resharer as icon and text on a reshared item.')],
'$display_sensitive' => ['display_sensitive' , $this->t('Display sensitive content'), $display_sensitive, $this->t('If enabled, pictures in posts marked as "sensitive" will not be blurred.')],
'$stay_local' => ['stay_local' , $this->t('Stay local'), $stay_local, $this->t("Don't go to a remote system when following a contact link.")],
'$show_page_drop' => ['show_page_drop' , $this->t('Show the post deletion checkbox'), $show_page_drop, $this->t("Display the checkbox for the post deletion on the network page.")],
'$display_eventlist' => ['display_eventlist' , $this->t('DIsplay the event list'), $display_eventlist, $this->t("Display the birthday reminder and event list on the network page.")],

View file

@ -213,7 +213,7 @@ class Crop extends BaseSettings
DI::page()['htmlhead'] .= Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/profile/photo/crop_head.tpl'), []);
$filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . '.' . $imagecrop['ext'];
$filename = $imagecrop['resource-id'] . '-' . $imagecrop['scale'] . $imagecrop['ext'];
$tpl = Renderer::getMarkupTemplate('settings/profile/photo/crop.tpl');
$o = Renderer::replaceMacros($tpl, [
'$filename' => $filename,

View file

@ -52,8 +52,6 @@ class Index extends BaseSettings
$filesize = intval($_FILES['userfile']['size']);
$filetype = $_FILES['userfile']['type'];
$filetype = Images::getMimeTypeBySource($src, $filename, $filetype);
$maximagesize = Strings::getBytesFromShorthand(DI::config()->get('system', 'maximagesize', 0));
if ($maximagesize && $filesize > $maximagesize) {
@ -63,7 +61,7 @@ class Index extends BaseSettings
}
$imagedata = @file_get_contents($src);
$Image = new Image($imagedata, $filetype);
$Image = new Image($imagedata, $filetype, $filename);
if (!$Image->isValid()) {
DI::sysmsg()->addNotice(DI::l10n()->t('Unable to process image.'));

View file

@ -29,7 +29,6 @@ use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Database\Definition\DbaDefinition;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\Post;
@ -47,8 +46,7 @@ use Psr\Log\LoggerInterface;
**/
class UserExport extends BaseSettings
{
/** @var DbaDefinition */
private $dbaDefinition;
private DbaDefinition $dbaDefinition;
public function __construct(DbaDefinition $dbaDefinition, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
@ -86,10 +84,12 @@ class UserExport extends BaseSettings
* options shown on "Export personal data" page
* list of array( 'link url', 'link text', 'help text' )
*/
$t = self::getFormSecurityToken('userexport');
$options = [
['settings/userexport/account', $this->l10n->t('Export account'), $this->l10n->t('Export your account info and contacts. Use this to make a backup of your account and/or to move it to another server.')],
['settings/userexport/backup', $this->l10n->t('Export all'), $this->l10n->t('Export your account info, contacts and all your items as json. Could be a very big file, and could take a lot of time. Use this to make a full backup of your account (photos are not exported)')],
['settings/userexport/contact', $this->l10n->t('Export Contacts to CSV'), $this->l10n->t('Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.')],
['settings/userexport/account?t=' . $t, $this->l10n->t('Export account'), $this->l10n->t('Export your account info and contacts. Use this to make a backup of your account and/or to move it to another server.')],
['settings/userexport/backup?t=' . $t, $this->l10n->t('Export all'), $this->l10n->t('Export your account info, contacts and all your items as json. Could be a very big file, and could take a lot of time. Use this to make a full backup of your account (photos are not exported)')],
['settings/userexport/contact?t=' . $t, $this->l10n->t('Export Contacts to CSV'), $this->l10n->t('Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.')],
];
Hook::callAll('uexport_options', $options);
@ -115,20 +115,21 @@ class UserExport extends BaseSettings
}
if (isset($this->parameters['action'])) {
self::checkFormSecurityTokenForbiddenOnError('userexport', 't');
switch ($this->parameters['action']) {
case 'backup':
header('Content-type: application/json');
header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '.' . $this->parameters['action'] . '"');
header('Content-Disposition: attachment; filename="' . $this->session->getLocalUserNickname() . '.' . $this->parameters['action'] . '"');
$this->echoAll($this->session->getLocalUserId());
break;
case 'account':
header('Content-type: application/json');
header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '.' . $this->parameters['action'] . '"');
header('Content-Disposition: attachment; filename="' . $this->session->getLocalUserNickname() . '.' . $this->parameters['action'] . '"');
$this->echoAccount($this->session->getLocalUserId());
break;
case 'contact':
header('Content-type: application/csv');
header('Content-Disposition: attachment; filename="' . DI::app()->getLoggedInUserNickname() . '-contacts.csv' . '"');
header('Content-Disposition: attachment; filename="' . $this->session->getLocalUserNickname() . '-contacts.csv' . '"');
$this->echoContactsAsCSV($this->session->getLocalUserId());
break;
}
@ -156,11 +157,8 @@ class UserExport extends BaseSettings
if (!isset($row[$column])) {
continue;
}
if ($field['type'] == 'datetime') {
$p[$column] = $row[$column] ?? DBA::NULL_DATETIME;
} else {
$p[$column] = $row[$column];
}
$p[$column] = $row[$column];
}
$result[] = $p;
}

View file

@ -89,7 +89,7 @@ class Tos extends BaseModule
$rules = "[ol]";
foreach (explode("\n", $lines) as $line) {
if (trim($line)) {
$rules .= "\n[*]" . trim($line);
$rules .= "\n[li]" . trim($line);
}
}
$rules .= "\n[/ol]\n";

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