Compare commits

..

322 Commits

Author SHA1 Message Date
Mario
deb4c54ebf version 11.2.1 2026-05-20 07:53:27 +02:00
Mario
281f518705 update changelog
(cherry picked from commit 496dade675)

Co-authored-by: Mario <mario@mariovavti.com>
2026-05-20 05:51:49 +00:00
Mario
a8d9747b12 test variable before using it
(cherry picked from commit 82b8ed2652)

Co-authored-by: Mario <mario@mariovavti.com>
2026-05-20 05:49:33 +00:00
Mario
4b15b07b8b update changelog
(cherry picked from commit 2625fe9527)

Co-authored-by: Mario <mario@mariovavti.com>
2026-05-20 05:43:42 +00:00
Mario
607a5488d6 Improve detecting suspicious ActivityStreams keys
Using string comparison on the whole key does not work, as some keys
will be given prefixes during expansion. We need to check if the payload
has keys that _contain_ the suspicious keywords we're looking for.


(cherry picked from commit 0c7731bb76)

Co-authored-by: Harald Eilertsen <haraldei@anduin.net>
2026-05-18 19:06:06 +00:00
Mario
519b52ccdc check for currently unsafe json-ld constructs
(cherry picked from commit 67d73f74ac)

45460e99 check for currently unsafe constructs
5ebb8546 move the jsonld unsafe keys check up a little so that it will actually terminate if positive.
363e2ab5 fix invalid json
b2362c8c only expand and check jsonld if verifying
d21ac6ef jsonld: refactor and hard fail on normalisation or expansion error
2e64496e Merge branch 'dev' into json-ld
c0918861 revert always hard fail

Co-authored-by: Mario <mario@mariovavti.com>
2026-05-18 19:05:16 +00:00
Mario
99fd2e1cb9 tests: Use minimal channel for LDSignature tests
Move the channel keypair and other fields needed by the
LDSignature::verify function to the test case itself. This makes the
test case independent on any potential future changes to the fixtures.


(cherry picked from commit 8d283e0be5)

Co-authored-by: Harald Eilertsen <haraldei@anduin.net>
2026-05-18 19:03:59 +00:00
Mario
a69c460ee6 tests: Add basic test for LDSignature::verify
(cherry picked from commit 0cd682d85e)

Co-authored-by: Harald Eilertsen <haraldei@anduin.net>
2026-05-18 19:03:11 +00:00
Mario
71ba6406e3 tests: Fix invalid keypair for channel fixture
(cherry picked from commit 9aff1d4024)

Co-authored-by: Harald Eilertsen <haraldei@anduin.net>
2026-05-18 19:02:48 +00:00
Mario
4922acb18a actually there is no need to set App::$profile here because it will be set in profile_load() if applicable
(cherry picked from commit 0376b5d442)

Co-authored-by: Mario <mario@mariovavti.com>
2026-05-18 19:01:04 +00:00
Mario
a0ea19e51c do not set App::$profile for removed channels and minor cleanup
(cherry picked from commit b20ed4f455)

Co-authored-by: Mario <mario@mariovavti.com>
2026-05-18 19:00:15 +00:00
Mario
8c4457db8b messageFilter: make sure we do not choke if we expect a string but an array is given 2026-03-27 21:53:04 +00:00
Mario
89a1af1794 Merge branch '11.2RC' 2026-03-26 08:08:37 +00:00
Mario
d5e0c24e13 version 11.2 2026-03-26 08:06:51 +00:00
Mario
8fe73d73ce Merge branch 'dev' into 11.2RC 2026-03-26 08:05:53 +00:00
Mario
7559d6bb5c update changelog 2026-03-26 08:05:06 +00:00
Mario
6c40576f94 Merge branch 'dev' into 11.2RC 2026-03-26 07:50:56 +00:00
Mario
1c9c0dc70e update changelog 2026-03-26 07:50:08 +00:00
Mario
bf4227bfef Merge branch 'drop-private-members-from-apidocs' into 'dev'
Remove private members from API docs

See merge request hubzilla/core!2275
2026-03-25 18:12:54 +00:00
Mario
c296d4bbed Merge branch 'drop-custom-ca-certs' into 'dev'
Remove custom CA certs

See merge request hubzilla/core!2274
2026-03-25 18:12:21 +00:00
Harald Eilertsen
68452fb07b Remove private members from API docs
Private class members are internal implementation details, and should
not be in public API documentation.
2026-03-25 18:29:38 +01:00
Harald Eilertsen
7f98cd606b Remove custom CA certs
The included collection of CA certificates has not been updated since
2021, which means it's outdated and possibly a security risk. Also it
should not be necessary, as curl is able to find the system installed CA
certs which is more likely to be up to date and safer.
2026-03-25 18:06:39 +01:00
Mario
d0ae7a0493 Merge branch 'route-apidoc-fixes' into 'dev'
Zotlabs\Extend\Route: API docs and visibility/type fixes

See merge request hubzilla/core!2273
2026-03-25 15:18:27 +00:00
Harald Eilertsen
81e79eec04 Zotlabs\Extend\Route: Add type annotations
Make the code safer by adding type annotations for parameters and return
values.
2026-03-25 15:34:24 +01:00
Harald Eilertsen
7d70f2f0f2 Zotlabs\Extend\Route: Set method visibility
Make `Route::set()` private. It is not referenced outside of the class
itself, and is also unsafe as it uncritically replaces the system routes
with whatever value it was passed.

The remaining functions are set to public visibility.
2026-03-25 12:45:44 +01:00
Harald Eilertsen
016a11ad27 Zotlabs\Extend\Route: Add API docs + licence info 2026-03-25 12:33:11 +01:00
Mario
1637e61681 update changelog 2026-03-25 10:30:27 +00:00
Mario
1ec0e91405 fix issue in route and widget unregister: we should remove only if both arguments are different. Also only register routes and widgets if they are not yet registered 2026-03-25 10:16:49 +00:00
Mario
3ab52a060b add sleep intervals when adding worker tasks in a loop in Activity::init_background_fetch()
(cherry picked from commit 955ee217e3)

Co-authored-by: Mario <mario@mariovavti.com>
2026-03-25 07:54:51 +00:00
Mario
866cd52073 Convo: fetched collections can be huge - add a slight delay between storing items if we did not fetch them from the network to allow the DB to do its thing
(cherry picked from commit 6d8bfe58ef)

Co-authored-by: Mario <mario@mariovavti.com>
2026-03-25 07:52:33 +00:00
Mario
1774140307 Storage/Directory: fix issue where we returned a partial path instead of throwing exception if a directory of a path could not be found 2026-03-23 20:57:46 +00:00
Mario
698dce044d rc2 2026-03-18 09:13:58 +00:00
Mario
bf0d73515e Merge branch 'dev' into 11.2RC 2026-03-18 09:13:23 +00:00
Mario
8152da1275 update changelog 2026-03-18 09:13:00 +00:00
Mario
01de48b994 Merge branch 'dev' into 11.2RC 2026-03-18 09:02:20 +00:00
Mario
eb10820195 Merge branch 'daemon-externals-endless-loop' into 'dev'
Daemon/Externals: Shorten endless loop

See merge request hubzilla/core!2272
2026-03-18 09:01:32 +00:00
Harald Eilertsen
9d33456c89 Daemon/Externals: Shorten endless loop
If the query towards the hubloc table don't return any entries, the loop
would continue without modifying any of the variables that could
possibly make the loop end.
2026-03-17 21:42:28 +01:00
Mario
56c7f76230 Merge branch 'dev' into 11.2RC 2026-03-17 11:47:27 +00:00
Mario
3dd9559d9f fix spdx info and remove superfluous use statements 2026-03-17 06:24:03 +00:00
Mario Vavti
51ac502d97 refactor drop_query_params() to deal with array params and add test 2026-03-16 10:32:59 +01:00
Mario
955ee217e3 add sleep intervals when adding worker tasks in a loop in Activity::init_background_fetch() 2026-03-14 19:58:09 +00:00
Mario
6d8bfe58ef Convo: fetched collections can be huge - add a slight delay between storing items if we did not fetch them from the network to allow the DB to do its thing 2026-03-14 19:41:26 +00:00
Mario
f98f540256 update changelog 2026-03-14 19:37:37 +00:00
Mario
54c7319075 fix version 2026-03-14 10:33:06 +00:00
Mario
72c930f964 remove accent color test 2026-03-14 10:31:32 +00:00
Mario
ce24b86841 fix fatal error in italian hstrings.php 2026-03-14 10:29:48 +00:00
Mario
f63ba541d7 bump dev version 2026-03-13 16:14:46 +00:00
Mario
a9f54473db version and strings and move hmessages.po to hmessages.pot 2026-03-13 16:11:13 +00:00
Mario
eb5ea6536b fix php warning 2026-03-11 19:05:39 +00:00
Mario
2e9b64347d Merge branch 'gettext-updates' into 'dev'
util/run_xgettext: Rename template to .pot and force PHP matching

See merge request hubzilla/core!2271
2026-03-11 18:48:47 +00:00
Mario
4fcff43fb5 Merge branch 'tests-make-loadFixtures-public' into 'dev'
tests: Make loadFixtures public

See merge request hubzilla/core!2270
2026-03-11 18:41:24 +00:00
Harald Eilertsen
260b61ba3b util/run_xgettext: Use PHP matching rules
Explicitly tell xgettext to use PHP matching rules to find translatable
strings.

Project......: Improve Superblock Addon
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-11 11:51:35 +01:00
Harald Eilertsen
d0b7e4ea79 util/run_xgettext: Rename template to .pot
Renaming the translation template to *.pot, works better with some
tooling like poedit and lokalize.

Project......: Improve Superblock Addon
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-11 11:48:09 +01:00
Harald Eilertsen
33c95c810a tests: Make loadFixtures public
Some tests will need to load their own database fixtures. By making this
API public we make it easy for them to do so.

This change is needed by the tests for the Superblock addon.

Project......: Improve Superblock Addon
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-11 11:40:31 +01:00
Mario
81105ff9de Merge branch 'null-date-fix' into 'dev'
Remove use of NULL_DATE constant in core

See merge request hubzilla/core!2269
2026-03-08 17:07:41 +00:00
Harald Eilertsen
ced3113516 Remove unneccessary use statements 2026-03-07 12:00:56 +01:00
Harald Eilertsen
cbd208eea3 Remove use of NULL_DATE constant in core
The NULL_DATE constant is defined conditionally in the DBA static class.
This causes issues with static analyzing tools like PHPStan, because
they can not really know if the constant is defined or not.

We could make PHPStan ignore this, but since there already is a
`get_null_date()` method on the `dba_driver` class, this patch
changes the code to use this method instead.

We could also use the public static attribute `$null_date` on the DBA
class directly, but using a method feels cleaner, and allows for making
the attribute private, or even removing it completely at some later
time.

I'm not removing the NULL_DATE constant for now, in case it is in use by
any extensions.
2026-03-07 11:15:46 +01:00
Mario Vavti
ad85825cab remove composer dev stuff 2026-03-04 18:34:45 +01:00
Mario Vavti
2fb816139a guzzlehttp/psr7 has been removed from the HttpSigner library because it is not required there, hubzilla still needs it 2026-03-04 18:29:33 +01:00
DDEV User
c9166b26c5 update composer libs 2026-03-04 17:51:14 +01:00
Mario Vavti
86d58065b3 deal with leading or missing @ and add more tests 2026-03-04 10:25:32 +01:00
Mario
1f265cc6d5 fix tests (hopefully) 2026-03-04 08:44:09 +00:00
Mario
4474fdd4f9 introduce parse_webbie() with basic tests 2026-03-04 08:34:45 +00:00
Mario
f71eeab5be fix php warning 2026-03-03 11:27:01 +00:00
Mario
b3526415f9 allow to override curl useragent 2026-03-03 10:59:11 +00:00
Mario
471ded3efa Merge branch 'sys_stat_fixes' into dev 2026-03-03 09:19:09 +00:00
Mario
e954d8c55e mod network: dismiss privacy filter for various filter options - fix issue #1973 2026-03-03 09:17:56 +00:00
Mario
70f82c3967 stop duplicating content in reactions - fix issue #1970 2026-03-03 07:57:39 +00:00
Mario
707e07bbbc Merge branch 'sys_stat_fixes' into 'dev'
Only poll perfstats if the widget is actually visible

See merge request hubzilla/core!2267
2026-03-02 13:47:51 +00:00
Mario
e89eb04427 Merge branch 'dev' into sys_stat_fixes 2026-03-02 08:59:45 +00:00
Mario
ae0e82ee3a wait for domn content loaded 2026-03-02 08:48:14 +00:00
Mario
9ea5d20a3d use JS listener instead of jquery 2026-03-02 08:45:42 +00:00
Mario
fd69008484 this probably should not be const but keep it non global anyway 2026-03-02 08:05:31 +00:00
Mario
3256aa8be9 only poll perfstats if the widget id actually visible (the widget is mostly hidden when viewing content in HQ) 2026-03-02 07:34:43 +00:00
Mario
add26a5b5f fix Queue::count() return value 2026-03-01 19:12:01 +00:00
Mario
dc03263bef Merge branch 'system-status-activity-widget' into 'dev'
Add system status activity widget to HQ for admins

See merge request hubzilla/core!2266
2026-03-01 18:42:54 +00:00
Harald Eilertsen
1897cd0b1b Make system status widget a bit more snappy
Fetch the initial data when the widget loads, and then update every five
sec after that. Also better indication of uninitialized values.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-01 11:47:39 +01:00
Harald Eilertsen
ba24958b37 Remove some debug logging in system status widget
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-01 11:38:07 +01:00
Harald Eilertsen
52a2a0d89a Pass loadavg as array from Perfstats
There's no reason we should format the data into a string in the
Perfstats module. Let the recipient do what they want with it instead.

As an example, we reduce the precision of the loadavg stats in the
system status widget. 3 digits precision should be more than enough for
this type of status display.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-01 11:34:28 +01:00
Harald Eilertsen
91944da69e Report queries/sec in system status widget
The total number of queries is not that interesting for performance
measurements, so let's do queries pr/second instead. Adds a timestamp
column to the dataset, which could be useful for other purposes as well
(like making a graph over time etc.)

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-03-01 11:16:05 +01:00
Harald Eilertsen
78e30a4d32 Simplify stats for getting size of output queue
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-26 16:28:59 +01:00
Harald Eilertsen
ccd6d1a38c Move db stats to separate classes for each db type
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-26 16:19:17 +01:00
Mario
f14c1be963 Some projects use double typing for its objects (bandwagon in this case) - we do not support this, hence go with the first entry and hope for the best. 2026-02-26 11:31:56 +00:00
Harald Eilertsen
db5e92b72d Remove unused/incomplete function
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:34:04 +01:00
Harald Eilertsen
3d3580b23f Validate requests to perfstats
Make some stricter rules for accessing the perfstats module. We only
want to respond to GET request for json data made by a site admin.

This means we also need to pass along the credentials in the request.
Didn't immediately figure out how to do that using `z_fetch_url`, so we
just fall back to using the JavaScript to populate the widget. This
makes it idle for about 5 sec before it gets the first data sample.

I think that's probably OK.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:21:09 +01:00
Harald Eilertsen
04d44c9965 Remove link from system status widget title
At least for now, there's no sensible link target, so it's better to not
link anywhere.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:21:09 +01:00
Harald Eilertsen
82bd91d9d7 Log error if system status can't load
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:21:09 +01:00
Harald Eilertsen
492533729d Human labels for system status widget
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:21:09 +01:00
Harald Eilertsen
283b606c09 Update system status activity widget periodically
Adds a bit of javascript that requests the performance stats every 5
seconds.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:21:09 +01:00
Harald Eilertsen
b0b5523f2b Add perfstat module
Moves collecting performance statistics for the system status activity
widget to a separate module. This will make it easier to get the data
from JavaScript or external monitoring tools.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:21:09 +01:00
Harald Eilertsen
738797467d Turn dba_driver params into attributes
To allow for inspecting the params used to connect to the database, we
now save the params in the dba_driver object instance as readonly
attributes. An exception is made to the $pass parameter which is set to
protected access, to make it slightly harder to accidentally leak it.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 21:20:52 +01:00
Harald Eilertsen
a2ee5705f4 Add System Status panel to HQ for admins
Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 20:15:58 +01:00
Harald Eilertsen
cc1713b69a Add Queue::get_undelivered() helper function
Returns the number of undelivered messages in the outq.

Instead of having direct database queries at various places in the code,
I thought it would be better to keep them in a more logical place.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 20:15:06 +01:00
Harald Eilertsen
6f5e4c5c2e Add QueueWorkerStats class
A simple class to fetch and hold queueworker stats.

Project......: Performance Profiling
Sponsored-by.: NLnet NGI0 Commons Fund
2026-02-23 20:13:33 +01:00
Mario
3e0b9c01b6 Merge branch 'channel-manage-tpl' into 'dev'
move manage channel alert to tpl

See merge request hubzilla/core!2265
2026-02-17 09:22:07 +00:00
SK
25218fea43 move manage channel alert to tpl
allows a way to customize appearance of html for channel manager alerts
2026-02-15 06:42:35 +05:30
Mario
c907e569f1 fix wrong var after refactor 2026-02-14 10:41:52 +00:00
Mario
b6bec6f7b7 reverse logic 2026-02-14 10:29:14 +00:00
Mario
31b5cfe7ef if the event timezone is different from the channel timezone, display the timezone we have adjusted to instead of the actual event timezone. Otherwise we might create UX confusion about if the displayed time is already timezone adjusted or not. 2026-02-13 08:50:25 +00:00
Mario
0efb3c5c95 comment out deprecated use of tags and attachments in activities 2026-02-13 08:25:05 +00:00
Mario
d88f3169c8 fix possibly duplicated terms in activity object 2026-02-13 08:22:19 +00:00
Mario
3772e910df fixup after testing to get a minimalistic working config 2026-02-12 20:12:18 +00:00
Mario
e995d45b53 Merge branch 'patch-1' into 'dev'
Updated the nginx config example to meet the more modern approach.

See merge request hubzilla/core!2249
2026-02-12 19:45:21 +00:00
spirillen
669136bce7 Updated the nginx config example to meet the more modern approach. 2026-02-12 19:45:20 +00:00
Mario
2448e6df27 Merge branch 'fix-undefined-var-issues' into 'dev'
Fix various possibly undefined var issues

See merge request hubzilla/core!2264
2026-02-12 18:30:59 +00:00
Mario
4c0b37db66 fix required $x variable overwritten with attach_mkdirp() data on update 2026-02-12 18:18:00 +00:00
Mario Vavti
85d42cebe2 Merge branch '11.0RC' 2026-01-30 14:21:21 +01:00
Mario Vavti
fcafaef190 version 11.0 2026-01-30 14:20:12 +01:00
Mario
6fa8deddb5 update changelog
(cherry picked from commit 0ea3f3d36d)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-30 13:18:14 +00:00
Mario Vavti
0ea3f3d36d update changelog 2026-01-30 14:14:11 +01:00
Mario
1ef7fcda0a Remove capability to update xchan entries via api. If this functionality is required it should be implemented over a channel endpoint where xchan and channel info would be updated after verification.
(cherry picked from commit cb7dc2059a)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-30 13:00:39 +00:00
Mario Vavti
cb7dc2059a Remove capability to update xchan entries via api. If this functionality is required it should be implemented over a channel endpoint where xchan and channel info would be updated after verification. 2026-01-30 13:34:54 +01:00
Harald Eilertsen
e988bc9fae Fix various possibly undefined var issues
These are all cases where a variable is used where it may possibly be
undefined at the time of use. Typically the var is defined within a
conditional block, but is used outside of that block, or even within
other conditional blocks.

In the common case the variables will most likely be defined, but it is
possible to reach the code where they are used without the variable
being defined.

All issues discovered with PHPStan at level 1.
2026-01-29 22:31:16 +01:00
Mario
bbd60c1a31 update changelog
(cherry picked from commit cb23a9e235)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 16:28:59 +00:00
Mario
951187e540 fix issue where reply-to button is not diplayed for anonymous visitors allthough permission is granted
(cherry picked from commit 8d9623674d)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 16:28:37 +00:00
Mario Vavti
cb23a9e235 update changelog 2026-01-29 17:27:29 +01:00
Mario Vavti
8d9623674d fix issue where reply-to button is not diplayed for anonymous visitors allthough permission is granted 2026-01-29 17:25:45 +01:00
Mario
0c835c3403 update changelog
(cherry picked from commit 6d181ee69e)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 15:57:25 +00:00
Mario Vavti
6d181ee69e update changelog 2026-01-29 16:56:58 +01:00
Mario
0b491a6626 fix wrong icon class
(cherry picked from commit 2d97f8fa25)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 15:55:11 +00:00
Mario
1a0684cc55 fix issue deleting multiple files
(cherry picked from commit 98a3c97820)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 15:54:48 +00:00
Mario Vavti
2d97f8fa25 fix wrong icon class 2026-01-29 16:45:27 +01:00
Mario Vavti
98a3c97820 fix issue deleting multiple files 2026-01-29 16:36:07 +01:00
Mario
081a61a276 update changelog
(cherry picked from commit 89e1328ed0)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 11:39:34 +00:00
Mario Vavti
89e1328ed0 update changelog 2026-01-29 12:38:59 +01:00
Mario
3b516f2ded another hotfix for an issue which requires investigation
(cherry picked from commit 99a1569d07)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-29 11:36:21 +00:00
Mario
8c386191fa move html from search module to tpl
(cherry picked from commit a0cb5fcb3f)

Co-authored-by: SK <sk@utsukta.org>
2026-01-29 11:17:49 +00:00
Mario
5cb4db0353 Merge branch 'dev-send_reg_approval' into 'dev'
(Re?)enable email to user after account approval

See merge request hubzilla/core!2263
2026-01-29 10:45:25 +00:00
Mario
dc43cd9a85 Merge branch 'dev-fix-approve-email' into 'dev'
Add new send_reg_approval_email_from_register function

See merge request hubzilla/core!2262
2026-01-29 10:42:21 +00:00
Mario
5a6a7386a8 Merge branch 'more-xchan-tests' into 'dev'
tests: A few more xchan tests

See merge request hubzilla/core!2261
2026-01-29 10:38:35 +00:00
Mario
4f545e31dd Merge branch 'tests-add-ajax-request-helper' into 'dev'
tests: Add ajax request helper to module test case

See merge request hubzilla/core!2260
2026-01-29 10:35:43 +00:00
Mario
437c0a8913 Merge branch 'search-module-tpl' into 'dev'
move html from search module to tpl

See merge request hubzilla/core!2258
2026-01-29 10:33:05 +00:00
ltning
268fccdb30 Fix SQL in send_reg_confirmation_email_from_register 2026-01-28 20:06:59 +00:00
ltning
bb7689be93 Return boolean instead of nonexistent array 2026-01-28 02:51:58 +00:00
ltning
bd63af69b2 (Re?)enable email to user after account approval
Existing function account_allow() does not seem to be used a whole
lot, and certainly not when approving a registration request.

Created new function send_reg_confirmation_email_from_register
(going for longest-function-name-in-Hubzilla) which attempts to
send such a confirmation email, and also a new email template.

Plugged this into the create_account_from_register function.

Open question: Should the sending of this email be among the success
criteria for the create_account_from_register function?
2026-01-28 02:51:46 +00:00
ltning
e5d4358d61 Clean up send_reg_approval_email_from_register, new email template
Since there is no easy way to produce direct links to approve/reject
registrations, create new email template which simply links to the
Accounts admin page.

In response to comments:
 - Add type annotations
 - Simplify some 'if' statements
 - add and fix some whitespace
 - simplify input arguments to the function
2026-01-27 16:32:16 +00:00
ltning
0fa4962620 Clean up some whitespace confusion in the previous commit 2026-01-27 16:32:16 +00:00
ltning
fe4d6229a4 Add new send_reg_approval_email_from_register function
Current send_reg_approval_email function expects account to already
exist, and does not seem to be called anywhere. New function takes
necessary information from the register table.

Added call to new function in Regate.php after verification is
done. Will fail and roll back if the function call fails or if no
admin accounts were found to notify (delivered<1).
2026-01-27 16:32:16 +00:00
Mario
10c5de4f6e another hotfix for an issue which requires investigation
(cherry picked from commit 99a1569d07)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-27 11:27:01 +00:00
Mario Vavti
99a1569d07 another hotfix for an issue which requires investigation 2026-01-27 12:26:06 +01:00
Harald Eilertsen
9fb5cd12be tests: A few more xchan tests 2026-01-27 09:43:44 +01:00
Harald Eilertsen
d85c737db7 tests: Add ajax request helper to module test case
This is a bit crude, but serves the purpose for now. To emulate an AJAX
request we set the `Content-Type` header to `application/json`, and json
encode the params in the body of the request.
2026-01-26 23:03:07 +01:00
Mario Vavti
9657fc50e9 Merge branch 'dev' into 11.0RC 2026-01-25 13:06:34 +01:00
Mario
de468a29b0 hotfix for issue with the potential to stuff up the queuewrker
(cherry picked from commit 794b456b8a)

Co-authored-by: Mario Vavti <mario@mariovavti.com>
2026-01-25 12:05:49 +00:00
Mario Vavti
794b456b8a hotfix for issue with the potential to stuff up the queuewrker 2026-01-25 13:02:15 +01:00
SK
a0cb5fcb3f move html from search module to tpl 2026-01-22 15:26:20 +05:30
Mario
0d382634ec Merge branch 'access_dropdown-tpl' into 'dev'
move html from accesslist module to tpl

See merge request hubzilla/core!2257
2026-01-21 13:24:35 +00:00
SK
225c83dfbe move html from accesslist module to tpl 2026-01-19 14:00:39 +05:30
Mario Vavti
083c4c95d1 Merge branch 'dev' into 11.0RC 2026-01-18 13:07:17 +01:00
Mario Vavti
e736945f1d update changelog 2026-01-18 13:06:42 +01:00
Mario Vavti
4a2e5add36 update changelog 2026-01-18 12:56:59 +01:00
Mario Vavti
067a79a778 Merge branch 'dev' into 11.0RC 2026-01-17 13:02:37 +01:00
Mario
cb0102b971 Merge branch 'channel-activities-header' into 'dev'
move channel-activities header to its tpl

See merge request hubzilla/core!2254
2026-01-17 11:44:55 +00:00
Saiwal K
8b46767d30 move channel-activities header to its tpl 2026-01-17 11:44:54 +00:00
Mario Vavti
26b5dabe72 Merge branch 'dev' into 11.0RC 2026-01-16 12:38:41 +01:00
Mario Vavti
3f39d0d249 fix cloud root folder shows unknown error 2026-01-16 12:38:02 +01:00
Mario Vavti
2b4d2baa7b version 2026-01-16 10:35:29 +01:00
Mario Vavti
3130a94a4c bump redbasic min/max version 2026-01-16 10:27:54 +01:00
Mario Vavti
d44c004bd0 bump dev version 2026-01-15 17:24:39 +01:00
Mario Vavti
c655046e1f version, strings and updated util/run_xgettext.sh 2026-01-15 17:13:32 +01:00
Mario Vavti
b32c1c1e22 composer update 2026-01-15 14:50:39 +01:00
Mario
38f040f9b5 Merge branch 'minor-test-improvements' into 'dev'
tests: Make addonname/tests base addon tests dir

See merge request hubzilla/core!2250
2026-01-15 10:53:57 +00:00
Mario
7dcaebf281 Merge branch 'italian-translation' into 'dev'
Italian translation

See merge request hubzilla/core!2253
2026-01-15 10:53:04 +00:00
Mario
fad5f98405 Merge branch 'bookmarks-layout-tpl' into 'dev'
move html from bookmarks module to its tpl

See merge request hubzilla/core!2255
2026-01-15 10:48:01 +00:00
SK
ea07bd1693 move html from bookmarks module to its tpl 2026-01-15 02:15:26 +05:30
Mario
4a7b2e92a5 make sure we deal with a string
(cherry picked from commit b6153edf6b)

Co-authored-by: Mario <mario@mariovavti.com>
2026-01-08 19:50:31 +00:00
Mario Vavti
deaab14c5f revert revert 2026-01-07 11:37:22 +01:00
Mario Vavti
8258b8b088 revert 2026-01-07 10:52:22 +01:00
Mario Vavti
db5c217a21 reuse the stored filetype 2026-01-07 10:48:11 +01:00
prealpinux
724ee7dbab Italian translation 2026-01-06 22:40:30 +01:00
Mario Vavti
fb9fe0d3c4 bump version 2026-01-05 15:43:32 +01:00
Mario Vavti
9019636449 update attach.filetype field length to match photo.mimetype field length and attempt to find mimetype with finfo class if applicable. 2026-01-05 15:34:42 +01:00
Mario Vavti
933b4fbcfe bump version 2026-01-05 12:21:55 +01:00
Mario Vavti
7675ed0145 add (not exactly correct -> see fixme) mime types for docx and xlsx 2026-01-05 12:14:01 +01:00
Mario
73e7f0aad2 Escape args in xchan_fetch
(cherry picked from commit cedc6c4230)

Co-authored-by: Harald Eilertsen <haraldei@anduin.net>
2026-01-04 19:58:46 +00:00
Mario
f25211d7ff Merge branch 'xchan-fetch-escape-args' into 'dev'
Escape args in xchan_fetch

See merge request hubzilla/core!2252
2026-01-04 19:53:51 +00:00
Mario
30cf6d827c Merge branch 'issue-1960-fix-grammar-in-enotify' into 'dev'
Fix grammar in Enotify::submit

See merge request hubzilla/core!2251
2026-01-04 19:49:42 +00:00
Mario
1323ea8e18 Merge branch 'dev' into 'dev'
move shared post formatting to its own tpl

See merge request hubzilla/core!2248
2026-01-04 19:36:06 +00:00
Saiwal K
9f98f6bbd5 move shared post formatting to its own tpl 2026-01-04 19:36:06 +00:00
Harald Eilertsen
cedc6c4230 Escape args in xchan_fetch 2026-01-04 14:55:03 +01:00
Mario Vavti
b6a58fbf6e make sure editor init also works from the tiles view 2026-01-03 19:31:56 +01:00
Mario Vavti
e295765bef fix typo and wrongedited timestamp. also add data-{id, type} to cloud directory listings 2026-01-02 15:31:54 +01:00
Harald Eilertsen
141dc2fdb7 Fix grammar in Enotify::submit
Fixes issue #1960: https://framagit.org/hubzilla/core/-/issues/1960
2025-12-29 20:37:20 +01:00
Harald Eilertsen
c051c4d0aa tests: Make addonname/tests base addon tests dir
This makes it easier to treat anything under tests/ as namespaced for
PSR-4 autoloading.
2025-12-21 11:05:37 +01:00
Harald Eilertsen
e289078f82 tests: Fix content-type/length in Module\TestCase 2025-12-21 11:04:25 +01:00
Mario
87f79381d9 MessageFilter: move decoding to the caller and also check the summary field 2025-12-19 08:24:22 +00:00
Mario
98840ae1d0 fix deprecation warning 2025-12-16 12:07:20 +00:00
Mario
28746891c8 curl_close() is deprected and noop since PHP version 8.0 2025-12-16 11:47:10 +00:00
Mario
e5d0ef79ef GD imagedestroy() is deprected and noop since PHP version 8.0 2025-12-16 11:16:28 +00:00
Mario Vavti
2eb51233f6 fix hashtag and mentions count after fixing encoding and add tests 2025-12-12 19:15:28 +01:00
Mario Vavti
5432819788 add test for encoded filter entry 2025-12-12 18:51:53 +01:00
Mario
c03f543b54 encoding issue 2025-12-12 17:44:27 +00:00
Mario
de6506eb57 bump version 2025-12-12 11:53:31 +00:00
Mario
fb48bbe2c1 fix replies id reference 2025-12-12 11:52:05 +00:00
Mario
65132c8fdc Merge branch 'dev' of https://framagit.org/hubzilla/core into dev 2025-12-12 11:44:45 +00:00
Mario
2c0936187a improved replies id detection 2025-12-12 11:44:08 +00:00
Mario Vavti
48dbba2f4d implement new version of MessageFilter derived from forte and add some more tests 2025-12-12 12:42:01 +01:00
Mario
16068af0bb remove sodium-plus 2025-12-10 18:42:08 +00:00
Mario
d09b01245f generate password hash with PBKDF2 instead of built in hash function based on argon2 which too resource heavy for our usecase (see comments) also the nonsumo library is almost half in size - we ship both libs now but only use the nonsumo. This fixes crypto for chromium mobile versions. drawback: this change is not backward compatible. 2025-12-10 18:37:18 +00:00
Mario
bdd0b0a6fb sodium-plus is not maintained anymore - use https://github.com/jedisct1/libsodium.js (sumo version because it contains the crypto_pwhash() function) directly instead 2025-12-10 09:41:25 +00:00
Mario
48030617ab check for sodium in decrypt function 2025-12-10 07:34:36 +00:00
Mario
bb49a51be3 feed: fix channel links and check for top=0. also filter out Add and Remove activities when building the feed (we should probably do this in fetch_items() but that will require some refactoring 2025-12-09 12:05:19 +00:00
Mario
da266f5739 Merge branch 'dev' into 'dev'
add active themes list to siteinfo

See merge request hubzilla/core!2247
2025-12-09 11:34:05 +00:00
Mario
f6a4997d6f fix issue #1954 2025-12-09 11:29:51 +00:00
Mario
7a99089204 refactor module feed to allow a (for now) hidden pconfig to allow switching between simple (toplevels only) and complete activity feed, defaulting to simple. Also reomve option for json formatted feed since it is not supported in the backend - issue #1953 2025-12-09 10:51:07 +00:00
Mario
2e55973da2 remove ofeed and ochannel modules which were here to serve deprecated and removed ostatus 2025-12-09 10:02:00 +00:00
Mario
80f4eea9e9 remove support for deprecated AS1 verbs and objects in the network stream filters - improves performance 2025-12-09 08:26:41 +00:00
Mario
be5c8aa2a3 more work on determining whether an event should be adjusted is an all-day event 2025-12-09 08:24:53 +00:00
Mario
643d25b34b simplify and be slightly more strict to define allday events 2025-12-05 22:38:20 +00:00
Mario Vavti
c29d79854e Merge branch 'dev' of https://framagit.org/hubzilla/core into dev 2025-12-05 22:24:33 +01:00
Mario
2834544451 an attempt to better deal with events that have no endTime and events that inline timezone offset in the timestamp 2025-12-05 21:16:30 +00:00
Mario Vavti
60eb8aa42b provide the event hash and timezone in the event object 2025-12-05 22:04:52 +01:00
Mario
aff39521cd revert removing of iconfig parsing (it is still required for events for now). Dismiss activitypub.rawmsg and diaspora.fields from parsing 2025-12-05 19:24:28 +00:00
Mario
f19ad4b087 bump version 2025-12-05 09:26:19 +00:00
Mario
fc243196d9 store the object cache right on and remove some commented out code 2025-12-05 09:19:26 +00:00
SK
42e78d9666 add active themes list to siteinfo
Helps choosing instance when creating clones for compatible themes.
2025-12-05 07:36:13 +05:30
Mario
26d1653bfb Merge branch 'hooks-update-api-docs' into 'dev'
Update API docs for Hook interface

See merge request hubzilla/core!2245
2025-12-04 18:41:02 +00:00
Mario Vavti
f39ccab6c1 update composer libs 2025-12-04 14:24:20 +01:00
Mario
a0e6dcbb77 allow the post_mail permission to work independend from the send_stream/post_comment permissions and leave the post_mail perm limit at PERMS_SPECIFIC for the public channel role - issue #1951 2025-12-04 12:31:04 +00:00
Mario
320a0c1b62 only reset obj cache if edited timestamp has changed 2025-12-03 21:02:16 +00:00
Mario
84d52a9ad1 Merge branch 'improve-test-isolation' into 'dev'
Improve test isolation

See merge request hubzilla/core!2246
2025-12-03 19:33:27 +00:00
Mario
dc298ce6c1 bump version 2025-12-03 19:31:11 +00:00
Mario
b464cd6181 this is somehow delicate: make sure we will not relay an item again in case it comes back to us from a channel that sources our channel 2025-12-03 19:05:17 +00:00
Harald Eilertsen
4eb7e29bab Improve test isolation
To ensure tests don't step on each others toes, make sure we back up
the static properties of the global App class before running tests that
modify any of these properties.
2025-12-02 19:58:41 +01:00
Mario
63243808b8 add quoteUri for mastodon which seems to have removed support for quoteUrl 2025-12-02 12:23:38 +00:00
Mario
f271448767 also remove the comment now that we do not use the effective_uid param anymore 2025-12-02 11:57:51 +00:00
Mario
3915164bb4 we do not need to decode iconfig anymore 2025-12-02 10:24:54 +00:00
Harald Eilertsen
94e5aa172c Update API docs for hooks 2025-11-29 22:52:30 +01:00
Harald Eilertsen
ff68bee174 Doxygen: New tag @sideeffect
Use this tag to document unexpected side effects of an API.
2025-11-29 22:49:54 +01:00
Mario
25aa7d5738 fix activity signer not set 2025-11-27 10:10:24 +00:00
Mario
07e3a218cb only check author and owner mismatch 2025-11-27 09:59:33 +00:00
Mario
e6ad00dabc lookup only one item 2025-11-26 10:07:10 +00:00
Mario
23d781e348 improved checks 2025-11-26 08:39:34 +00:00
Mario
b53cacd54c bump version 2025-11-26 07:38:46 +00:00
Mario
ade3da4d89 dismiss objects where the force flag is set for now 2025-11-25 22:50:47 +00:00
Mario
1e5f614a5e Revert "more check possible ownership mismatch"
This reverts commit 159c8dbd5f.
2025-11-25 11:03:27 +00:00
Mario
159c8dbd5f more check possible ownership mismatch 2025-11-25 10:23:03 +00:00
Mario
5faa285f6f version 2025-11-25 08:38:53 +00:00
Mario
c9af19cdb2 check possible ownership mismatch 2025-11-25 08:38:05 +00:00
Mario
3e74e4784c move ObjCache::Set() from Activity::decode_note() to Activity::store() and Libzot::import() where we have already gathered more infos 2025-11-24 22:34:29 +00:00
Mario
bcbd7f4f6d bump version 2025-11-24 07:53:41 +00:00
Mario
7ead6086dc remove trailing slash 2025-11-24 07:49:30 +00:00
Mario
4685568e0d item links will be replaced at the sync receiver side - use the channel url of the current site when storing. fixes issue #1932 2025-11-24 07:48:54 +00:00
Mario
ec57f8293a refine shre to quote conversion 2025-11-23 10:50:21 +00:00
Mario
9e5344d624 fix typo in last commit 2025-11-22 19:57:28 +00:00
Mario
4c122fd3b3 do not attempt tag delivery if not item type post - fix issue #1941 2025-11-22 19:54:58 +00:00
Mario
d318a1b807 make postgres happy
(cherry picked from commit 7959dd9f57)

Co-authored-by: Mario <mario@mariovavti.com>
2025-11-21 22:05:10 +00:00
Mario
7959dd9f57 make postgres happy 2025-11-21 21:54:15 +00:00
Mario
fde03c7ae0 remove commented out code 2025-11-21 20:50:14 +00:00
Mario
7fcc770fbf rename (un)serialise() -> json_(un)serialize() 2025-11-21 20:43:35 +00:00
Mario Vavti
dcb09a8b39 update smarty and base58 2025-11-21 16:45:54 +01:00
Mario
57c22f4d0f fix test 2025-11-21 10:45:45 +00:00
Mario
ed1cfa5c7b fix block/unblock account - issue #1947 2025-11-21 09:25:51 +00:00
Mario
3878dbd6bd show viewsource link for pubstream items 2025-11-21 08:27:53 +00:00
Mario
554577fad7 update changelog 2025-11-21 07:04:28 +00:00
Mario
3ac99479fe Merge branch 'dev' of https://framagit.org/hubzilla/core into dev 2025-11-20 23:06:02 +00:00
Mario
7cadb43029 fix insufficient target attribution for forums - channel_url() requires the channel array not the address 2025-11-20 23:05:12 +00:00
Mario
f25ac63f18 Merge branch 'fix-warnings-in-owatest' into 'dev'
Set content type/length for module test requests

See merge request hubzilla/core!2244
2025-11-20 21:20:27 +00:00
Mario
8a79d8d06f Merge branch 'more-tests+speedups' into 'dev'
More tests + speed improvements

See merge request hubzilla/core!2243
2025-11-20 20:49:55 +00:00
Harald Eilertsen
ddf7fad82f Set content type/length for module test requests
Just use default values of 'text/html' and 0 for now. Should probably be
set to more realistic values at some point, but for now it seems to
work.

This fixes warnings in the OwaTest.
2025-11-20 21:48:53 +01:00
Mario
6a9d569d5a singleton object cache - initial commit 2025-11-20 20:41:40 +00:00
Mario
ad017baaa6 singleton object cache - initial commit 2025-11-20 20:41:20 +00:00
Mario
b5d673c102 Merge branch 'test-reload-fixtures-from-db' into 'dev'
Reload fixures from db

See merge request hubzilla/core!2242
2025-11-20 07:21:16 +00:00
Mario Vavti
d7fa707f00 Merge branch 'dev' of https://framagit.org/hubzilla/core into dev 2025-11-20 08:02:58 +01:00
Mario Vavti
b4b7ec1693 cross protocol message payload restructuring - initial commit 2025-11-20 07:58:50 +01:00
Harald Eilertsen
6f8b7f177d Replace use of create_identity in MagicTest
Use db fixtures instead. Make the test much snappier.
2025-11-19 22:01:42 +01:00
Harald Eilertsen
7a2a621309 Add hubloc test db fixture
Added entries in the hubloc table to correspond to the channel entries
in channel.yml. These would normally be created by `create_identity`, so
some code expects them to be there.
2025-11-19 21:59:45 +01:00
Harald Eilertsen
fe25bab3a7 Stub crypto calls from CreateIdentityTest
This speeds up the test runs significantly.
2025-11-19 16:58:08 +01:00
Harald Eilertsen
3abd764512 Drop create_identity from Zotfinger test 2025-11-16 22:14:33 +01:00
Harald Eilertsen
dc255a4ecf Begin tests for Profiles module 2025-11-15 11:26:23 +01:00
Harald Eilertsen
fc201ae067 Fix warnings in profile_edit template 2025-11-15 11:26:23 +01:00
Harald Eilertsen
de5455b401 Fix warnings for optional args in field templates 2025-11-15 11:26:22 +01:00
Harald Eilertsen
1243207b8f Fix undefined vars in Profiles module 2025-11-15 11:26:22 +01:00
Harald Eilertsen
39f4a56348 Begin tests for Zotfinger module 2025-11-15 11:26:11 +01:00
Mario
d48c9ce562 only unserialis(z)e if we deal with a string 2025-11-14 18:34:25 +00:00
Mario
6ced694b53 more streamline with jsalmon signature removal 2025-11-14 14:10:46 +00:00
Mario
bc128c604b streamline with jsalmon signature removal 2025-11-14 12:35:29 +00:00
Mario
a2168870b9 add a hint vor cahed state 2025-11-14 12:20:27 +00:00
Mario
c0197b698e fix undefined var 2025-11-14 12:01:58 +00:00
Mario
f2ae99f64e adapt mod viewsrc 2025-11-14 10:06:19 +00:00
Mario
14f6667687 more json serialisation for iconfig 2025-11-14 10:05:59 +00:00
Mario Vavti
6f9348540e Merge branch 'dev' of https://framagit.org/hubzilla/core into dev 2025-11-14 10:45:29 +01:00
Mario Vavti
51b667020e use json serialisation for iconfig 2025-11-14 10:44:56 +01:00
Mario
c6d54d03de try to provide the complete raw object in viewsrc 2025-11-13 20:43:56 +00:00
Harald Eilertsen
8e35ef2faa Add test db fixtures for channel table
Add the system user and a test user to the table for use by tests.
2025-11-13 19:51:39 +01:00
Harald Eilertsen
b74a2dff9e Reload test fixtures from db
Reload inserted rows from the DB when loading the test fixtures. This
ensures that we get all default fields and id columns initialized and
validated. Some code under test will require this to work as expected.
2025-11-13 19:51:39 +01:00
Harald Eilertsen
6400bcc81f Set logged in shannel for delete account test
If the logged in account is not set, it triggers a warning.
2025-11-13 19:51:39 +01:00
Harald Eilertsen
4abe0703d7 tests: Use root passwd to set up MySQL test db 2025-11-13 19:51:39 +01:00
Mario
4b389ddba9 Merge branch 'dba-fix-insert' into 'dev'
Improve dba_pdo::insert function

See merge request hubzilla/core!2241
2025-11-13 16:14:06 +00:00
Harald Eilertsen
1e92aeb7f9 Improve dba_pdo::insert function 2025-11-13 16:14:05 +00:00
Mario
b6153edf6b make sure we deal with a string 2025-11-13 09:50:46 +00:00
Mario
93c333a9bc bump version 2025-11-11 11:49:23 +00:00
Mario
43142dde9f OWA2: initial commit 2025-11-11 11:47:57 +00:00
Mario
63bc905061 wrong order of args 2025-11-10 21:03:22 +00:00
Mario
573c6f3df3 shares kiss 2025-11-10 19:50:10 +00:00
Mario
28692b867c nesting shares will be too messy in regard to interop. so when sharing a post with shares, just pick the shares. 2025-11-10 09:25:31 +00:00
Mario
74b8f1f240 Revert "correctly parse and display nested shares"
This reverts commit 0be74299ca.
2025-11-10 09:01:38 +00:00
Mario
0be74299ca correctly parse and display nested shares 2025-11-10 08:01:18 +00:00
Mario
866d88de53 fix quote issue with forums 2025-11-07 20:29:51 +00:00
Mario
69acee497d bump dev version 2025-11-07 19:57:49 +00:00
Mario Vavti
6c74672d40 bump composer PHP version to 8.2 and update libs 2025-11-07 20:55:45 +01:00
Mario
6320506c27 update install document 2025-11-07 19:36:56 +00:00
Mario
1ee0f3ce1d remove unused sprintf.js 2025-11-07 19:23:53 +00:00
Mario
0730d31c22 move twitteroauth to addon_common 2025-11-07 19:04:04 +00:00
Mario
a2a8b2e9fb move slinky to addon_common 2025-11-07 18:57:18 +00:00
Mario
8821fa9a0e this is only used by the openid addon. move it there. 2025-11-07 18:40:14 +00:00
Mario
363100a612 this is only used by the wppost addon. move it there. 2025-11-07 15:09:08 +00:00
Mario
1f7ac5e787 this is only usewd by the wiki addon. move it there. 2025-11-07 14:59:16 +00:00
Mario
2233a0317e remove bootbox lib and unused code 2025-11-07 14:52:30 +00:00
Mario
498ef78e6f remove unused images 2025-11-07 14:45:12 +00:00
Mario
8a606365c8 update path 2025-11-07 14:42:52 +00:00
Mario
9c27f94709 move language specific folders into its own subfolder in /view 2025-11-07 14:38:55 +00:00
Mario
a99e067b91 remove deprecated sjcl library and references 2025-11-07 12:39:58 +00:00
Mario
831f4324ae fix fullscreen button class 2025-11-07 11:59:54 +00:00
Mario
ad205abd90 Merge branch 'profile-visibility-editor-tpl' into 'dev'
updated profile visibility editor to use tpl file for layout

See merge request hubzilla/core!2240
2025-11-07 11:13:38 +00:00
Mario
231f4a28eb remove deprecated code 2025-11-05 20:47:38 +00:00
Mario
95b52b6aa9 adapt mod dreport to the changes in 385c23a2b 2025-11-05 20:43:50 +00:00
Mario
385c23a2b6 fix delivery report when syncing cloned channels 2025-11-05 20:29:29 +00:00
Mario
3a81edbcb0 bump version 2025-11-04 10:45:55 +00:00
Mario
2f377089e6 deprecate outbound JSalmon signatures 2025-11-04 10:45:15 +00:00
Mario
6427e84053 release date 2025-11-04 09:29:51 +00:00
SK
73b931ed14 updated building profile visibility editor to use tpl file for layout 2025-11-01 01:47:30 +05:30
657 changed files with 37168 additions and 73178 deletions

151
CHANGELOG
View File

@@ -1,3 +1,154 @@
Hubzilla 11.2.1 (2026-05-20)
- Fix channel creation failing in some situations
- Drop payloads with unsafe json-ld keys in LDSignatures::verify() and add tests
- Fix App::$profile set for removed channels
- Fix MessageFilter breaking when expecting string but array is given
- Superblock: fix blocking failed in some setups (sponsored by NLnet NGI0 Commons Fund/Superblock)
- Superblock: fix missing import of attribute (sponsored by NLnet NGI0 Commons Fund/Superblock)
- Superblock: improved detection of reshares (sponsored by NLnet NGI0 Commons Fund/Superblock)
- Cards: fix PHP warning
- Gallery: fix PHP warnings and only implement observer related javascript if there actually is an observer
- Diaspora: fix missing local namespace for thr_parent
Hubzilla 11.2 (2026-03-26)
Features
- Introduce parse_webbie() for preparing webbies and URLs for webfinger usage
- Allow to override cUrl useragent
- New HQ system status widget for admins (sponsored by NLnet NGI0 Commons Fund/Performance Profiling)
- Refactor drop_query_params() to deal with array params and add test
Maintenance
- Remove private members from API docs
- Remove custom CA certs
- Add type annotations in Extend\Route
- Set method visibility in Extend\Route
- Add API docs and licence info to Extend\Route
- Add a short sleep interval to Activity::init_background_fetch() when adding new work items in a loop
- Add a short sleep interval to the convo daemon loop to spread the load for large collections
- Use PHP matching rules in util/run_xgettext
- Store translation templates as .pot instead of .po
- Deprecate NULL_DATE constant in favor of DBA::get_null_date()
- Composer require guzzlehttp/psr7
- Update composer libs
- Move HQ channel notifications widget HTML to template
- Deprecate tags and attachment in activities
- Update the nginx config example to meet the more modern approach
Bugfixes
- Fix route and widget register() not deduplicating entries
- Fix issue in route and widget unregister() where we unregistered even if only one of the two arguments did not match
- Fix issue in Storage/Directory where we returned a partial path instead of throwing exception if a directory of a path could not be found
- Fix possible endless loop in externals daemon
- Fix fatal error in italian translation file
- Fix mod network not displaying direct messages when filters active - issue #1973
- Fix ghost notifications with reshared items - issue #1970
- Fix issue with double typed objects in Lib/Activity
- Fix events displaying event timezone instead of adjusted to timezone
- Fix duplicated terms in activity object
- Fix last modified timestamp not updating in attach_store()
Addons
- Wopi: fix headers already set warning when serving the file to the client
- Superblock: complete rewrite with extended functionality and added tests for version 3.0 (sponsored by NLnet NGI0 Commons Fund/Superblock)
- Diaspora: use Diaspora/2 useragent when fetching hcards to prevent being redirected to some shady bot guard
- Add composer config and autoload files for addons
- Wopi: return early in construct_page hook and
- Wopi: fix wrong hook name in uninstall function
Hubzilla 11.0 (2026-01-30)
Features
- Rewrite Lib/MessageFilter (ported from forte) and add more tests
- Rewrite editor encryption feature to implement libsodium and PBDKF2 for password hashing
- Change default feed behaviour to return only toplevels - issue #1953
- Add active themes list to siteinfo
- Show viewsource link for pubstream items
- Implement singleton object cache
- Restructure cross protocol message payload
- View source will now display the raw object instead of just the object body
- Implement OWA2
Maintenance
- Remove capability to update xchan entries via api
- Move mod search HTML to template
- Move channel activities HTML to template
- Update util/run_xgettext.sh to ignore not relevant directories
- Update composer libs
- Move mod bookmarks HTML to template
- Update italian translation
- Use finfo class to determine mime type in attach_store()
- Bump attach.filetype field length to match photo.mimetype
- Move share container HTML to template
- Update cloud directory template to provid necessary data for the WOPI addon
- Remove appearances of curl_close() - deprected and noop since PHP version 8.0
- Remove appearances of GD imagedestroy() - deprected and noop since PHP version 8.0
- Improve replies id detection
- Remove deprecated ofeed and ochannel modules
- Remove support for deprecated AS1 verbs and objects in the network stream filters
- Improve detection of allday events
- Improve handling of events with no endTime
- Provide the event hash and timezone in the event object
- Improve test isolation
- Add quoteUri field to activities
- Update API docs for hooks
- Improve share to quote conversion
- Rename (un)serialise() -> json_(un)serialize()
- CI: Replace use of create_identity in MagicTest
- CI: Add test db fixtures for hubloc table
- CI: Stub crypto calls from CreateIdentityTest (performance)
- CI: Add tests for Profiles module
- CI: Add tests for Zotfinger module
- Use json serialisation for iconfig
- CI: Add test db fixtures for channel table
- CI: Reload test fixtures from db
- CI: Set logged in channel for delete account test
- CI: Use root passwd to set up MySQL test db
- Improve dba_pdo::insert function
- Update install document
- Remove unused sprintf.js
- Move twitteroauth lib to addon_common
- Move slinky lib to addon_common
- Move openid lib to openid addon
- Move XRI lib to wppost addon
- Move diff lib to wiki addon
- Remove unused bootbox lib
- Remove unused images
- Move language specific folders into its own subfolder in /view
- Remove deprecated sjcl lib and references
- Deprecate outbound JSalmon signatures
Bugfixes
- Fix reply-to button not diplayed for anonymous visitors allthough permission is granted
- Fix wrong icon class in mod cloud
- Fix bulk deleting files
- Fix cloud root folder shows unknown error
- Fix edited timestamp in attach_store() in case of update
- Fix grammar in Lib/Enotify::submit()
- Fix content-type and length header in Module\TestCase
- Fix args in xchan_fetch() not escaped
- Fix notification for events linking to wrong message id - issue #1954
- Fix post_mail permission not working independend from the send_stream/post_comment permissions - issue #1951
- Fix item relayed again in case it comes back from a channel that sources our channel
- Fix activity signer not set
- Fix category link not update at clone - issue #1932
- Fix tag delivery attempted item type is not post - fix issue #1941
- Fix block/unblock account - issue #1947
- Fix warnings in profile_edit template
- Fix warnings for optional args in field templates
- Fix undefined vars in mod profiles
- Fix fullscreen button class
- Fix delivery report when syncing cloned channels
Addon
- New addon implementing basic WOPI protocol - integrates collabora with the Files app
- Redphotos: addon removed - was used for migration from redmatrix to hubzilla
- Redfiles: addon removed - was used for migration from redmatrix to hubzilla
- Nsfw: rewrite to implement new MessageFilter
- Wiki: fix long loading time due to oembed attempts
- Wiki: improved SQL query
Hubzilla 10.6.1 (2025-11-21)
- Fix insufficient target attribution for forums
- Fix reshare regression in forum logic

View File

@@ -38,7 +38,6 @@ class PermissionRoles {
];
$ret['limits'] = PermissionLimits::Std_Limits();
$ret['limits']['post_comments'] = PERMS_AUTHED;
$ret['limits']['post_mail'] = PERMS_AUTHED;
$ret['limits']['post_like'] = PERMS_AUTHED;
$ret['limits']['chat'] = PERMS_AUTHED;
break;

View File

@@ -212,6 +212,7 @@ class Permissions {
* @return array Associative array with
* * \e array \b perms Permission array
* * \e int \b automatic 0 or 1
* * \e srtring \b role
*/
static public function connect_perms($channel_id) {
@@ -230,70 +231,6 @@ class Permissions {
}
}
// look up the permission role to see if it specified auto-connect
// and if there was no permcat or a default permcat, set the perms
// from the role
/*
$role = get_pconfig($channel_id, 'system', 'permissions_role');
if ($role) {
$xx = PermissionRoles::role_perms($role);
if ($xx['perms_auto'])
$automatic = 1;
if ((!$my_perms) && ($xx['perms_connect'])) {
$default_perms = $xx['perms_connect'];
$my_perms = Permissions::FilledPerms($default_perms);
}
}
*/
// If we reached this point without having any permission information,
// it is likely a custom permissions role. First see if there are any
// automatic permissions.
/*
if (!$my_perms) {
$m = Permissions::FilledAutoperms($channel_id);
if ($m) {
$automatic = 1;
$my_perms = $m;
}
}
*/
// If we reached this point with no permissions, the channel is using
// custom perms but they are not automatic. They will be stored in abconfig with
// the channel's channel_hash (the 'self' connection).
/*
if (!$my_perms) {
$r = q("select channel_hash from channel where channel_id = %d",
intval($channel_id)
);
if ($r) {
$x = q("select * from abconfig where chan = %d and xchan = '%s' and cat = 'my_perms'",
intval($channel_id),
dbesc($r[0]['channel_hash'])
);
if ($x) {
foreach ($x as $xv) {
$my_perms[$xv['k']] = intval($xv['v']);
}
}
}
}
*/
return (['perms' => $my_perms, 'automatic' => $automatic, 'role' => $pc]);
}
/*
static public function serialise($p) {
$n = [];
if ($p) {
foreach ($p as $k => $v) {
if (intval($v)) {
$n[] = $k;
}
}
}
return implode(',', $n);
}
*/
}

View File

@@ -6,6 +6,7 @@ use Zotlabs\Lib\Activity;
use Zotlabs\Lib\ActivityStreams;
use Zotlabs\Lib\ASCollection;
use Zotlabs\Lib\ASCache;
use Zotlabs\Lib\Config;
class Convo {
@@ -33,6 +34,7 @@ class Convo {
}
$force = $argv[4] ?? false;
$interval = Config::Get('queueworker', 'queue_interval', 500000);
foreach ($channels as $channel_id) {
$channel = channelx_by_n($channel_id);
@@ -46,6 +48,8 @@ class Convo {
}
foreach ($messages as $message) {
$network_fetch = false;
if (is_string($message)) {
$cached = ASCache::Get($message);
if ($cached) {
@@ -54,6 +58,8 @@ class Convo {
}
else {
// logger('convo_fetching: ' . $message);
$network_fetch = true;
$data = Activity::fetch($message, $channel);
if ($data) {
ASCache::Set($message, $data);
@@ -65,6 +71,12 @@ class Convo {
$data = $message;
}
if (!$network_fetch) {
// Add some delay so that the DB will not be overwhelmed
// Fetched from network will already have a slight delay
usleep($interval);
}
$AS = new ActivityStreams($data);
if ($AS->is_valid() && is_array($AS->obj)) {
$item = Activity::decode_note($AS);

View File

@@ -2,7 +2,9 @@
namespace Zotlabs\Daemon;
use DBA;
use Zotlabs\Lib\Config;
use Zotlabs\Lib\ObjCache;
use Zotlabs\Lib\Libsync;
use Zotlabs\Lib\Libzotdir;
@@ -92,7 +94,7 @@ class Cron {
// delete expired access tokens
$r = q("select atoken_id from atoken where atoken_expires > '%s' and atoken_expires < %s",
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
db_utcnow()
);
if ($r) {
@@ -236,6 +238,73 @@ class Cron {
if (!$restart)
Master::Summon(array('Cronhooks'));
// move as obj cache to fs
if (!Config::Get('system', 'as_objects_moved')) {
$results = dbq("select iconfig.*, item.mid from iconfig left join item on iid = item.id where cat = 'activitypub' and k = 'rawmsg' limit 300");
if ($results) {
foreach ($results as $result) {
if (is_string($result['v'])) {
if (str_starts_with($result['v'], '{')) {
$result['v'] = json_decode($result['v'], true);
}
elseif (str_starts_with($result['v'], 'json:')) {
$result['v'] = json_unserialize($result['v']);
}
elseif (preg_match('|^a:[0-9]+:{.*}$|s', $result['v'])) {
$result['v'] = unserialize($result['v'], ['allowed_classes' => false]);
}
}
if (is_array($result['v'])) {
ObjCache::Set($result['mid'], $result['v']);
}
q("delete from iconfig where id = %d",
intval($result['id'])
);
}
}
else {
Config::Set('system', 'as_objects_moved', 1);
}
}
// move diaspora obj cache to fs
if (!Config::Get('system', 'diaspora_objects_moved')) {
$results = dbq("select iconfig.*, item.mid from iconfig left join item on iid = item.id where cat = 'diaspora' and k = 'fields' limit 300");
if ($results) {
foreach ($results as $result) {
if (is_string($result['v'])) {
if (str_starts_with($result['v'], '{')) {
$result['v'] = json_decode($result['v'], true);
}
elseif (str_starts_with($result['v'], 'json:')) {
$result['v'] = json_unserialize($result['v']);
}
elseif (preg_match('|^a:[0-9]+:{.*}$|s', $result['v'])) {
$result['v'] = unserialize($result['v'], ['allowed_classes' => false]);
}
}
if (is_array($result['v'])) {
ObjCache::Set($result['mid'], $result['v'], 'diaspora');
}
q("delete from iconfig where id = %d",
intval($result['id'])
);
}
}
else {
Config::Set('system', 'diaspora_objects_moved', 1);
}
}
Config::Set('system', 'lastcron', datetime_convert());
//All done - clear the lockfile

View File

@@ -74,6 +74,8 @@ class Externals {
}
}
$attempts++;
if (!$url) {
continue;
}
@@ -85,7 +87,6 @@ class Externals {
$blacklisted = true;
}
$attempts++;
// make sure we can eventually break out if somebody blacklists all known sites

View File

@@ -4,6 +4,8 @@ namespace Zotlabs\Daemon;
use Zotlabs\Lib\Activity;
use Zotlabs\Lib\Config;
use Zotlabs\Lib\IConfig;
use Zotlabs\Lib\ObjCache;
use Zotlabs\Lib\Libzot;
use Zotlabs\Lib\Queue;
@@ -264,7 +266,6 @@ class Notifier {
}
if (!item_forwardable($target_item)) {
//hz_syslog(print_r($target_item,true));
logger('notifier: target item not forwardable', LOGGER_DEBUG);
return;
}
@@ -317,10 +318,15 @@ class Notifier {
return;
}
$m = get_iconfig($target_item, 'activitypub', 'signed_data');
$m = ObjCache::Get($target_item['mid']);
if (!$m) {
$m = IConfig::Get($target_item, 'activitypub', 'rawmsg');
}
// Re-use existing signature unless the activity type changed to a Tombstone, which won't verify.
if ($m && (!intval($target_item['item_deleted']))) {
self::$encoded_item = json_decode($m, true);
self::$encoded_item = $m;
}
else {
$activity = Activity::encode_activity($target_item);

View File

@@ -9,10 +9,14 @@ class Onedirsync {
static public function run($argc, $argv) {
if ($argc < 2 || is_int($argv[1]) === false) {
logger('onedirsync: no update id');
return;
}
logger('onedirsync: start ' . intval($argv[1]));
if (($argc > 1) && (intval($argv[1])))
$update_id = intval($argv[1]);
$update_id = intval($argv[1]);
if (!$update_id) {
logger('onedirsync: no update id');

View File

@@ -2,6 +2,7 @@
namespace Zotlabs\Daemon;
use DBA;
use Zotlabs\Lib\Activity;
use Zotlabs\Lib\ActivityStreams;
use Zotlabs\Lib\ASCollection;
@@ -15,10 +16,14 @@ class Onepoll {
static public function run($argc, $argv) {
if ($argc < 2 || is_int($argv[1]) === false) {
logger('onepoll: no contact');
return;
}
logger('onepoll: start');
if (($argc > 1) && (intval($argv[1])))
$contact_id = intval($argv[1]);
$contact_id = intval($argv[1]);
if (!$contact_id) {
logger('onepoll: no contact');
@@ -34,7 +39,7 @@ class Onepoll {
$contacts = q("SELECT abook.*, xchan.* FROM abook
LEFT JOIN xchan ON xchan_hash = abook_xchan
WHERE abook_id = %d",
intval($contact_id)
$contact_id
);
if (!$contacts) {
@@ -53,7 +58,7 @@ class Onepoll {
logger("onepoll: poll: ($contact_id) IMPORTER: {$importer['xchan_name']}, CONTACT: {$contact['xchan_name']}");
$last_update = ((($contact['abook_updated'] === $contact['abook_created']) || ($contact['abook_updated'] <= NULL_DATE))
$last_update = ((($contact['abook_updated'] === $contact['abook_created']) || ($contact['abook_updated'] <= DBA::$dba->get_null_date()))
? datetime_convert('UTC', 'UTC', 'now - 7 days')
: datetime_convert('UTC', 'UTC', $contact['abook_updated'] . ' - 2 days')
);

View File

@@ -2,6 +2,7 @@
namespace Zotlabs\Daemon;
use DBA;
use Zotlabs\Lib\Config;
class Poller {
@@ -117,7 +118,7 @@ class Poller {
// if we've never connected with them, start the mark for death countdown from now
if ($c <= NULL_DATE) {
if ($c <= DBA::$dba->get_null_date()) {
q("update abook set abook_connected = '%s' where abook_id = %d",
dbesc(datetime_convert()),
intval($contact['abook_id'])
@@ -173,7 +174,7 @@ class Poller {
if ($dirmode == DIRECTORY_MODE_SECONDARY || $dirmode == DIRECTORY_MODE_PRIMARY) {
$r = q("SELECT * FROM updates WHERE ud_update = 1 AND (ud_last = '%s' OR ud_last > %s - INTERVAL %s)",
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
db_utcnow(),
db_quoteinterval('7 DAY')
);
@@ -184,7 +185,7 @@ class Poller {
// If they didn't respond when we attempted before, back off to once a day
// After 7 days we won't bother anymore
if ($rr['ud_last'] > NULL_DATE)
if ($rr['ud_last'] > DBA::$dba->get_null_date())
if ($rr['ud_last'] > datetime_convert('UTC', 'UTC', 'now - 1 day'))
continue;

View File

@@ -1,15 +1,57 @@
<?php
/*
* SPDX-FileCopyrightText: 2025 The Hubzilla Community
* SPDX-FileContributor: redmatrix
* SPDX-FileContributor: Klaus Weidenbach
* SPDX-FileContributor: zotlabs
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Extend;
use App;
/**
* @brief Hook class.
* A class for hooking into Hubzilla.
*
* Hooks are functions that Hubzilla will invoke at certain points in the code
* during execution. An addon can register a callback handler that will be
* called whenever the specified hook is invoked. A callback handler is a
* function that takes a reference to an array containing the callback
* arguments as it's only argument.
*
* @see call_hooks
* @see load_hooks
*/
class Hook {
/**
* Register a callback handler for a hook.
*
* A callback handler is a function that takes a reference to an array
* containing the callback arguments as it's only argument.
*
* The contents and meaning of the array depends on the hook invoked. By
* modifying the contents of the array the hook can pass data back to the
* caller.
*
* Once the `Hook::register` function has been called, the callback may be
* invoked.
*
* @param string $hook The name of the hook to register a handler for.
* @param string $file The source file of the callback handler.
* @param string|array $function
* The function name of the callback handler, as a
* string or an array.
* @param int $version Hook interface version, allways 1.
* @param int $priority The priority of the callback handler, higher
* numbers takes precedence.
*
* @return true if the handler was already registered, otherwise the result
* from inserting the hook in the database.
*/
static public function register($hook,$file,$function,$version = 1,$priority = 0) {
if(is_array($function)) {
$function = serialize($function);
@@ -45,6 +87,14 @@ class Hook {
return $r;
}
/**
* Register an array of hook callback handlers.
*
* All of the handlers must be in the same source file.
*
* @param string $file The source file of the callback handlers.
* @param array $arr An array of `hookname => functionname` pairs.
*/
static public function register_array($file,$arr) {
if($arr) {
foreach($arr as $k => $v) {
@@ -54,6 +104,20 @@ class Hook {
}
/**
* Unregister a hook callback handler.
*
* @param string $hook The name of the hook to register a callback handler for.
* @param string $file The source file of the hook callback handler.
* @param string|array $function
* The function name of the callback handler, as a
* string or an array.
* @param int $version Hook interface version, allways 1.
* @param int $priority The priority of the callback handler, higher
* numbers takes precedence.
*
* @return The result of the database delete operation.
*/
static public function unregister($hook,$file,$function,$version = 1,$priority = 0) {
if(is_array($function)) {
$function = serialize($function);
@@ -70,11 +134,13 @@ class Hook {
}
/**
* @brief Unregister all hooks with this file component.
* Unregister all hooks handlers from a given source file.
*
* Useful for addon upgrades where you want to clean out old interfaces.
*
* @param string $file
* @param string $file The source file where the hook handlers were defined.
*
* @return The result from the database delete operation.
*/
static public function unregister_by_file($file) {
$r = q("DELETE FROM hook WHERE file = '%s' ",
@@ -85,25 +151,20 @@ class Hook {
}
/**
* @brief Inserts a hook into a page request.
* Inserts a hook into a page request.
*
* Insert a short-lived hook into the running page request.
* Hooks are normally persistent so that they can be called
* across asynchronous processes such as delivery and poll
* processes.
* Insert a short-lived hook into the running page request. Hooks are
* normally persistent so that they can be called across asynchronous
* processes such as delivery and poll processes.
*
* insert_hook lets you attach a hook callback immediately
* which will not persist beyond the life of this page request
* or the current process.
* This function lets you attach a hook callback immediately which will not
* persist beyond the life of this page request or the current process.
*
* @param string $hook
* name of hook to attach callback
* @param string $fn
* function name of callback handler
* @param int $version
* hook interface version, 0 uses two callback params, 1 uses one callback param
* @param int $priority
* currently not implemented in this function, would require the hook array to be resorted
* @param string $hook Name of hook to attach callback.
* @param string|array $fn Name of callback handler as a string or array.
* @param int $version Hook interface version, allways 1.
* @param int $priority Currently not implemented in this function,
* would require the hook array to be resorted.
*/
static public function insert($hook, $fn, $version = 0, $priority = 0) {
if(is_array($fn)) {
@@ -119,4 +180,4 @@ class Hook {
App::$hooks[$hook][] = array('', $fn, $priority, $version);
}
}
}

View File

@@ -1,23 +1,106 @@
<?php
/*
* SPDX-FileCopyrightText: 2018 The Hubzilla Community
* SPDX-FileContributor: Zotlabs
* SPDX-FileContributor: Mario <mario@mariovavti.com>
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Extend;
use Zotlabs\Lib\Config;
/**
* Class for managing routes.
*
* Routes connect a URL path to a module that will handle requests to that
* path.
*
* For example by registering a route like this:
*
* ```php
* Route::register(
* __DIR__ . '/Mod_Myroute.php',
* 'myroute'
* );
* ```
*
* Hubzilla will direct requests to the '/myroute' URL path to the 'Myroute'
* controller located in the '/Mod_Myroute.php' file in the same directory as
* the file this code was called from.
*
* Routes are stored persistently, so this function will typically be called from
* the `<addon>_load()` function if called from an addon. Accordingly, the route must
* be unregistered when no longer needed, like this:
*
* ```php
* Route::unregister(
* __DIR__ . '/Mod_Myroute.php',
* 'myroute'
* );
* ```
*
* This will typically be called from the `<addon>_unload()` function in an addon.
*/
class Route {
static function register($file,$modname) {
/**
* Register a new route.
*
* Example:
* ```php
* Route::register(
* __DIR__ . '/Mod_Myroute.php',
* 'myroute'
* );
* ```
*
* The route is stored persistently, and must be unregistered when no longer needed.
*
* @param string $file The file containing the controller for handling requests to this route.
* @param string $modname The name of the module (URL path).
*
* @see {@link Zotlabs::Extend::Route.unregister() unregister()}
* @see {@link Zotlabs::Extend::Route.unregister_by_file() unregister_by_file()}
*/
public static function register(string $file, string $modname): void {
$rt = self::get();
foreach ($rt as $r) {
if ($r[0] === $file && $r[1] === $modname) {
return;
}
}
$rt[] = [ $file, $modname ];
self::set($rt);
}
static function unregister($file,$modname) {
/**
* Unregister a route.
*
* Example:
* ```php
* Route::unregister(
* __DIR__ . '/Mod_Myroute.php',
* 'myroute'
* );
* ```
*
* @param string $file The file containing the controller for handling requests to this route.
* @param string $modname The name of the module (URL path).
*
* @see {@link Zotlabs::Extend::Route.register() register()}
* @see {@link Zotlabs::Extend::Route.unregister_by_file() unregister_by_file()}
*/
public static function unregister(string $file, string $modname): void {
$rt = self::get();
if($rt) {
$n = [];
foreach($rt as $r) {
if($r[0] !== $file && $r[1] !== $modname) {
if(!($r[0] === $file && $r[1] === $modname)) {
$n[] = $r;
}
}
@@ -25,7 +108,23 @@ class Route {
}
}
static function unregister_by_file($file) {
/**
* Unregister all routes by source file.
*
* Removes all persistently stored routes with hanclers in the
* given source file.
*
* Example:
* ```php
* Route::unregister_by_file(__DIR__ . '/Mod_Myroute.php');
* ```
*
* @param string $file The file containing the controllers to remove.
*
* @see {@link Zotlabs::Extend::Route.register() register()}
* @see {@link Zotlabs::Extend::Route.unregister() unregister()}
*/
public static function unregister_by_file(string $file): void {
$rt = self::get();
if($rt) {
$n = [];
@@ -38,11 +137,18 @@ class Route {
}
}
static function get() {
/**
* Get an array of all defined routes.
*
* @return An array of routes, where each entry is an array
* containing two elements, the file, and the module
* name.
*/
public static function get(): array {
return Config::Get('system','routes',[]);
}
static function set($r) {
private static function set(array $r): mixed {
return Config::Set('system','routes',$r);
}
}

View File

@@ -8,6 +8,13 @@ class Widget {
static function register($file,$widget) {
$rt = self::get();
foreach ($rt as $r) {
if ($r[0] === $file && $r[1] === $widget) {
return;
}
}
$rt[] = [ $file, $widget ];
self::set($rt);
}
@@ -17,7 +24,7 @@ class Widget {
if($rt) {
$n = [];
foreach($rt as $r) {
if($r[0] !== $file && $r[1] !== $widget) {
if(!($r[0] === $file && $r[1] === $widget)) {
$n[] = $r;
}
}

View File

@@ -26,7 +26,7 @@ class ASCache {
$ret = Cache::get($key, self::getAge());
if ($ret) {
return unserialise($ret);
return json_unserialize($ret);
}
return [];
@@ -42,7 +42,7 @@ class ASCache {
return;
}
Cache::set($key, serialise($obj));
Cache::set($key, json_serialize($obj));
}
public static function isCacheable(array $obj): bool

View File

@@ -95,6 +95,8 @@ class ASCollection {
return false;
}
$data = null;
if (is_array($this->nextpage)) {
$data = $this->nextpage;
}

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Lib;
use App;
use DBA;
use Zotlabs\Access\PermissionLimits;
use Zotlabs\Access\PermissionRoles;
use Zotlabs\Access\Permissions;
@@ -91,6 +92,8 @@ class Activity {
logger('fetch: ' . $url, LOGGER_DEBUG);
$start_timestamp = microtime(true);
if (strpos($url, 'x-zot:') === 0) {
$x = ZotURL::fetch($url, $channel);
}
@@ -129,7 +132,6 @@ class Activity {
}
$h = HTTPSig::create_sig($headers, $channel['channel_prvkey'], channel_url($channel), false);
$start_timestamp = microtime(true);
$x = z_fetch_url($url, true, $redirects, ['headers' => $h]);
}
@@ -382,7 +384,12 @@ class Activity {
if ($items) {
$x = [];
foreach ($items as $i) {
$m = IConfig::Get($i['id'], 'activitypub', 'rawmsg');
$m = ObjCache::Get($i['mid']);
if (!$m) {
$m = IConfig::Get($i['id'], 'activitypub', 'rawmsg');
}
if ($m) {
if (is_string($m))
$t = json_decode($m, true);
@@ -501,8 +508,8 @@ class Activity {
}
}
$ret['id'] = ((strpos($i['mid'], 'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid']));
$ret['diaspora:guid'] = $i['uuid'];
$ret['id'] = ((strpos($i['mid'], 'http') === 0) ? $i['mid'] : z_root() . '/item/' . urlencode($i['mid']));
$ret['uuid'] = $i['uuid'];
$images = [];
$audios = [];
@@ -562,7 +569,7 @@ class Activity {
$ret['published'] = datetime_convert('UTC', 'UTC', $i['created'], ATOM_TIME);
if ($i['created'] !== $i['edited'])
$ret['updated'] = datetime_convert('UTC', 'UTC', $i['edited'], ATOM_TIME);
if ($i['expires'] > NULL_DATE) {
if ($i['expires'] > DBA::$dba->get_null_date()) {
$ret['expires'] = datetime_convert('UTC', 'UTC', $i['expires'], ATOM_TIME);
}
@@ -587,7 +594,7 @@ class Activity {
$ret['commentPolicy'] = (($i['item_wall']) ? map_scope(PermissionLimits::Get($i['uid'], 'post_comments')) : '');
if (array_key_exists('comments_closed', $i) && $i['comments_closed'] !== EMPTY_STR && $i['comments_closed'] > NULL_DATE) {
if (array_key_exists('comments_closed', $i) && $i['comments_closed'] !== EMPTY_STR && $i['comments_closed'] > DBA::$dba->get_null_date()) {
if ($ret['commentPolicy']) {
$ret['commentPolicy'] .= ' ';
}
@@ -639,14 +646,15 @@ class Activity {
// TODO: Do not replace the if the owner is a forum.
// Receivers will not be able to fetch the original in that case.
if (str_contains($i['body'], '[/share]') && !$i['owner']['xchan_pubforum']) {
preg_match_all('/\[share(.*?)\[\/share\]/ism', $i['body'], $all_shares, PREG_SET_ORDER);
if (str_contains($i['body'], '[/share]')) {
preg_match_all('/\[share(.*?)\](.*?)\[\/share\]/ism', $i['body'], $all_shares, PREG_SET_ORDER);
$quote_urls = [];
$obj_links = [];
foreach ($all_shares as $share) {
// Extract the link attribute from each [share] block
if (preg_match("/link='(.*?)'/ism", $share[1], $match)) {
// Extract the link attribute from each [share] block if slated for quote
if (str_contains($share[1], "quote='true'") && preg_match("/link='(.*?)'/ism", $share[1], $match)) {
$url = $match[1];
$quote_urls[] = $url;
@@ -666,6 +674,7 @@ class Activity {
if ($quote_urls) {
$ret['quoteUrl'] = $quote_urls[0];
$ret['quoteUri'] = $quote_urls[0];
if (empty($ret['tag'])) {
$ret['tag'] = $obj_links;
@@ -829,8 +838,7 @@ class Activity {
if ($iconfig && array_key_exists('iconfig', $item) && is_array($item['iconfig'])) {
foreach ($item['iconfig'] as $att) {
if ($att['sharing']) {
$value = ((is_string($att['v']) && preg_match('|^a:[0-9]+:{.*}$|s', $att['v'])) ? unserialize($att['v']) : $att['v']);
$ret[] = ['type' => 'PropertyValue', 'name' => 'zot.' . $att['cat'] . '.' . $att['k'], 'value' => $value];
$ret[] = ['type' => 'PropertyValue', 'name' => 'zot.' . $att['cat'] . '.' . $att['k'], 'value' => $att['v']];
}
}
}
@@ -851,6 +859,10 @@ class Activity {
$entry = [];
if (isset($att['type']) && $att['type'] === 'PropertyValue') {
if (isset($att['name'])) {
if (in_array($att['name'], ['zot.activitypub.rawmsg', 'zot.diaspora.fields'])) {
continue;
}
$key = explode('.', $att['name']);
if (count($key) === 3 && $key[0] === 'zot') {
$entry['cat'] = $key[1];
@@ -997,7 +1009,7 @@ class Activity {
$ret['id'] = ((strpos($i['mid'], 'http') === 0) ? $i['mid'] : z_root() . '/activity/' . urlencode($i['mid']));
}
$ret['diaspora:guid'] = $i['uuid'];
$ret['uuid'] = $i['uuid'];
if (!empty($i['title']))
$ret['name'] = html2plain(bbcode($i['title']));
@@ -1114,6 +1126,7 @@ class Activity {
return [];
}
/* Those should not be required in activities anymore after version 11
$t = self::encode_taxonomy($i);
if ($t) {
$ret['tag'] = $t;
@@ -1123,6 +1136,7 @@ class Activity {
if ($a) {
$ret['attachment'] = $a;
}
*/
if (intval($i['item_private']) === 0) {
$ret['to'] = [ACTIVITY_PUBLIC_INBOX];
@@ -1551,7 +1565,7 @@ class Activity {
'abook_created' => datetime_convert(),
'abook_updated' => datetime_convert(),
'abook_connected' => datetime_convert(),
'abook_dob' => NULL_DATE,
'abook_dob' => DBA::$dba->get_null_date(),
'abook_pending' => intval(($automatic) ? 0 : 1),
'abook_instance' => z_root()
]
@@ -2003,6 +2017,8 @@ class Activity {
$multi = true;
}
$answer_found = false;
if ($response) {
$mid = $response['mid'];
$content = trim($response['title']);
@@ -2032,7 +2048,6 @@ class Activity {
}
}
$answer_found = false;
$foundPrevious = false;
if ($multi) {
for ($c = 0; $c < count($o['anyOf']); $c++) {
@@ -2074,7 +2089,7 @@ class Activity {
}
}
}
if ($pollItem['comments_closed'] > NULL_DATE) {
if ($pollItem['comments_closed'] > DBA::$dba->get_null_date()) {
if ($pollItem['comments_closed'] > datetime_convert()) {
$o['closed'] = datetime_convert('UTC', 'UTC', $pollItem['comments_closed'], ATOM_TIME);
// set this to force an update
@@ -2126,15 +2141,16 @@ class Activity {
}
static function decode_note($act) {
$response_activity = false;
$s = [];
$obj_type = is_array($act->objprop('type')) ? $act->objprop('type')[0] : $act->objprop('type');
// These activities should have been handled separately in the Inbox module and should not be turned into posts
if (
in_array($act->type, ['Follow', 'Accept', 'Reject', 'Create', 'Update']) &&
($act->objprop('type') === 'Follow' || ActivityStreams::is_an_actor($act->objprop('type')))
($obj_type === 'Follow' || ActivityStreams::is_an_actor($obj_type))
) {
return false;
}
@@ -2207,7 +2223,7 @@ class Activity {
}
}
if (in_array($act->type, ['Invite', 'Create']) && $act->objprop('type') === 'Event') {
if (in_array($act->type, ['Invite', 'Create']) && $obj_type === 'Event') {
$s['mid'] = $s['parent_mid'] = $act->id;
}
@@ -2239,25 +2255,25 @@ class Activity {
$mention = self::get_actor_bbmention($obj_actor['id']);
if ($act->type === 'Like') {
$content['content'] = sprintf(t('Likes %1$s\'s %2$s'), $mention, $act->obj['type']) . EOL . EOL . ($content['content'] ?? '');
$content['content'] = sprintf(t('Likes %1$s\'s %2$s'), $mention, $act->obj['type']);
}
if ($act->type === 'Dislike') {
$content['content'] = sprintf(t('Doesn\'t like %1$s\'s %2$s'), $mention, $act->obj['type']) . EOL . EOL . ($content['content'] ?? '');
$content['content'] = sprintf(t('Doesn\'t like %1$s\'s %2$s'), $mention, $act->obj['type']);
}
// handle event RSVPs
if (($act->objprop('type') === 'Event') || ($act->objprop('type') === 'Invite' && array_path_exists('object/type', $act->obj) && $act->obj['object']['type'] === 'Event')) {
if (in_array($obj_type, ['Event', 'Invite'])) {
if ($act->type === 'Accept') {
$content['content'] = sprintf(t('Will attend %s\'s event'), $mention) . EOL . EOL . ($content['content'] ?? '');
$content['content'] = sprintf(t('Will attend %s\'s event'), $mention);
}
if ($act->type === 'Reject') {
$content['content'] = sprintf(t('Will not attend %s\'s event'), $mention) . EOL . EOL . ($content['content'] ?? '');
$content['content'] = sprintf(t('Will not attend %s\'s event'), $mention);
}
if ($act->type === 'TentativeAccept') {
$content['content'] = sprintf(t('May attend %s\'s event'), $mention) . EOL . EOL . ($content['content'] ?? '');
$content['content'] = sprintf(t('May attend %s\'s event'), $mention);
}
if ($act->type === 'TentativeReject') {
$content['content'] = sprintf(t('May not attend %s\'s event'), $mention) . EOL . EOL . ($content['content'] ?? '');
$content['content'] = sprintf(t('May not attend %s\'s event'), $mention);
}
}
@@ -2291,7 +2307,7 @@ class Activity {
if ($s['mid'] === $s['parent_mid']) {
$s['item_thread_top'] = 1;
$s['item_nocomment'] = 0;
$s['comments_closed'] = NULL_DATE;
$s['comments_closed'] = DBA::$dba->get_null_date();
// it is a parent node - decode the comment policy info if present
if ($act->objprop('commentPolicy')) {
@@ -2347,13 +2363,19 @@ class Activity {
// Otherwise they will appear doubled.
if ($quote_urls && !str_contains($s['body'], '[/share]')) {
foreach($quote_urls as $quote_url) {
if (!is_string($quote_url)) {
// FIXME: requires investigation
logger('Not a string: ' . print_r($quote_url,true));
continue;
}
$quote = self::get_quote($quote_url);
if (!$quote) {
continue;
}
$s['body'] = self::pasteQuote($s['body'], $quote);
$s['body'] = self::pasteQuote($s['body'] ?? EMPTY_STR, $quote);
$s['term'] = $quote['term'];
}
}
@@ -2361,7 +2383,7 @@ class Activity {
$s['verb'] = self::activity_mapper($act->type);
// Mastodon does not provide update timestamps when updating poll tallies which means race conditions may occur here.
if ($act->type === 'Update' && $act->objprop('type') === 'Question' && $s['edited'] === $s['created']) {
if ($act->type === 'Update' && $obj_type === 'Question' && $s['edited'] === $s['created']) {
$s['edited'] = datetime_convert();
}
@@ -2369,8 +2391,8 @@ class Activity {
$s['item_deleted'] = 1;
}
if ($act->objprop('type')) {
$s['obj_type'] = self::activity_obj_mapper($act->obj['type']);
if ($obj_type) {
$s['obj_type'] = self::activity_obj_mapper($obj_type);
}
$s['obj'] = $act->obj;
@@ -2431,7 +2453,7 @@ class Activity {
$s = self::bb_attach($s);
}
if ($act->objprop('type') === 'Question' && in_array($act->type, ['Create', 'Update'])) {
if ($obj_type === 'Question' && in_array($act->type, ['Create', 'Update'])) {
if ($act->objprop('endTime')) {
$s['comments_closed'] = datetime_convert('UTC', 'UTC', $act->obj['endTime']);
}
@@ -2443,7 +2465,7 @@ class Activity {
if (!$response_activity) {
if ($act->objprop('type') === 'Profile') {
if ($obj_type === 'Profile') {
$s['parent_mid'] = $s['mid'];
$s['item_thread_top'] = 1;
}
@@ -2453,7 +2475,7 @@ class Activity {
// right now just link to the largest mp4 we find that will fit in our
// standard content region
if ($act->objprop('type') === 'Video') {
if ($obj_type === 'Video') {
$vtypes = [
'video/mp4',
@@ -2535,7 +2557,7 @@ class Activity {
}
}
if ($act->objprop('type') === 'Audio') {
if ($obj_type === 'Audio') {
$atypes = [
'audio/mpeg',
@@ -2567,7 +2589,7 @@ class Activity {
}
if ($act->objprop('type') === 'Image' && strpos($s['body'], 'zrl=') === false) {
if ($obj_type === 'Image' && strpos($s['body'], 'zrl=') === false) {
$ptr = null;
@@ -2582,10 +2604,10 @@ class Activity {
foreach ($ptr as $vurl) {
if (strpos($s['body'], $vurl['href']) === false) {
$bb_imgs = '[zmg]' . $vurl['href'] . '[/zmg]' . "\r\n";
$s['body'] = $bb_imgs . $s['body'];
break;
}
}
$s['body'] = $bb_imgs . $s['body'];
}
elseif (is_string($act->obj['url'])) {
if (strpos($s['body'], $act->obj['url']) === false) {
@@ -2595,7 +2617,7 @@ class Activity {
}
}
if ($act->objprop('type') === 'Page' && !$s['body']) {
if ($obj_type === 'Page' && !$s['body']) {
$ptr = null;
$purl = EMPTY_STR;
@@ -2635,7 +2657,7 @@ class Activity {
}
}
if (in_array($act->objprop('type'), ['Note', 'Article', 'Page', 'Question'])) {
if (in_array($obj_type, ['Note', 'Article', 'Page', 'Question'])) {
$ptr = null;
if (array_key_exists('url', $act->obj)) {
@@ -2674,55 +2696,11 @@ class Activity {
$s['item_private'] = 2;
}
$ap_rawmsg = '';
$diaspora_rawmsg = '';
$raw_arr = [];
$raw_arr = json_decode($act->raw, true);
// This is a zot6 packet and the raw activitypub or diaspora message json
// is possibly available in the attachement.
if (array_key_exists('signed', $raw_arr) && isset($act->data['attachment']) && is_array($act->data['attachment'])) {
foreach($act->data['attachment'] as $a) {
if (
isset($a['type']) && $a['type'] === 'PropertyValue' &&
isset($a['name']) && $a['name'] === 'zot.activitypub.rawmsg' &&
isset($a['value'])
) {
$ap_rawmsg = $a['value'];
}
if (
isset($a['type']) && $a['type'] === 'PropertyValue' &&
isset($a['name']) && $a['name'] === 'zot.diaspora.fields' &&
isset($a['value'])
) {
$diaspora_rawmsg = $a['value'];
}
}
}
if (!$ap_rawmsg && array_key_exists('signed', $raw_arr)) {
// zap
$ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES);
}
if ($ap_rawmsg) {
IConfig::Set($s, 'activitypub', 'rawmsg', $ap_rawmsg, 1);
}
elseif (!array_key_exists('signed', $raw_arr)) {
IConfig::Set($s, 'activitypub', 'rawmsg', $act->raw, 1);
}
if ($diaspora_rawmsg) {
IConfig::Set($s, 'diaspora', 'fields', $diaspora_rawmsg, 1);
}
if ($act->raw_recips) {
IConfig::Set($s, 'activitypub', 'recips', $act->raw_recips);
}
if ($act->objprop('type') === 'Event' && $act->objprop('timezone')) {
if ($obj_type === 'Event' && $act->objprop('timezone')) {
IConfig::Set($s, 'event', 'timezone', $act->objprop('timezone'), true);
}
@@ -2888,15 +2866,11 @@ class Activity {
}
if (tgroup_check($channel['channel_id'], $item) && (!$is_child_node)) {
// for forum deliveries, make sure we keep a copy of the signed original
IConfig::Set($item, 'activitypub', 'rawmsg', $act->raw, 1);
$allowed = true;
}
if (intval($item['item_private']) === 2) {
if (perm_is_allowed($channel['channel_id'], $observer_hash, 'post_mail')) {
$allowed = true;
}
$allowed = perm_is_allowed($channel['channel_id'], $observer_hash, 'post_mail');
}
if ($is_sys_channel) {
@@ -2963,12 +2937,18 @@ class Activity {
return;
if ($is_sys_channel) {
$incl = Config::Get('system','pubstream_incl');
$excl = Config::Get('system','pubstream_excl');
$incl = Config::Get('system', 'pubstream_incl', '');
$excl = Config::Get('system', 'pubstream_excl', '');
if(($incl || $excl) && !MessageFilter::evaluate($item, $incl, $excl)) {
logger('post is filtered');
return;
if ($incl || $excl) {
$plaintext = prepare_text($item['body'], ((isset($item['mimetype'])) ? $item['mimetype'] : 'text/bbcode'));
$plaintext = html2plain((isset($item['summary']) && $item['summary']) ? $item['summary'] . ' ' . $plaintext : $plaintext);
$plaintext = html2plain((isset($item['title']) && $item['title']) ? $item['title'] . ' ' . $plaintext : $plaintext);
if (!(new MessageFilter($item, html_entity_decode($incl), html_entity_decode($excl), ['plaintext' => $plaintext]))->evaluate()) {
logger('post is filtered');
return;
}
}
}
@@ -3067,13 +3047,22 @@ class Activity {
// TODO: not implemented
// self::rewrite_mentions($item);
$r = q("select id, created, edited from item where mid = '%s' and uid = %d limit 1",
if (!ObjCache::Get($item['mid'])) {
ObjCache::Set($item['mid'], $act->data);
}
$r = q("select id, created, edited, owner_xchan, author_xchan from item where mid = '%s' and uid = %d limit 1",
dbesc($item['mid']),
intval($item['uid'])
);
if ($r) {
if ($item['edited'] > $r[0]['edited']) {
// Only update the object cache if there is no owner/author mismatch.
if ($r[0]['owner_xchan'] === $item['owner_xchan'] && $r[0]['author_xchan'] === $item['author_xchan']) {
ObjCache::Set($item['mid'], $act->data);
}
$item['id'] = $r[0]['id'];
$x = item_store_update($item, deliver: false);
}
@@ -3108,13 +3097,20 @@ class Activity {
sync_an_item($channel['channel_id'], $x['item_id']);
$replies_id = null;
if (isset($act->obj['replies'])) {
$replies_id = is_array($act->obj['replies']) ? $act->obj['replies']['id'] : $act->obj['replies'];
}
// Only store replies collection for background fetching if the item has been fetched.
// A message that has just been posted usually will not have any replies yet.
// Also dismiss duplicates.
$attempt_replies_fetch = isset($act->obj['replies']['id']) && !empty($item['item_fetched']) && !in_array($channel['channel_id'], App::$cache['as_fetch_collection'][$act->obj['replies']['id']]['channels'] ?? []);
$attempt_replies_fetch = $replies_id && !empty($item['item_fetched']) && !in_array($channel['channel_id'], App::$cache['as_fetch_collection'][$replies_id]['channels'] ?? []);
if ($attempt_replies_fetch) {
App::$cache['as_fetch_collection'][$act->obj['replies']['id']]['channels'][] = $channel['channel_id'];
App::$cache['as_fetch_collection'][$act->obj['replies']['id']]['force'] = intval($force);
App::$cache['as_fetch_collection'][$replies_id]['channels'][] = $channel['channel_id'];
App::$cache['as_fetch_collection'][$replies_id]['force'] = intval($force);
}
}
}
@@ -3334,6 +3330,12 @@ class Activity {
return true;
}
// FIXME: it appears sometimes $s is an array (needs invetigation)
if (!is_string($s)) {
btlogger('Not a string: ' . print_r($s, true));
return true;
}
$s_alt = htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
if (
@@ -3636,7 +3638,7 @@ class Activity {
$cached = ASCache::Get($url);
if ($cached) {
// logger('cached: ' . $url);
$a = unserialise($cached);
$a = $cached;
}
else {
// logger('fetching: ' . $url);
@@ -3646,7 +3648,6 @@ class Activity {
}
}
if ($a) {
$act = new ActivityStreams($a);
@@ -3746,12 +3747,11 @@ class Activity {
'conversation' => 'ostatus:conversation',
'guid' => 'diaspora:guid',
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'Hashtag' => 'as:Hashtag',
'quoteUrl' => 'as:quoteUrl',
'quoteUri' => 'http://fedibird.com/ns#quoteUri'
];
}
@@ -3790,6 +3790,9 @@ class Activity {
*/
public static function init_background_fetch(string $observer_hash = '') {
$interval = Config::Get('queueworker', 'queue_interval', 500000);
if (isset(App::$cache['zot_fetch_objects'])) {
foreach (App::$cache['zot_fetch_objects'] as $mid => $info) {
$force = $info['force'];
@@ -3803,6 +3806,10 @@ class Activity {
}
Master::Summon(['Zotconvo', $channels_str, $mid, $force]);
if ($interval) {
usleep($interval);
}
}
}
@@ -3824,6 +3831,10 @@ class Activity {
}
Master::Summon(['Fetchparents', $channels_str, $observer_hash, $mid, $force]);
if ($interval) {
usleep($interval);
}
}
}
@@ -3840,6 +3851,10 @@ class Activity {
}
Master::Summon(['Convo', $channels_str, $observer_hash, $mid, $force]);
if ($interval) {
usleep($interval);
}
}
}

View File

@@ -553,6 +553,7 @@ class ActivityStreams {
}
$url = unparse_url($parseUrl);
$this->signer = ['id' => $url];
$hublocs = Activity::get_actor_hublocs($url);

View File

@@ -1028,12 +1028,7 @@ class Apps {
if(! $syslist)
return;
foreach($syslist as $k => $li) {
if($li['guid'] === $guid) {
$position = $k;
break;
}
}
$position = array_find_key($syslist, fn ($v) => $v['guid'] === $guid);
if(! $position)
return;
@@ -1082,12 +1077,7 @@ class Apps {
if(! $syslist)
return;
foreach($syslist as $k => $li) {
if($li['guid'] === $guid) {
$position = $k;
break;
}
}
$position = array_find_key($syslist, fn ($v) => $v['guid'] === $guid);
if($position >= count($syslist) - 1)
return;

View File

@@ -132,8 +132,8 @@ class Config {
$value = App::$config[$family][$key];
if (! is_array($value)) {
if (substr($value, 0, 5) == 'json:') {
return json_decode(substr($value, 5), true);
if (str_starts_with($value, 'json:')) {
return json_unserialize($value);
} else if (preg_match('|^a:[0-9]+:{.*}$|s', $value)) {
// Unserialize in inherently unsafe. Try to mitigate by not
// allowing unserializing objects. Only kept for backwards

43
Zotlabs/Lib/DbStats.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
/*
* SPDX-FileCopyrightText: 2026 The Hubzilla Community
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Lib;
use DBA;
/**
* Abstract class to obtain statistics from the database.
*
* This class should not be instantiated on it's own, but you can get
* a concrete class for the configured database type of this site by
* calling the `DbStats::getStats()` function.
*/
abstract class DbStats {
/**
* Get an object for getting statistics from the database.
*
* @return DbStats The concrete class for obtaining the statistics from
* this instances database.
*/
public static function getStats(): DbStats {
return DBA::$dba->is_postgres()
? new PostgresDbStats()
: new MySQLDbStats();
}
/**
* Return the number of queries recorded by the database.
*
* @return int Number of queries.
*/
public abstract function getQueries(): int;
// Prevent instantiation of this class
private function __construct() {}
}

View File

@@ -431,7 +431,7 @@ class Enotify {
elseif (isset($params['type']) && $params['type'] === NOTIFY_INTRO) {
$subject = sprintf( t('[$Projectname:Notify] Introduction received'));
$preamble = sprintf( t('You\'ve received an new connection request from \'%1$s\' at %2$s'), $sender['xchan_name'], $sitename);
$preamble = sprintf( t('You\'ve received a new connection request from \'%1$s\' at %2$s'), $sender['xchan_name'], $sitename);
$epreamble = sprintf( t('You\'ve received [zrl=%1$s]a new connection request[/zrl] from %2$s.'),
$siteurl . '/connections/ifpending',
'[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]');
@@ -511,7 +511,7 @@ class Enotify {
*/
$hash = ((in_array($params['verb'], ['Create', 'Update'])) ? $params['item']['uuid'] : $params['item']['thr_parent_uuid']);
$hash = ((in_array($params['verb'], ['Create', 'Update', 'Invite'])) ? $params['item']['uuid'] : $params['item']['thr_parent_uuid']);
if (!$hash) {
$hash = new_uuid();

View File

@@ -34,8 +34,20 @@ class IConfig {
if(is_array($item) && array_key_exists('iconfig',$item) && is_array($item['iconfig'])) {
foreach($item['iconfig'] as $c) {
if (isset($c['iid']) && $c['iid'] == $iid && isset($c['cat']) && $c['cat'] == $family && isset($c['k']) && $c['k'] == $key)
if (isset($c['iid']) && $c['iid'] == $iid && isset($c['cat']) && $c['cat'] == $family && isset($c['k']) && $c['k'] == $key) {
if (is_string($c['v'])) {
if (str_starts_with($c['v'], 'json:')) {
$c['v'] = json_unserialize($c['v']);
} else if (preg_match('|^a:[0-9]+:{.*}$|s', $c['v'])) {
// Unserialize in inherently unsafe. Try to mitigate by not
// allowing unserializing objects. Only kept for backwards
// compatibility. JSON serialization should be prefered.
$c['v'] = unserialize($c['v'], ['allowed_classes' => false]);
}
}
return $c['v'];
}
}
}
@@ -44,12 +56,24 @@ class IConfig {
dbesc($family),
dbesc($key)
);
if($r) {
$r[0]['v'] = ((preg_match('|^a:[0-9]+:{.*}$|s',$r[0]['v'])) ? unserialize($r[0]['v']) : $r[0]['v']);
if($is_item)
if (str_starts_with($r[0]['v'], 'json:')) {
$r[0]['v'] = json_unserialize($r[0]['v']);
} else if (preg_match('|^a:[0-9]+:{.*}$|s', $r[0]['v'])) {
// Unserialize in inherently unsafe. Try to mitigate by not
// allowing unserializing objects. Only kept for backwards
// compatibility. JSON serialization should be prefered.
$r[0]['v'] = unserialize($r[0]['v'], ['allowed_classes' => false]);
}
if ($is_item) {
$item['iconfig'][] = $r[0];
}
return $r[0]['v'];
}
return $default;
}
@@ -73,7 +97,7 @@ class IConfig {
static public function Set(&$item, $family, $key, $value, $sharing = false) {
$dbvalue = ((is_array($value)) ? serialize($value) : $value);
$dbvalue = ((is_array($value)) ? json_serialize($value) : $value);
$dbvalue = ((is_bool($dbvalue)) ? intval($dbvalue) : $dbvalue);
$is_item = false;

View File

@@ -25,18 +25,18 @@ class Img_filesize {
static function getLocalFileSize($url) {
$fname = basename($url);
$resolution = 0;
if(strpos($fname,'.') !== false)
$fname = substr($fname,0,strpos($fname,'.'));
if(substr($fname,-2,1) == '-') {
$resolution = intval(substr($fname,-1,1));
$fname = substr($fname,0,-2);
}
$r = q("SELECT filesize FROM photo WHERE resource_id = '%s' AND imgscale = %d LIMIT 1",
dbesc($fname),
intval($resolution)
@@ -116,7 +116,6 @@ function getRemoteFileSize($url)
curl_exec($ch);
curl_getinfo($ch);
curl_close($ch);
return $size;
}
}

View File

@@ -8,9 +8,10 @@ class LDSignatures {
static function verify($data,$pubkey) {
$expand_and_check_unsafe = true;
$ohash = self::hash(self::signable_options($data['signature']));
$dhash = self::hash(self::signable_data($data));
$ohash = self::hash(self::signable_options($data['signature']), $expand_and_check_unsafe);
$dhash = self::hash(self::signable_data($data), $expand_and_check_unsafe);
$x = Crypto::verify($ohash . $dhash,base64_decode($data['signature']['signatureValue']), $pubkey);
logger('LD-verify: ' . intval($x));
@@ -74,11 +75,11 @@ class LDSignatures {
return json_encode($newopts,JSON_UNESCAPED_SLASHES);
}
static function hash($obj) {
return hash('sha256', self::normalise($obj));
static function hash($obj, $expand_and_check_unsafe = false) {
return hash('sha256', self::normalise($obj, $expand_and_check_unsafe));
}
static function normalise($data) {
static function normalise($data, $expand_and_check_unsafe) {
$ret = '';
if(is_string($data)) {
@@ -90,6 +91,15 @@ class LDSignatures {
jsonld_set_document_loader('jsonld_document_loader');
if ($expand_and_check_unsafe) {
$expanded = jsonld_expand($data);
if (self::contains_unsafe_keys($expanded)) {
logger('contains_unsafe_keys: ' . print_r($data,true));
throw new \Exception('json-ld graph modification operation detected');
}
}
try {
$ret = jsonld_normalize($data,[ 'algorithm' => 'URDNA2015', 'format' => 'application/nquads' ]);
}
@@ -132,6 +142,41 @@ class LDSignatures {
}
static function contains_unsafe_keys(array|object $data, int $depth = 0): bool
{
if ($depth > 64) {
return true;
}
$unsafe_keys = ['@graph', '@included', '@reverse'];
if (is_object($data)) {
$data = (array) $data;
}
if (is_array($data)) {
foreach ($data as $key => $value) {
//
// We can't use `in_array` since the keys may contain more than
// just the keyword after expansion, typically "_:@included"
// for an unnamed node with the "@included" key.
//
// So we use `array_filter` with a callback instead:
$matches = array_filter($unsafe_keys, fn ($k) => strpos($key, $k) !== false);
if (!empty($matches)) {
return true;
}
if (is_array($value) || is_object($value)) {
if (self::contains_unsafe_keys($value, $depth + 1)) {
return true;
}
}
}
}
return false;
}
}

View File

@@ -199,8 +199,7 @@ class Libsync {
dbesc($sender)
);
$mid = 'sync';
$mid = $arr['item'][0]['message_id'] ?? 'sync';
$DR = new DReport(z_root(), $sender, $d, $mid);
@@ -306,15 +305,8 @@ class Libsync {
if (array_key_exists('item', $arr) && $arr['item']) {
sync_items($channel, $arr['item'], ((array_key_exists('relocate', $arr)) ? $arr['relocate'] : null));
$mid = $arr['item'][0]['message_id'] . '#sync';
}
// deprecated, maintaining for a few months for upward compatibility
// this should sync webpages, but the logic is a bit subtle
//if (array_key_exists('item_id', $arr) && $arr['item_id'])
// sync_items($channel, $arr['item_id']);
if (array_key_exists('menu', $arr) && $arr['menu'])
sync_menus($channel, $arr['menu']);
@@ -757,12 +749,11 @@ class Libsync {
*/
call_hooks('process_channel_sync_delivery', $addon);
$DR = new DReport(z_root(), $d, $d, $mid, 'channel sync processed');
$DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
$DR->update('channel sync processed');
$result[] = $DR->get();
}
return $result;
}

View File

@@ -3,10 +3,10 @@
namespace Zotlabs\Lib;
use App;
use DBA;
use Zotlabs\Access\PermissionLimits;
use Zotlabs\Access\Permissions;
use Zotlabs\Daemon\Master;
use Zotlabs\Lib\Config;
use Zotlabs\Web\HTTPSig;
require_once('include/crypto.php');
@@ -116,10 +116,6 @@ class Libzot {
}
if ($msg) {
$actors = get_hubloc_id_urls_by_x($channel['channel_hash']);
if ($encoding === 'activitystreams' && array_key_exists('actor', $msg) && is_string($msg['actor']) && in_array($msg['actor'], $actors)) {
$msg = JSalmon::sign($msg, $actors[0], $channel['channel_prvkey']);
}
$data['data'] = $msg;
}
else {
@@ -353,7 +349,7 @@ class Libzot {
$next_birthday = datetime_convert('UTC', 'UTC', $record['data']['profile']['next_birthday']);
}
else {
$next_birthday = NULL_DATE;
$next_birthday = DBA::$dba->get_null_date();
}
$profile_assign = get_pconfig($channel['channel_id'], 'system', 'profile_assign', '');
@@ -1302,8 +1298,17 @@ class Libzot {
$item['comment_policy'] = 'authenticated';
}
if (isset($AS->meta['signed_data']) && $AS->meta['signed_data']) {
IConfig::Set($item, 'activitypub', 'signed_data', $AS->meta['signed_data'], false);
if (!ObjCache::Get($item['mid'])) {
ObjCache::Set($item['mid'], $AS->data);
}
else {
$existing = q("SELECT owner_xchan, author_xchan FROM item WHERE mid = '%s' LIMIT 1",
dbesc($item['mid'])
);
if ($existing && $existing[0]['owner_xchan'] === $item['owner_xchan'] && $existing[0]['author_xchan'] === $item['author_xchan']) {
ObjCache::Set($item['mid'], $AS->data);
}
}
logger('Activity received: ' . print_r($item, true), LOGGER_DATA, LOG_DEBUG);
@@ -1645,12 +1650,19 @@ class Libzot {
if (intval($channel['channel_system']) && (!$arr['item_private']) && (!$relay)) {
$local_public = true;
$incl = Config::Get('system','pubstream_incl');
$excl = Config::Get('system','pubstream_excl');
$incl = Config::Get('system','pubstream_incl', '');
$excl = Config::Get('system','pubstream_excl', '');
if(($incl || $excl) && !MessageFilter::evaluate($arr, $incl, $excl)) {
$local_public = false;
continue;
if ($incl || $excl) {
$plaintext = prepare_text($arr['body'], ((isset($arr['mimetype'])) ? $arr['mimetype'] : 'text/bbcode'));
$plaintext = html2plain((isset($arr['summary']) && $arr['summary']) ? $arr['summary'] . ' ' . $plaintext : $plaintext);
$plaintext = html2plain((isset($arr['title']) && $arr['title']) ? $arr['title'] . ' ' . $plaintext : $plaintext);
if (!(new MessageFilter($arr, html_entity_decode($incl), html_entity_decode($excl), ['plaintext' => $plaintext]))->evaluate()) {
logger('post is filtered');
$local_public = false;
continue;
}
}
$r = q("select xchan_selfcensored, xchan_censored from xchan where xchan_hash = '%s'",
@@ -1659,6 +1671,7 @@ class Libzot {
// don't import sys channel posts from selfcensored or censored authors
if ($r && ($r[0]['xchan_selfcensored'] || $r[0]['xchan_censored'])) {
logger('author is censored');
$local_public = false;
continue;
}
@@ -1829,9 +1842,7 @@ class Libzot {
}
if (intval($arr['item_private']) === 2) {
if (!perm_is_allowed($channel['channel_id'], $sender, 'post_mail')) {
$allowed = false;
}
$allowed = perm_is_allowed($channel['channel_id'], $sender, 'post_mail');
}
if (!$allowed) {
@@ -1893,11 +1904,13 @@ class Libzot {
else {
$DR->update('update ignored');
$result[] = $DR->get();
// We need this line to ensure wall-to-wall comments and add/remove activities are relayed (by falling through to the relay bit),
// and at the same time not relay any other relayable posts more than once, because to do so is very wasteful.
if (!intval($r[0]['item_origin']))
// The second part should prevent possible items that come back to us from channels that source our channel from being relayed again (sender != owner or author).
if (!intval($r[0]['item_origin']) || (intval($r[0]['item_origin']) && !in_array($sender, [$r[0]['owner_xchan'], $r[0]['author_xchan']]))) {
continue;
}
}
@@ -2148,10 +2161,9 @@ class Libzot {
}
if (isset($AS->meta['signed_data'])) {
IConfig::Set($arr, 'activitypub', 'signed_data', $AS->meta['signed_data'], false);
$j = json_decode($AS->meta['signed_data'], true);
if ($j) {
IConfig::Set($arr, 'activitypub', 'rawmsg', json_encode(JSalmon::unpack($j['data'])), true);
ObjCache::Set($arr['mid'], json_encode(JSalmon::unpack($j['data'])));
}
}

View File

@@ -2,6 +2,7 @@
namespace Zotlabs\Lib;
use DBA;
use Zotlabs\Lib\Config;
use Zotlabs\Lib\Libzot;
use Zotlabs\Lib\Zotfinger;
@@ -216,7 +217,7 @@ class Libzotdir {
[
'site_url' => DIRECTORY_FALLBACK_MASTER,
'site_flags' => DIRECTORY_MODE_PRIMARY,
'site_update' => NULL_DATE,
'site_update' => DBA::$dba->get_null_date(),
'site_directory' => DIRECTORY_FALLBACK_MASTER . '/dirsearch',
'site_realm' => DIRECTORY_REALM,
'site_valid' => 1,
@@ -247,7 +248,7 @@ class Libzotdir {
$token = Config::Get('system','realm_token');
$syncdate = (($rr['site_sync'] <= NULL_DATE) ? datetime_convert('UTC','UTC','now - 2 days') : $rr['site_sync']);
$syncdate = (($rr['site_sync'] <= DBA::$dba->get_null_date()) ? datetime_convert('UTC','UTC','now - 2 days') : $rr['site_sync']);
$x = z_fetch_url($rr['site_directory'] . '?f=&sync=' . urlencode($syncdate) . (($token) ? '&t=' . $token : ''));
if (! $x['success'])
@@ -724,7 +725,7 @@ class Libzotdir {
if ($u) {
$x = q("UPDATE updates SET $date_sql $flag_sql ud_last = '%s', ud_host = '%s', ud_addr = '%s', ud_update = 0 WHERE ud_id = %d",
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
dbesc(z_root()),
dbesc($addr),
intval($u[0]['ud_id'])

View File

@@ -4,260 +4,417 @@ namespace Zotlabs\Lib;
require_once('include/html2plain.php');
class MessageFilter {
class MessageFilter
{
protected $lastMatch = '';
protected $item = null;
protected $include = '';
protected $exclude = '';
protected $options = [];
protected $tags = null;
protected $language = '';
protected $text = '';
protected $excludeRules = [];
protected $includeRules = [];
public static function evaluate($item, $incl, $excl) {
public function __construct($item, $include = '', $exclude = '', $options = [])
{
$this->item = $item;
$this->include = $include;
$this->exclude = $exclude;
$this->options = $options;
$this->setup();
}
$text = prepare_text($item['body'], ((isset($item['mimetype'])) ? $item['mimetype'] : 'text/bbcode'));
$text = html2plain((!empty($item['title'])) ? $item['title'] . ' ' . $text : $text);
protected function setup()
{
// Option: plaintext
// Improve language detection by providing a plaintext version of $item['body'] which has no markup constructs/tags.
$lang = null;
if ((strpos($incl, 'lang=') !== false) || (strpos($excl, 'lang=') !== false) || (strpos($incl, 'lang!=') !== false) || (strpos($excl, 'lang!=') !== false)) {
$lang = detect_language($text);
}
if (array_key_exists('plaintext', $this->options)) {
$this->text = $this->options['plaintext'];
} else {
$this->text = $this->item['body'];
}
$tags = ((isset($item['term']) && is_array($item['term']) && count($item['term'])) ? $item['term'] : false);
$this->language = '';
$until = null;
// Language matching is a bit tricky, because the language can be ambiguous (detect_language() returns '').
// If the language is ambiguous, the message will pass (be accepted) regardless of language rules.
// exclude always has priority
if (str_contains($this->include, 'lang=')
|| str_contains($this->exclude, 'lang=')
|| str_contains($this->include, 'lang!=')
|| str_contains($this->exclude, 'lang!=')) {
$this->language = detect_language($this->text);
}
$exclude = (($excl) ? explode("\n", $excl) : null);
$this->tags = ((isset($this->item['term']) && is_array($this->item['term'])
&& count($this->item['term'])) ? $this->item['term'] : null);
if ($exclude) {
foreach ($exclude as $word) {
$word = html_entity_decode(trim($word));
if (! $word) {
continue;
}
if (isset($lang) && ((strpos($word, 'lang=') === 0) || (strpos($word, 'lang!=') === 0))) {
if (!strlen($lang)) {
// Result is ambiguous. As we are matching deny rules only at this time, continue tests.
// Any matching deny rule concludes testing.
continue;
}
if (strpos($word, 'lang=') === 0 && strcasecmp($lang, trim(substr($word, 5))) == 0) {
return false;
} elseif (strpos($word, 'lang!=') === 0 && strcasecmp($lang, trim(substr($word, 6))) != 0) {
return false;
}
}
elseif (str_starts_with($word, 'until=')) {
$until = strtotime(trim(substr($word, 6)));
if ($until > strtotime($item['created'] . ' UTC')) {
return false;
}
}
elseif (substr($word, 0, 1) === '#' && $tags) {
foreach ($tags as $t) {
if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return false;
}
}
} elseif (substr($word, 0, 1) === '$' && $tags) {
foreach ($tags as $t) {
if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return false;
}
}
} elseif (substr($word, 0, 2) === '?+') {
if (self::test_condition(substr($word, 2), $item['obj'])) {
return false;
}
} elseif (substr($word, 0, 1) === '?') {
if (self::test_condition(substr($word, 1), $item)) {
return false;
}
} elseif ((strpos($word, '/') === 0) && preg_match($word, $text)) {
return false;
} elseif (stristr($text, $word) !== false) {
return false;
}
}
}
$this->excludeRules = $this->parse($this->exclude);
$this->includeRules = $this->parse($this->include);
$include = (($incl) ? explode("\n", $incl) : null);
}
if ($include) {
foreach ($include as $word) {
$word = html_entity_decode(trim($word));
if (! $word) {
continue;
}
if (isset($lang) && ((strpos($word, 'lang=') === 0) || (strpos($word, 'lang!=') === 0))) {
if (!strlen($lang)) {
// Result is ambiguous. However we are checking allow rules
// and an ambiguous language is always permitted.
return true;
}
if (strpos($word, 'lang=') === 0 && strcasecmp($lang, trim(substr($word, 5))) == 0) {
return true;
} elseif (strpos($word, 'lang!=') === 0 && strcasecmp($lang, trim(substr($word, 6))) != 0) {
return true;
}
}
elseif (str_starts_with($word, 'until=')) {
$until = strtotime(trim(substr($word, 6)));
if ($until > strtotime($item['created'] . ' UTC')) {
return true;
}
}
elseif (substr($word, 0, 1) === '#' && $tags) {
foreach ($tags as $t) {
if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return true;
}
}
} elseif (substr($word, 0, 1) === '$' && $tags) {
foreach ($tags as $t) {
if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) {
return true;
}
}
} elseif (substr($word, 0, 2) === '?+') {
if (self::test_condition(substr($word, 2), $item['obj'])) {
return true;
}
} elseif (substr($word, 0, 1) === '?') {
if (self::test_condition(substr($word, 1), $item)) {
return true;
}
} elseif ((strpos($word, '/') === 0) && preg_match($word, $text)) {
return true;
} elseif (stristr($text, $word) !== false) {
return true;
}
}
} else {
return true;
}
protected function parse($string): array
{
$rules = [];
if (! strlen($string)) {
return $rules;
}
return false;
}
$phrases = preg_split("/(\s\|\|\s|\s&&\s|\n)/", $string, flags: PREG_SPLIT_DELIM_CAPTURE);
if (!$phrases) {
return $rules;
}
for ($index = 0; $index < count($phrases); $index ++) {
// Even indices are rules and odd indices are operations, linefeed is an implict OR.
if (!($index & 1)) {
$currentRule = ['operation' => '', 'rule' => $phrases[$index]];
if ($index && isset($phrases[$index - 1])) {
$currentRule['operation'] = $phrases[$index - 1];
if ($currentRule['operation'] === "\n") {
$currentRule['operation'] = ' || ';
}
$index++;
}
$rules[] = $currentRule;
}
}
return $rules;
}
/**
* Evaluate a conditional expression with support for AND (&&) and OR (||) operators.
*
* - ?foo ~= baz which will check if item.foo contains the string 'baz';
* - ?foo == baz which will check if item.foo is the string 'baz';
* - ?foo != baz which will check if item.foo is not the string 'baz';
* - ?foo >= 3 which will check if item.foo is greater than or equal to 3;
* - ?foo > 3 which will check if item.foo is greater than 3;
* - ?foo <= 3 which will check if item.foo is less than or equal to 3;
* - ?foo < 3 which will check if item.foo is less than 3;
*
* - ?foo {} baz which will check if 'baz' is an array element in item.foo
* - ?foo {*} baz which will check if 'baz' is an array key in item.foo
* - ?foo which will check for a return of a true condition for item.foo;
public function evaluate(): bool
{
$previousResult = $newResult = null;
// exclude always has priority
$exclude = $this->excludeRules;
$include = $this->includeRules;
if ($exclude) {
foreach ($exclude as $rule) {
if (!strlen(trim($rule['rule']))) {
continue;
}
if (!strlen($this->language) && ((str_starts_with($rule['rule'], 'lang=')) || (str_starts_with($rule['rule'], 'lang!=')))) {
continue;
}
$result = $this->evaluateRule($rule['rule']);
switch ($rule['operation']) {
case '':
$previousResult = $newResult = $result;
break;
case ' || ':
$newResult = $previousResult || $result;
break;
case ' && ':
$newResult = $previousResult && $result;
break;
}
}
if ($newResult) {
return false;
}
}
$previousResult = $newResult = null;
if ($include) {
foreach ($include as $rule) {
if (!strlen(trim($rule['rule']))) {
continue;
}
if (!strlen($this->language) && ((str_starts_with($rule['rule'], 'lang=')) || (str_starts_with($rule['rule'], 'lang!=')))) {
continue;
}
$result = $this->evaluateRule($rule['rule']);
switch ($rule['operation']) {
case '':
$previousResult = $newResult = $result;
break;
case ' || ':
$newResult = $previousResult || $result;
break;
case ' && ':
$newResult = $previousResult && $result;
break;
}
}
}
return $newResult ?? true;
}
protected function evaluateRule($ruleText): bool
{
$ruleText = trim($ruleText);
if (($this->language) && ((str_starts_with($ruleText, 'lang=')) || (str_starts_with($ruleText, 'lang!=')))) {
if (str_starts_with($ruleText, 'lang=') && strcasecmp($this->language, trim(substr($ruleText, 5))) == 0) {
$this->lastMatch = $ruleText;
return true;
} elseif (str_starts_with($ruleText, 'lang!=') && strcasecmp($this->language, trim(substr($ruleText, 6))) != 0) {
$this->lastMatch = $ruleText;
return true;
}
} elseif (str_starts_with($ruleText, 'until=')) {
$until = strtotime(trim(substr($ruleText, 6)));
if ($until > strtotime($this->item['created'] . ' UTC')) {
$this->lastMatch = $ruleText;
return true;
}
} elseif (str_starts_with($ruleText, '#') && $this->tags) {
// #hashtag match
foreach ($this->tags as $t) {
if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (!strcasecmp($t['term'], substr($ruleText, 1)) || (substr($ruleText, 1) === '*'))) {
$this->lastMatch = $ruleText;
return true;
}
}
// hashtag count match
if (substr($ruleText, 1, 1) === '>') {
$hashtagLimit = (int)substr($ruleText, 2);
$hashtagCount = 0;
foreach ($this->tags as $t) {
if ($t['ttype'] == TERM_HASHTAG || $t['ttype'] == TERM_COMMUNITYTAG) {
$hashtagCount++;
}
}
if ($hashtagLimit && $hashtagCount > $hashtagLimit) {
$this->lastMatch = $ruleText;
return true;
}
}
} elseif (str_starts_with($ruleText, '@') && $this->tags) {
// @mention match
foreach ($this->tags as $t) {
if ((($t['ttype'] == TERM_MENTION && (!strcasecmp($t['term'], substr($ruleText, 1)))) || (substr($ruleText, 1) === '*'))) {
$this->lastMatch = $ruleText;
return true;
}
}
// mention count match
if (substr($ruleText, 1, 1) === '>') {
$mentionLimit = (int)substr($ruleText, 2);
$mentionCount = 0;
foreach ($this->tags as $t) {
if ($t['ttype'] == TERM_MENTION) {
$mentionCount++;
}
}
if ($mentionLimit && $mentionCount > $mentionLimit) {
$this->lastMatch = $ruleText;
return true;
}
}
} elseif (str_starts_with($ruleText, '$') && $this->tags) {
foreach ($this->tags as $t) {
if (($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($ruleText, 1)) || (substr($ruleText, 1) === '*'))) {
$this->lastMatch = $ruleText;
return true;
}
}
} elseif (str_starts_with($ruleText, '?+') && is_array($this->item['obj'])) {
if ($this->test_condition(substr($ruleText, 2), $this->item['obj'])) {
$this->lastMatch = $ruleText;
return true;
}
} elseif (str_starts_with($ruleText, '?')) {
$this->item['ua'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (empty($this->item['app'])) {
$author_app = $this->item['author']['site_project'] ?? '';
if (!$author_app && isset($this->item['author'])) {
if (str_contains($this->item['author']['xchan_hash'], 'threads.net') || str_contains($this->item['author']['xchan_hash'], 'threads.com')) {
$author_app = 'threads';
}
}
$this->item['app'] = $author_app;
}
if ($this->test_condition(substr($ruleText, 1), $this->item)) {
unset($this->item['ua']);
$this->lastMatch = $ruleText;
return true;
}
unset($this->item['ua']);
} elseif ((str_starts_with($ruleText, '/')) && preg_match($ruleText, $this->item['body'])) {
$this->lastMatch = $ruleText;
return true;
} elseif (stristr($this->item['body'], $ruleText) !== false) {
$this->lastMatch = $ruleText;
return true;
}
return false;
}
public function getLastMatch(): string
{
return $this->lastMatch;
}
public function setLastMatch($string): MessageFilter
{
$this->lastMatch = $string;
return $this;
}
/**
* @brief Test for Conditional Execution conditions. Shamelessly ripped off from src/Render/Comanche
*
* This is extensible. The first version of variable testing supports tests of the forms:
*
* - ?foo ~= baz will check if item.foo contains the string 'baz';
* - ?foo == baz will check if item.foo is the string 'baz';
* - ?foo != baz will check if item.foo is not the string 'baz';
* - ?foo // baz will check if item.foo matches the regular expression 'baz';
* - ?foo >= 3 will check if item.foo is greater than or equal to 3;
* - ?foo > 3 will check if item.foo is greater than 3;
* - ?foo <= 3 will check if item.foo is less than or equal to 3;
* - ?foo < 3 will check if item.foo is less than 3;
* - ?foo & 2 will check if item.foo has the second bit set.
* - ?foo !& 2 will check if item.foo does not have the second bit set.
*
* - ?foo {} baz which will check if 'baz' is an array element in item.foo
* - ?foo {*} baz which will check if 'baz' is an array key in item.foo
* - ?foo which will check for a return of a true condition for item.foo;
* - ?!foo which will check for a return of a false condition for item.foo;
*
* The values 0, '', an empty array, and an unset value will all evaluate to false.
*
* @param string $s The condition string to evaluate.
* @param array $item The associative array providing variable values.
* @return bool True if the condition is met, false otherwise.
*/
* The values 0, '', an empty array, and an unset value will all evaluate to false.
*
* @param string $s
* @param array $item
* @return bool
*/
protected function test_condition($s,$item)
{
$s = trim($s);
public static function test_condition($s, $item) {
$s = trim($s);
if (preg_match('/(.*?)\s&\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x & (int) trim($matches[2])) {
return true;
}
return false;
}
// Handle OR (||)
// Split on '||' not inside quotes
$or_parts = preg_split('/\s*\|\|\s*/', $s);
if (count($or_parts) > 1) {
foreach ($or_parts as $part) {
if (self::test_condition(ltrim($part, '?+'), $item)) {
return true;
}
}
return false;
}
if (preg_match('/(.*?)\s!&\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if (!($x & (int) trim($matches[2]))) {
return true;
}
return false;
}
// Handle AND (&&)
// Split on '&&' not inside quotes
$and_parts = preg_split('/\s*\&\&\s*/', $s);
if (count($and_parts) > 1) {
foreach ($and_parts as $part) {
if (!self::test_condition(ltrim($part, '?+'), $item)) {
return false;
}
}
return true;
}
if (preg_match('/(.*?)\s~=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if (is_string($x) && stripos($x, trim($matches[2])) !== false) {
return true;
}
return false;
}
// Basic checks
if (preg_match('/(.*?)\s==\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x == trim($matches[2])) {
return true;
}
return false;
}
// Contains substring (case-insensitive)
if (preg_match('/(.*?)\s\~\=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return (stripos($x, trim($matches[2])) !== false);
}
if (preg_match('/(.*?)\s!=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x != trim($matches[2])) {
return true;
}
return false;
}
// Equality
if (preg_match('/(.*?)\s\=\=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return ($x == trim($matches[2]));
}
if (preg_match('/(.*?)\s\/\/\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if (substr(trim($matches[2]),0,1) !== substr(trim($matches[2]),-1)) {
$matches[2] = '/' . trim($matches[2]) . '/';
}
if (preg_match(trim($matches[2]), $x)) {
return true;
}
return false;
}
// Inequality
if (preg_match('/(.*?)\s\!\=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return ($x != trim($matches[2]));
}
if (preg_match('/(.*?)\s>=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x >= trim($matches[2])) {
return true;
}
return false;
}
// Greater than or equal
if (preg_match('/(.*?)\s\>\=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return ($x >= trim($matches[2]));
}
if (preg_match('/(.*?)\s<=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x <= trim($matches[2])) {
return true;
}
return false;
}
// Less than or equal
if (preg_match('/(.*?)\s\<\=\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return ($x <= trim($matches[2]));
}
if (preg_match('/(.*?)\s>\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x > trim($matches[2])) {
return true;
}
return false;
}
// Greater than
if (preg_match('/(.*?)\s\>\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return ($x > trim($matches[2]));
}
if (preg_match('/(.*?)\s<\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x < trim($matches[2])) {
return true;
}
return false;
}
// Less than
if (preg_match('/(.*?)\s\<\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return ($x < trim($matches[2]));
}
// Array contains value
if (preg_match('/(.*?)\s\{\}\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if (is_array($x) && in_array(trim($matches[2]), $x)) {
return true;
}
return false;
}
// Array contains value
if (preg_match('/(.*?)\s\{\}\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return (is_array($x) && in_array(trim($matches[2]), $x));
}
// Array contains key
if (preg_match('/(.*?)\s\{\*\}\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return (is_array($x) && array_key_exists(trim($matches[2]), $x));
}
// Ordering of this check (for falsiness) with relation to the following one (check for truthiness) is important.
// Falsy check
if (preg_match('/\!(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return !$x;
}
// Truthy check (default)
if (preg_match('/(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR);
return (bool)$x;
}
// If no conditions matched, return false
return false;
}
// Array contains key
if (preg_match('/(.*?)\s\{\*}\s(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if (is_array($x) && array_key_exists(trim($matches[2]), $x)) {
return true;
}
return false;
}
// Ordering of this check (for falseness) with relation to the following one (check for truthiness) is important.
if (preg_match('/!(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if (!$x) {
return true;
}
return false;
}
if (preg_match('/(.*?)$/', $s, $matches)) {
$x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR);
if ($x) {
return true;
}
return false;
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* SPDX-FileCopyrightText: 2026 The Hubzilla Community
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Lib;
use DBA;
use PDO;
/**
* Concrete implementation for getting stats from MySQL and MariaDB databases.
*/
class MySQLDbStats extends DbStats {
public function getQueries(): int {
//
// We can't use the regular Hubzilla db helper function here, as
// it will only return information from a `SELECT` statement.
//
// Use the basic PDO access instead.
//
$query = DBA::$dba->db->prepare('SHOW STATUS LIKE "Queries"');
$query->execute();
$result = $query->fetch(PDO::FETCH_ASSOC);
logger(print_r($result, true));
if (!empty($result)) {
return $result['Value'] ?? -1;
}
return 0;
}
}

40
Zotlabs/Lib/ObjCache.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
namespace Zotlabs\Lib;
class ObjCache
{
public static function Get($path, $type = 'as')
{
if (!$path) {
return [];
}
$localpath = Hashpath::path($path, 'store/[data]/[obj]/' . $type, 2, alg: 'sha256');
if (file_exists($localpath)) {
return json_unserialize(file_get_contents($localpath));
}
return [];
}
public static function Set($path, $content, $type = 'as') {
if (!$path) {
return;
}
$localpath = Hashpath::path($path, 'store/[data]/[obj]/' . $type, 2, alg: 'sha256');
file_put_contents($localpath, json_serialize($content));
}
public static function Delete($path, $type = 'as') {
if (!$path) {
return;
}
$localpath = Hashpath::path($path, 'store/[data]/[obj]/' . $type, 2, alg: 'sha256');
if (file_exists($localpath)) {
unlink($localpath);
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* SPDX-FileCopyrightText: 2026 The Hubzilla Community
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Lib;
use DBA;
/**
* Concrete implementation for getting stats from PostgreSQL databases.
*/
class PostgresDbStats extends DbStats {
public function getQueries(): int {
$sqlGetQps = <<<'SQL'
select (xact_commit + xact_rollback) as queries
from pg_stat_database
where datname='%s'
SQL;
$result = q($sqlGetQps, DBA::$dba->dbname);
if (!empty($result)) {
return $result[0]['queries'] ?? -1;
}
return 0;
}
}

View File

@@ -7,6 +7,20 @@ use Zotlabs\Zot6\Zot6Handler;
class Queue {
/**
* Get number of entries in the out queue.
*
* When delivery is successful, the item is removed from the out queue, so
* the number of items in the queue reflects the number of pending delivery
* attempts.
*
* @return int Number of items in the out queue.
*/
static function count(): int {
$r = dbq('select count(*) as total from outq');
return $r[0]['total'] ?? 0;
}
static function update($id, $add_priority = 0) {
logger('queue: requeue item ' . $id,LOGGER_DEBUG);

View File

@@ -0,0 +1,28 @@
<?php
/*
* SPDX-FileCopyrightText: 2026 The Hubzilla Community
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Lib;
class QueueWorkerStats
{
public readonly int $size;
public readonly int $active;
public function __construct() {
$query = <<<'SQL'
select count(*) as total from workerq
union (select count(*) as qworkers from workerq where workerq_reservationid is not null)
SQL;
$result = dbq('select count(*) as total from workerq');
$this->size = !empty($result) ? $result[0]['total'] : -1;
$result = dbq('select count(*) as qworkers from workerq where workerq_reservationid is not null');
$this->active = !empty($result) ? $result[0]['qworkers'] : -1;
}
}

View File

@@ -118,26 +118,28 @@ class Share {
$photo_bb = $object['body'];
}
if (strpos($this->item['body'], "[/share]") !== false) {
$pos = strpos($this->item['body'], "[share");
$bb = substr($this->item['body'], $pos);
} else {
$bb = "[share author='".urlencode($this->item['author']['xchan_name']).
"' profile='" . $this->item['author']['xchan_url'] .
"' avatar='" . $this->item['author']['xchan_photo_s'] .
"' link='" . $this->item['plink'] .
"' auth='" . (($this->item['author']['xchan_network'] === 'zot6') ? 'true' : 'false') .
"' posted='" . $this->item['created'] .
"' message_id='" . $this->item['mid'] .
"']";
if($this->item['title'])
if (!str_contains($this->item['body'], '[/share]')) {
$quote = in_array($this->item['author']['xchan_network'], ['zot6', 'activitypub']) ? "quote='true'" : '';
$bb .= "[share author='" . urlencode($this->item['author']['xchan_name']) . "'
profile='" . $this->item['author']['xchan_url'] . "'
avatar='" . $this->item['author']['xchan_photo_s'] . "'
link='" . $this->item['plink'] . "'
auth='" . (($this->item['author']['xchan_network'] === 'zot6') ? 'true' : 'false') . "'
posted='" . $this->item['created'] . "'
message_id='" . $this->item['mid'] . "'
$quote
]";
if ($this->item['title']) {
$bb .= '[h3][b]'.$this->item['title'].'[/b][/h3]'."\r\n";
}
$bb .= (($is_photo) ? $photo_bb . "\r\n" . $this->item['body'] : $this->item['body']);
$bb .= "[/share]";
}
return $bb;
}
}

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Lib;
use App;
use DBA;
use Zotlabs\Access\AccessList;
require_once('include/text.php');
@@ -120,10 +121,10 @@ class ThreadItem {
$locktype = 0;
}
$shareable = ((local_channel() && $conv->get_profile_owner() == local_channel()) && (intval($item['item_private']) === 0));
$shareable = ((local_channel() && $conv->get_profile_owner() == local_channel()) && (intval($item['item_private']) === 0) && !str_contains($item['body'], '[/share]'));
// allow an exemption for sharing stuff from your private feeds
if($item['author']['xchan_network'] === 'rss')
if ($item['author']['xchan_network'] === 'rss')
$shareable = true;
$repeatable = ((local_channel() && $conv->get_profile_owner() == local_channel()) && intval($item['item_private']) === 0 && in_array($item['author']['xchan_network'], ['zot6', 'activitypub']));
@@ -283,9 +284,12 @@ class ThreadItem {
$reply_to = [];
$reactions_allowed = false;
if($this->is_commentable() && $observer) {
if($this->is_commentable()) {
$reply_to = array( t("Reply to this message"), t("reply"), t("Reply to"));
$reactions_allowed = true;
if ($observer) {
$reactions_allowed = true;
}
}
$share = [];
@@ -412,7 +416,7 @@ class ThreadItem {
'isotime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'c'),
'localtime' => datetime_convert('UTC', date_default_timezone_get(), $item['created']),
'editedtime' => (($item['edited'] != $item['created']) ? sprintf(t('Last edited %s'), relative_time($item['edited'])) : ''),
'expiretime' => (($item['expires'] > NULL_DATE) ? sprintf(t('Expires %s'), relative_time($item['expires'])) : ''),
'expiretime' => (($item['expires'] > DBA::$dba->get_null_date()) ? sprintf(t('Expires %s'), relative_time($item['expires'])) : ''),
'lock' => $lock,
'locktype' => $locktype,
'delayed' => (($item['item_delayed']) ? sprintf(t('Published %s'), relative_time($item['created'])) : ''),

View File

@@ -8,6 +8,7 @@
namespace Zotlabs\Module;
use DBA;
use Zotlabs\Lib\Config;
require_once('include/account.php');
@@ -90,7 +91,7 @@ class Admin extends \Zotlabs\Web\Controller {
$r = q("SELECT COUNT(CASE WHEN account_id > 0 THEN 1 ELSE NULL END) AS total, COUNT(CASE WHEN account_expires > %s THEN 1 ELSE NULL END) AS expiring, COUNT(CASE WHEN account_expires < %s AND account_expires > '%s' THEN 1 ELSE NULL END) AS expired, COUNT(CASE WHEN (account_flags & %d)>0 THEN 1 ELSE NULL END) AS blocked FROM account",
db_utcnow(),
db_utcnow(),
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
intval(ACCOUNT_BLOCKED)
);
if ($r) {

View File

@@ -341,29 +341,27 @@ class Accounts {
* @SuppressWarnings(PHPMD.ShortVariable)
*/
private function block_unblock_accounts(): void {
if (!isset($_POST['user']) || !isset($_POST['blocked'])) {
if (!isset($_POST['user'])) {
return;
}
$users = $_POST['user'];
$blocked = $_POST['blocked'];
if (!is_array($users) || !is_array($blocked)) {
if (!is_array($users)) {
return;
}
foreach($users as $i => $id) {
// if account is blocked remove blocked bit-flag, otherwise add blocked bit-flag
$op = $blocked[$i] ? '& ~' : '| ';
$xor = db_getfunc('^');
q("UPDATE account SET account_flags = (account_flags $op%d) WHERE account_id = %d",
foreach($users as $id) {
q("UPDATE account SET account_flags = (account_flags $xor %d) WHERE account_id = %d",
intval(ACCOUNT_BLOCKED),
intval($id)
);
}
$count = count($users);
$fmt = tt("%s account blocked/unblocked", "%s account blocked/unblocked", $count);
$fmt = tt("%s account blocked/unblocked", "%s accounts blocked/unblocked", $count);
notice(sprintf($fmt, $count));
}

View File

@@ -82,7 +82,7 @@ class Attach_edit extends Controller {
$admin_delete = false;
$is_creator = (($creator == $observer_hash) ? true : false);
$move = ((! $copy && ($folder !== $newfolder || (($single) ? $filename !== $newfilename : false))) ? true : false);
$move = ((!$delete && !$copy && ($folder !== $newfolder || (($single) ? $filename !== $newfilename : false))) ? true : false);
$perms = get_all_perms($channel_id, $observer_hash);

View File

@@ -83,37 +83,31 @@ class Bookmarks extends \Zotlabs\Web\Controller {
$channel = \App::get_channel();
$o = '';
$o .= '<div class="generic-content-wrapper-styled">';
$o .= '<h3>' . t('Bookmarks') . '</h3>';
$x = menu_list(local_channel(),'',MENU_BOOKMARK);
if($x) {
foreach($x as $xx) {
$y = menu_fetch($xx['menu_name'],local_channel(),get_observer_hash());
$o .= menu_render($y,'',true);
$bookmarks = [];
$x = menu_list(local_channel(), '', MENU_BOOKMARK);
if ($x) {
foreach ($x as $xx) {
$y = menu_fetch($xx['menu_name'], local_channel(), get_observer_hash());
$bookmarks[] = menu_render($y, '', true);
}
}
$o .= '<h3>' . t('My Connections Bookmarks') . '</h3>';
$x = menu_list(local_channel(),'',MENU_SYSTEM|MENU_BOOKMARK);
if($x) {
foreach($x as $xx) {
$y = menu_fetch($xx['menu_name'],local_channel(),get_observer_hash());
$o .= menu_render($y,'',true);
$conn_bookmarks = [];
$x = menu_list(local_channel(), '', MENU_SYSTEM | MENU_BOOKMARK);
if ($x) {
foreach ($x as $xx) {
$y = menu_fetch($xx['menu_name'], local_channel(), get_observer_hash());
$conn_bookmarks[] = menu_render($y, '', true);
}
}
$o .= '</div>';
return $o;
return replace_macros(get_markup_template('bookmarks.tpl'), [
'$title1' => t('Bookmarks'),
'$title2' => t('My Connections Bookmarks'),
'$bookmarks' => $bookmarks,
'$conn_bookmarks' => $conn_bookmarks,
]);
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Zotlabs\Module;
use DBA;
class Changeaddr extends \Zotlabs\Web\Controller {
@@ -29,7 +30,7 @@ class Changeaddr extends \Zotlabs\Web\Controller {
if(! ($x && $x['account']))
return;
if($account['account_password_changed'] > NULL_DATE) {
if($account['account_password_changed'] > DBA::$dba->get_null_date()) {
$d1 = datetime_convert('UTC','UTC','now - 48 hours');
if($account['account_password_changed'] > $d1) {
notice( t('Channel name changes are not allowed within 48 hours of changing the account password.') . EOL);

View File

@@ -148,14 +148,14 @@ class Channel extends Controller {
'rel' => 'alternate',
'type' => 'application/atom+xml',
'title' => t('Posts and comments'),
'href' => z_root() . '/feed/' . $which
'href' => z_root() . '/feed/' . $which . '?top=0'
]);
head_add_link([
'rel' => 'alternate',
'type' => 'application/atom+xml',
'title' => t('Only posts'),
'href' => z_root() . '/feed/' . $which . '?f=&top=1'
'href' => z_root() . '/feed/' . $which . '?top=1'
]);

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Module;
use App;
use DBA;
use Zotlabs\Web\Controller;
use Zotlabs\Lib\Libsync;
use Zotlabs\Access\AccessList;
@@ -300,7 +301,7 @@ class Channel_calendar extends Controller {
from event left join item on item.resource_id = event.event_hash
where event.uid = %d and event.dtstart > '%s' and event.dtend > event.dtstart",
intval(local_channel()),
dbesc(NULL_DATE)
dbesc(DBA::$dba->get_null_date())
);
}
else {

View File

@@ -2,6 +2,7 @@
namespace Zotlabs\Module;
use App;
use DBA;
use Zotlabs\Lib\Config;
use Zotlabs\Web\Controller;
@@ -232,7 +233,7 @@ class Dirsearch extends Controller {
$spkt = array('transactions' => array());
$r = q("SELECT * FROM updates WHERE ud_update = 0 AND ud_last = '%s' AND ud_date >= '%s' ORDER BY ud_date DESC",
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
dbesc($sync)
);

View File

@@ -61,12 +61,10 @@ class Dreport extends \Zotlabs\Web\Controller {
return;
}
$r = q("select * from dreport where dreport_xchan = '%s' and (dreport_mid = '%s' or dreport_mid = '%s' or dreport_mid = '%s' or dreport_mid = '%s')",
$r = q("select * from dreport where dreport_xchan = '%s' and (dreport_mid = '%s' or dreport_mid = '%s')",
dbesc($channel['channel_hash']),
dbesc($mid),
dbesc($mid . '#sync'),
dbesc(str_replace('/item/', '/activity/', $mid)),
dbesc(str_replace('/item/', '/activity/', $mid) . '#sync')
dbesc(str_replace('/item/', '/activity/', $mid))
);
if(! $r) {

View File

@@ -1,22 +0,0 @@
<?php
namespace Zotlabs\Module;
require_once('include/security.php');
require_once('include/bbcode.php');
class Embed extends \Zotlabs\Web\Controller {
function init() {
$post_id = ((argc() > 1) ? intval(argv(1)) : 0);
if(! $post_id)
killme();
echo '[share=' . $post_id . '][/share]';
killme();
}
}

View File

@@ -2,49 +2,47 @@
namespace Zotlabs\Module;
use DBA;
use Zotlabs\Lib\PConfig;
use Zotlabs\Web\Controller;
require_once('include/items.php');
class Feed extends \Zotlabs\Web\Controller {
class Feed extends Controller {
function init() {
$params = [];
$params['begin'] = ((x($_REQUEST,'date_begin')) ? $_REQUEST['date_begin'] : NULL_DATE);
$params['end'] = ((x($_REQUEST,'date_end')) ? $_REQUEST['date_end'] : '');
$params['type'] = ((stristr(argv(0),'json')) ? 'json' : 'xml');
$params['pages'] = ((x($_REQUEST,'pages')) ? intval($_REQUEST['pages']) : 0);
$params['top'] = ((x($_REQUEST,'top')) ? intval($_REQUEST['top']) : 0);
$params['start'] = ((x($_REQUEST,'start')) ? intval($_REQUEST['start']) : 0);
$params['records'] = ((x($_REQUEST,'records')) ? intval($_REQUEST['records']) : 10);
$params['direction'] = ((x($_REQUEST,'direction')) ? dbesc($_REQUEST['direction']) : 'desc');
$params['cat'] = ((x($_REQUEST,'cat')) ? escape_tags($_REQUEST['cat']) : '');
$params['compat'] = ((x($_REQUEST,'compat')) ? intval($_REQUEST['compat']) : 0);
if (argc() < 2) {
killme();
}
if(! in_array($params['direction'],['asc','desc'])) {
if (observer_prohibited(true)) {
killme();
}
$channel = channelx_by_nick(argv(1));
if (!$channel) {
killme();
}
$params['begin'] = $_REQUEST['date_begin'] ?? DBA::$dba->get_null_date();
$params['end'] = $_REQUEST['date_end'] ?? '';
$params['type'] = 'xml';
$params['pages'] = ((!empty($_REQUEST['pages'])) ? intval($_REQUEST['pages']) : 0);
$params['top'] = ((array_key_exists('top', $_REQUEST)) ? intval($_REQUEST['top']) : PConfig::Get($channel['channel_id'], 'system', 'channel_simple_feed', 1));
$params['start'] = ((!empty($_REQUEST['start'])) ? intval($_REQUEST['start']) : 0);
$params['records'] = ((!empty($_REQUEST['records'])) ? intval($_REQUEST['records']) : 10);
$params['cat'] = ((!empty($_REQUEST['cat'])) ? escape_tags($_REQUEST['cat']) : '');
$params['compat'] = ((!empty($_REQUEST['compat'])) ? intval($_REQUEST['compat']) : 0);
$params['direction'] = ((!empty($_REQUEST['direction'])) ? dbesc($_REQUEST['direction']) : 'desc');
if (!in_array($params['direction'], ['asc', 'desc'])) {
$params['direction'] = 'desc';
}
if(argc() > 1) {
logger('public feed request from ' . $_SERVER['REMOTE_ADDR'] . ' for ' . $channel['channel_address']);
if(observer_prohibited(true)) {
killme();
}
echo get_public_feed($channel, $params);
killme();
$channel = channelx_by_nick(argv(1));
if(! $channel) {
killme();
}
logger('public feed request from ' . $_SERVER['REMOTE_ADDR'] . ' for ' . $channel['channel_address']);
echo get_public_feed($channel,$params);
killme();
}
}
}

View File

@@ -7,6 +7,7 @@ require_once('include/import.php');
require_once('include/perm_upgrade.php');
use App;
use DBA;
use URLify;
use Zotlabs\Daemon\Master;
use Zotlabs\Lib\Config;
@@ -331,7 +332,7 @@ class Import extends Controller {
else {
$photos = import_xchan_photo($xchan['xchan_photo_l'], $xchan['xchan_hash']);
if ($photos[4])
$photodate = NULL_DATE;
$photodate = DBA::$dba->get_null_date();
else
$photodate = $xchan['xchan_photo_date'];

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Module;
use App;
use DBA;
use URLify;
use Zotlabs\Lib\Config;
use Zotlabs\Lib\IConfig;
@@ -17,6 +18,7 @@ use Zotlabs\Lib\Libzot;
use Zotlabs\Lib\Libsync;
use Zotlabs\Lib\ThreadListener;
use Zotlabs\Access\PermissionRoles;
use Zotlabs\Lib\ObjCache;
require_once('include/crypto.php');
require_once('include/items.php');
@@ -185,7 +187,8 @@ class Item extends Controller {
$obj_type = ((!empty($_POST['obj_type'])) ? escape_tags($_POST['obj_type']) : 'Note');
// allow API to bulk load a bunch of imported items with sending out a bunch of posts.
$nopush = ((!empty($_POST['nopush'])) ? intval($_POST['nopush']) : 0);
$nopush = ((!empty($_POST['nopush'])) ? intval($_POST['nopush']) : $item_type !== ITEM_TYPE_POST);
/*
* Check service class limits
@@ -207,7 +210,7 @@ class Item extends Controller {
}
$expires = NULL_DATE;
$expires = DBA::$dba->get_null_date();
$route = '';
$parent_item = null;
@@ -557,7 +560,7 @@ class Item extends Controller {
if (!empty($_POST['expire'])) {
$expires = datetime_convert(date_default_timezone_get(), 'UTC', $_POST['expire']);
if ($expires <= datetime_convert())
$expires = NULL_DATE;
$expires = DBA::$dba->get_null_date();
}
}
@@ -755,7 +758,7 @@ class Item extends Controller {
$cats = explode(',', $categories);
foreach ($cats as $cat) {
$catlink = $owner_xchan['xchan_url'] . '?f=&cat=' . urlencode(trim($cat));
$catlink = channel_url($channel) . '?cat=' . urlencode(trim($cat));
$post_tags[] = [
'uid' => $profile_uid,
@@ -799,7 +802,7 @@ class Item extends Controller {
$item_origin = (($origin) ? 1 : 0);
$item_consensus = (($consensus) ? 1 : 0);
$item_nocomment = (($nocomment) ? 1 : 0);
$comments_closed = (($nocomment) ? $comments_closed : NULL_DATE);
$comments_closed = (($nocomment) ? $comments_closed : DBA::$dba->get_null_date());
// determine if this is a wall post
@@ -873,7 +876,7 @@ class Item extends Controller {
if ($obj['endTime']) {
$d = datetime_convert('UTC','UTC', $obj['endTime']);
if ($d > NULL_DATE) {
if ($d > DBA::$dba->get_null_date()) {
$comments_closed = $d;
}
}
@@ -1041,23 +1044,16 @@ class Item extends Controller {
$x = item_store_update($datarray, $execflag);
if ($x['success']) {
if ($x['success'] && intval($item_type) === ITEM_TYPE_POST) {
$item = [$x['item']];
xchan_query($item);
$item = fetch_post_tags($item);
$encoded_item = Activity::build_packet(Activity::encode_activity($item[0]), $channel, false);
ObjCache::Set($item[0]['mid'], $encoded_item);
$this->add_listeners($datarray);
}
/* sync this is done in item_store_update()
if (!$parent) {
$r = q("select * from item where id = %d",
intval($post_id)
);
if ($r) {
xchan_query($r);
$sync_item = fetch_post_tags($r);
Libsync::build_sync_packet($profile_uid, ['item' => [encode_item($sync_item[0], true)]]);
}
}
*/
if (!$nopush) {
Master::Summon(['Notifier', 'edit_post', $post_id]);
if (intval($x['approval_id'])) {
@@ -1080,9 +1076,17 @@ class Item extends Controller {
killme();
}
$post = item_store($datarray, $execflag);
if ($post['success']) {
if ($post['success'] && intval($item_type) === ITEM_TYPE_POST) {
$item = [$post['item']];
xchan_query($item);
// TODO: fetch_post_tags() will add term and iconfig twice if called twice and it looks like they are already added here
//$item = fetch_post_tags($item);
$encoded_item = Activity::build_packet(Activity::encode_activity($item[0]), $channel, false);
ObjCache::Set($item[0]['mid'], $encoded_item);
$this->add_listeners($datarray);
}
@@ -1162,19 +1166,6 @@ class Item extends Controller {
killme();
}
/* sync this is done in item_store_update()
if ($parent || $datarray['item_private'] == 1) {
$r = q("select * from item where id = %d",
intval($post_id)
);
if ($r) {
xchan_query($r);
$sync_item = fetch_post_tags($r);
Libsync::build_sync_packet($profile_uid, ['item' => [encode_item($sync_item[0], true)]]);
}
}
*/
$datarray['id'] = $post_id;
$datarray['llink'] = z_root() . '/display/' . $datarray['uuid'];
@@ -1213,11 +1204,6 @@ class Item extends Controller {
if ($mode === 'channel')
profile_load($channel['channel_address']);
$item[] = $datarray;
$item[0]['owner'] = $owner_xchan;
$item[0]['author'] = $observer;
$item[0]['attach'] = $datarray['attach'];
$json = [
'success' => 1,
'id' => $post_id,

View File

@@ -232,25 +232,17 @@ class Lockview extends Controller {
}
}
$access_list_header = '<div class="dropdown-header text-uppercase h6">' . t('Access') . '</div>';
$guest_access_list_header = '<div class="dropdown-header text-uppercase h6">' . t('Guest access') . '</div>';
$ocap_access_list_header = '<div class="dropdown-header text-uppercase h6">' . t('OCAP access') . '</div>';
$divider = '<div class="dropdown-divider"></div>';
$str = '';
$tpl = get_markup_template('access_dropdown.tpl');
if ($access_list) {
$str .= $access_list_header . implode($access_list);
}
echo replace_macros($tpl, [
'$access_header' => t('Access'),
'$guest_access_header' => t('Guest access'),
'$ocap_access_header' => t('OCAP access'),
if ($guest_access_list) {
$str .= $divider . $guest_access_list_header . implode($guest_access_list);
}
if ($ocap_access_list) {
$str .= $divider . $ocap_access_list_header . implode($ocap_access_list);
}
echo $str;
'$access_list' => $access_list ? implode($access_list) : '',
'$guest_access_list' => $guest_access_list ? implode($guest_access_list) : '',
'$ocap_access_list' => $ocap_access_list ? implode($ocap_access_list) : '',
]);
killme();
}

View File

@@ -6,6 +6,8 @@ use Zotlabs\Web\Controller;
use Zotlabs\Web\HTTPSig;
use Zotlabs\Lib\Libzot;
use Zotlabs\Lib\SConfig;
use GuzzleHttp\Psr7\Request;
use HttpSignature\HttpMessageSigner;
class Magic extends Controller {
@@ -101,26 +103,65 @@ class Magic extends Controller {
$dest = strip_zids($dest);
$dest = strip_query_param($dest,'f');
// We now post to the OWA endpoint. This improves security by providing a signed digest
// try RFC9421 first
$data = json_encode([ 'OpenWebAuth' => random_string() ]);
$request = new Request(
'GET',
$owapath,
[
'Host' => $parsed['host'],
'Date' => gmdate('D, d M Y H:i:s T'),
'Accept' => 'application/x-zot+json',
'X-Open-Web-Auth' => random_string(),
],
);
$headers = [];
$headers['Accept'] = 'application/x-zot+json' ;
$headers['Content-Type'] = 'application/x-zot+json' ;
$headers['X-Open-Web-Auth'] = random_string();
$headers['Host'] = $parsed['host'];
$headers['(request-target)'] = 'get /owa';
$signer = new HttpMessageSigner();
$signer->setPrivateKey($channel['channel_prvkey']);
$signer->setAlgorithm('rsa-v1_5-sha256');
$signer->setKeyId(channel_url($channel));
$signer->setCreated(time());
$signer->setExpires(time() + 3600);
$coveredFields = '("@method" "@target-uri" "host" "date" "accept" "x-open-web-auth")';
$request = $signer->signRequest($coveredFields, $request);
$signedHeaders = $signer->getHeaders($request);
$curlHeaders = [];
foreach ($signedHeaders as $key => $value) {
$curlHeaders[] = $key . ': ' . $value;
}
$headers = HTTPSig::create_sig($headers,$channel['channel_prvkey'], channel_url($channel),true,'sha512');
$redirects = 0;
$x = z_fetch_url($owapath, false, $redirects, ['headers' => $curlHeaders]);
logger('owa RFC9421 fetch returned: ' . print_r($x,true),LOGGER_DATA);
$x = z_fetch_url($owapath, false, $redirects, ['headers' => $headers]);
$rfc9421 = false;
logger('owa fetch returned: ' . print_r($x,true),LOGGER_DATA);
if ($x['success']) {
$rfc9421_result = json_decode($x['body'], true);
$rfc9421 = $rfc9421_result['success'];
}
if (!$rfc9421 || ($x['return_code'] >= 400 && $x['return_code'] != 404)) {
$headers = [];
$headers['Accept'] = 'application/x-zot+json' ;
$headers['Content-Type'] = 'application/x-zot+json' ;
$headers['X-Open-Web-Auth'] = random_string();
$headers['Host'] = $parsed['host'];
$headers['(request-target)'] = 'get /owa';
$headers = HTTPSig::create_sig($headers,$channel['channel_prvkey'], channel_url($channel),true,'sha512');
$redirects = 0;
$x = z_fetch_url($owapath, false, $redirects, ['headers' => $headers]);
logger('owa fetch returned: ' . print_r($x,true),LOGGER_DATA);
}
if ($x['success']) {
$j = json_decode($x['body'],true);
if ($j['success'] && $j['encrypted_token']) {
// decrypt the token using our private key
$token = '';
@@ -139,7 +180,6 @@ class Magic extends Controller {
echo $o;
killme();
}
}
}

View File

@@ -143,8 +143,11 @@ class Network extends \Zotlabs\Web\Controller {
}
}
if(x($_GET, 'search') || $file || (!$pf && $cid) || $hashtags || $verb || $category || $conv || $unseen)
if($search || $file || (!$pf && $cid) || $hashtags || $verb || $category || $conv || $unseen) {
$nouveau = true;
}
$dismiss_privacy_filter = array_intersect(['cid', 'star', 'conv', 'file', 'verb', 'cat', 'search'], array_keys($_GET));
$cid_r = [];
@@ -346,15 +349,15 @@ class Network extends \Zotlabs\Web\Controller {
// The name 'verb' is a holdover from the earlier XML
// ActivityStreams specification.
if (substr($verb, 0, 1) === '.') {
if (str_starts_with($verb, '.')) {
$sql_verb = substr($verb, 1);
$sql_extra .= sprintf(" AND item.obj_type like '%s' ",
dbesc(protect_sprintf('%' . $sql_verb . '%'))
$sql_extra .= sprintf(" AND item.obj_type = '%s' AND item.verb IN ('Create', 'Update', 'Invite') ",
dbesc(protect_sprintf($sql_verb))
);
}
else {
$sql_extra .= sprintf(" AND item.verb like '%s' ",
dbesc(protect_sprintf('%' . $verb . '%'))
$sql_extra .= sprintf(" AND item.verb = '%s' ",
dbesc(protect_sprintf($verb))
);
}
}
@@ -363,13 +366,14 @@ class Network extends \Zotlabs\Web\Controller {
$sql_extra .= term_query('item', $file, TERM_FILE);
}
if ($dm) {
$sql_extra .= ' AND item.item_private = 2 ';
if (!$dismiss_privacy_filter) {
if ($dm) {
$sql_extra .= ' AND item.item_private = 2 ';
}
else {
$sql_extra .= ' AND item.item_private IN (0, 1) ';
}
}
else {
$sql_extra .= ' AND item.item_private IN (0, 1) ';
}
if($conv) {
$item_thread_top = '';

View File

@@ -33,7 +33,7 @@ class New_channel extends \Zotlabs\Web\Controller {
// first name
if(strpos($x,' '))
$test[] = legal_webbie(substr($x,0,strpos($x,' ')));
if($test[0]) {
if (!empty($test[0])) {
// first name plus first initial of last
$test[] = ((strpos($x,' ')) ? $test[0] . legal_webbie(trim(substr($x,strpos($x,' '),2))) : '');
// first name plus random number
@@ -69,7 +69,7 @@ class New_channel extends \Zotlabs\Web\Controller {
// first name
if(strpos($x,' '))
$test[] = legal_webbie(substr($x,0,strpos($x,' ')));
if($test[0]) {
if (!empty($test[0])) {
// first name plus first initial of last
$test[] = ((strpos($x,' ')) ? $test[0] . legal_webbie(trim(substr($x,strpos($x,' '),2))) : '');
// first name plus random number

View File

@@ -1,69 +0,0 @@
<?php
namespace Zotlabs\Module;
require_once('include/contact_widgets.php');
require_once('include/items.php');
require_once("include/bbcode.php");
require_once('include/security.php');
require_once('include/conversation.php');
require_once('include/acl_selectors.php');
require_once('include/permissions.php');
/**
* @brief Channel Controller for broken OStatus implementations
*
*/
class Ochannel extends \Zotlabs\Web\Controller {
function init() {
$which = null;
if(argc() > 1)
$which = argv(1);
if(! $which) {
if(local_channel()) {
$channel = \App::get_channel();
if($channel && $channel['channel_address'])
$which = $channel['channel_address'];
}
}
if(! $which) {
notice( t('You must be logged in to see this page.') . EOL );
return;
}
$profile = 0;
$channel = \App::get_channel();
if((local_channel()) && (argc() > 2) && (argv(2) === 'view')) {
$which = $channel['channel_address'];
$profile = argv(1);
}
head_add_link( [
'rel' => 'alternate',
'type' => 'application/atom+xml',
'href' => z_root() . '/ofeed/' . $which
]);
// Run profile_load() here to make sure the theme is set before
// we start loading content
profile_load($which,$profile);
}
function get($update = 0, $load = false) {
if(argc() < 2)
return;
if($load)
$_SESSION['loadtime'] = datetime_convert();
return '<script>window.location.href = "' . z_root() . '/' . str_replace('ochannel/','channel/',\App::$query_string) . '";</script>';
}
}

View File

@@ -1,51 +0,0 @@
<?php
namespace Zotlabs\Module;
/* Ofeed: Broken feed for software which requires broken feeds */
require_once('include/items.php');
class Ofeed extends \Zotlabs\Web\Controller {
function init() {
$params = [];
$params['begin'] = ((x($_REQUEST,'date_begin')) ? $_REQUEST['date_begin'] : NULL_DATE);
$params['end'] = ((x($_REQUEST,'date_end')) ? $_REQUEST['date_end'] : '');
$params['type'] = ((stristr(argv(0),'json')) ? 'json' : 'xml');
$params['pages'] = ((x($_REQUEST,'pages')) ? intval($_REQUEST['pages']) : 0);
$params['top'] = ((x($_REQUEST,'top')) ? intval($_REQUEST['top']) : 0);
$params['start'] = ((x($_REQUEST,'start')) ? intval($_REQUEST['start']) : 0);
$params['records'] = ((x($_REQUEST,'records')) ? intval($_REQUEST['records']) : 10);
$params['direction'] = ((x($_REQUEST,'direction')) ? dbesc($_REQUEST['direction']) : 'desc');
$params['cat'] = ((x($_REQUEST,'cat')) ? escape_tags($_REQUEST['cat']) : '');
$params['compat'] = ((x($_REQUEST,'compat')) ? intval($_REQUEST['compat']) : 1);
if(! in_array($params['direction'],['asc','desc'])) {
$params['direction'] = 'desc';
}
if(argc() > 1) {
if(observer_prohibited(true)) {
killme();
}
$channel = channelx_by_nick(argv(1));
if(! $channel) {
killme();
}
logger('public feed request from ' . $_SERVER['REMOTE_ADDR'] . ' for ' . $channel['channel_address']);
echo get_public_feed($channel,$params);
killme();
}
}
}

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Module;
use App;
use DBA;
use Zotlabs\Lib\Activity;
use Zotlabs\Lib\ActivityStreams;
use Zotlabs\Lib\Config;
@@ -47,7 +48,7 @@ class Outbox extends Controller {
$params = [];
$params['begin'] = ((x($_REQUEST, 'date_begin')) ? $_REQUEST['date_begin'] : NULL_DATE);
$params['begin'] = ((x($_REQUEST, 'date_begin')) ? $_REQUEST['date_begin'] : DBA::$dba->get_null_date());
$params['end'] = ((x($_REQUEST, 'date_end')) ? $_REQUEST['date_end'] : '');
$params['type'] = 'json';
$params['pages'] = ((x($_REQUEST, 'pages')) ? intval($_REQUEST['pages']) : 0);

View File

@@ -19,96 +19,128 @@ use Zotlabs\Web\Controller;
class Owa extends Controller {
public function init(): void
{
{
$ret = [ 'success' => false ];
if (!$this->validateAuthorizationHeader()) {
$this->error('Missing or invalid authorization header.');
// try OpenWebAuth over RFC9421
$sigdata = HTTPSig::verify(EMPTY_STR);
if ($sigdata && $sigdata['portable_id'] && $sigdata['header_valid']) {
$portable_id = $sigdata['portable_id'];
if (!check_channelallowed($portable_id)) {
json_return_and_die($ret, 'application/x-zot+json');
}
if (!check_siteallowed($sigdata['signer'])) {
json_return_and_die($ret, 'application/x-zot+json');
}
$hubs = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE hubloc_hash = '%s' ORDER BY hubloc_id DESC",
dbesc($portable_id)
);
if ($hubs) {
logger('OWA RFC9421 success: ' . $hubs[0]['hubloc_id_url'], LOGGER_DATA);
$ret['success'] = true;
$token = random_string(32);
Verify::create('owt', 0, $token, $hubs[0]['hubloc_id_url']);
$result = '';
openssl_public_encrypt($token, $result, $hubs[0]['xchan_pubkey']);
$ret['encrypted_token'] = base64url_encode($result);
}
}
$_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_REMOTE_USER'];
$sigblock = HTTPSig::parse_sigheader($_SERVER['HTTP_AUTHORIZATION']);
if ($sigblock) {
$keyId = $sigblock['keyId'];
$parsed = parse_url($keyId);
if (str_starts_with($parsed['scheme'],'http')) {
unset($parsed['fragment']);
unset($parsed['query']);
$keyId = unparse_url($parsed);
else {
if (!$this->validateAuthorizationHeader()) {
$this->error('Missing or invalid authorization header.');
}
else {
$keyId = str_replace('acct:', '', $keyId);
}
if ($keyId) {
$r = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE (hubloc_addr = '%s' OR hubloc_id_url = '%s' OR xchan_hash = '%s')
AND hubloc_deleted = 0 AND xchan_pubkey != ''
ORDER BY hubloc_id DESC",
dbesc($keyId),
dbesc($keyId),
dbesc($keyId)
);
if (! $r) {
$found = discover_by_webbie($keyId);
logger('found = ' . print_r($found, true));
if ($found) {
$r = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE (hubloc_addr = '%s' OR hubloc_id_url = '%s' OR xchan_hash = '%s') AND hubloc_deleted = 0 AND xchan_pubkey != '' ORDER BY hubloc_id DESC ",
dbesc($keyId),
dbesc($keyId),
dbesc($keyId)
);
}
$_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_REMOTE_USER'];
$sigblock = HTTPSig::parse_sigheader($_SERVER['HTTP_AUTHORIZATION']);
if ($sigblock) {
$keyId = $sigblock['keyId'];
$parsed = parse_url($keyId);
if (str_starts_with($parsed['scheme'],'http')) {
unset($parsed['fragment']);
unset($parsed['query']);
$keyId = unparse_url($parsed);
}
if ($r) {
foreach ($r as $hubloc) {
$verified = HTTPSig::verify(file_get_contents('php://input'), $hubloc['xchan_pubkey']);
if ($verified && $verified['header_signed'] && $verified['header_valid'] && ($verified['content_valid'] || (! $verified['content_signed']))) {
logger('OWA header: ' . print_r($verified,true),LOGGER_DATA);
logger('OWA success: ' . $hubloc['hubloc_id_url'],LOGGER_DATA);
$ret['success'] = true;
$token = random_string(32);
Verify::create('owt',0,$token,$hubloc['hubloc_id_url']);
$result = '';
openssl_public_encrypt($token,$result,$hubloc['xchan_pubkey']);
$ret['encrypted_token'] = base64url_encode($result);
break;
} else {
logger('OWA fail: ' . $hubloc['hubloc_id'] . ' ' . $hubloc['hubloc_id_url']);
else {
$keyId = str_replace('acct:', '', $keyId);
}
if ($keyId) {
$r = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE (hubloc_addr = '%s' OR hubloc_id_url = '%s' OR xchan_hash = '%s')
AND hubloc_deleted = 0 AND xchan_pubkey != ''
ORDER BY hubloc_id DESC",
dbesc($keyId),
dbesc($keyId),
dbesc($keyId)
);
if (! $r) {
$found = discover_by_webbie($keyId);
logger('found = ' . print_r($found, true));
if ($found) {
$r = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE (hubloc_addr = '%s' OR hubloc_id_url = '%s' OR xchan_hash = '%s') AND hubloc_deleted = 0 AND xchan_pubkey != '' ORDER BY hubloc_id DESC ",
dbesc($keyId),
dbesc($keyId),
dbesc($keyId)
);
}
}
if (!$ret['success']) {
if ($r) {
foreach ($r as $hubloc) {
$verified = HTTPSig::verify(file_get_contents('php://input'), $hubloc['xchan_pubkey']);
if ($verified && $verified['header_signed'] && $verified['header_valid'] && ($verified['content_valid'] || (! $verified['content_signed']))) {
logger('OWA header: ' . print_r($verified,true),LOGGER_DATA);
logger('OWA success: ' . $hubloc['hubloc_id_url'],LOGGER_DATA);
$ret['success'] = true;
$token = random_string(32);
Verify::create('owt',0,$token,$hubloc['hubloc_id_url']);
$result = '';
openssl_public_encrypt($token,$result,$hubloc['xchan_pubkey']);
$ret['encrypted_token'] = base64url_encode($result);
break;
} else {
logger('OWA fail: ' . $hubloc['hubloc_id'] . ' ' . $hubloc['hubloc_id_url']);
}
}
// Possible a reinstall?
// In this case we probably already have an old hubloc
// but not the new one yet.
if (!$ret['success']) {
$found = discover_by_webbie($keyId);
// Possible a reinstall?
// In this case we probably already have an old hubloc
// but not the new one yet.
if ($found) {
$r = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE (hubloc_addr = '%s' OR hubloc_id_url = '%s') AND hubloc_deleted = 0 ORDER BY hubloc_id DESC LIMIT 1",
dbesc(str_replace('acct:', '', $keyId)),
dbesc($keyId)
);
$found = discover_by_webbie($keyId);
if ($r) {
$verified = HTTPSig::verify(file_get_contents('php://input'), $r[0]['xchan_pubkey']);
if ($verified && $verified['header_signed'] && $verified['header_valid'] && ($verified['content_valid'] || (! $verified['content_signed']))) {
logger('OWA header: ' . print_r($verified,true), LOGGER_DATA);
logger('OWA success: ' . $r[0]['hubloc_id_url'], LOGGER_DATA);
$ret['success'] = true;
$token = random_string(32);
Verify::create('owt', 0, $token, $r[0]['hubloc_id_url']);
$result = '';
openssl_public_encrypt($token, $result, $r[0]['xchan_pubkey']);
$ret['encrypted_token'] = base64url_encode($result);
} else {
logger('OWA fail: ' . $hubloc['hubloc_id'] . ' ' . $hubloc['hubloc_id_url']);
if ($found) {
$r = q("SELECT * FROM hubloc LEFT JOIN xchan ON hubloc_hash = xchan_hash
WHERE (hubloc_addr = '%s' OR hubloc_id_url = '%s') AND hubloc_deleted = 0 ORDER BY hubloc_id DESC LIMIT 1",
dbesc(str_replace('acct:', '', $keyId)),
dbesc($keyId)
);
if ($r) {
$verified = HTTPSig::verify(file_get_contents('php://input'), $r[0]['xchan_pubkey']);
if ($verified && $verified['header_signed'] && $verified['header_valid'] && ($verified['content_valid'] || (! $verified['content_signed']))) {
logger('OWA header: ' . print_r($verified,true), LOGGER_DATA);
logger('OWA success: ' . $r[0]['hubloc_id_url'], LOGGER_DATA);
$ret['success'] = true;
$token = random_string(32);
Verify::create('owt', 0, $token, $r[0]['hubloc_id_url']);
$result = '';
openssl_public_encrypt($token, $result, $r[0]['xchan_pubkey']);
$ret['encrypted_token'] = base64url_encode($result);
} else {
logger('OWA fail: ' . $hubloc['hubloc_id'] . ' ' . $hubloc['hubloc_id_url']);
}
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
/* Handler for perfstats requests.
*
* SPDX-FileCopyrightText: 2026 The Hubzilla Community
* SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net>
*
* SPDX-License-Identifier: MIT
*/
namespace Zotlabs\Module;
use DBA;
use Zotlabs\Lib\DbStats;
use Zotlabs\Lib\Queue;
use Zotlabs\Lib\QueueWorkerStats;
use Zotlabs\Web\Controller;
/**
* Controller for the `/perfstats` module.
*
* Collects various performance stats for the site, and reponds with the stats
* as a json array.
*/
class Perfstats extends Controller
{
public function init(): void {
//
// We only accept GET requests
//
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_status_exit(400, 'Unsupported method');
}
//
// We only accept json requests
//
if (getBestSupportedMimeType(['application/json']) === null) {
http_status_exit(400, 'No supported format');
}
//
// Only admins should be given access
//
if (!is_site_admin()) {
http_status(401, 'Access denied');
json_return_and_die(['error' => 'access denied']);
}
$data = $this->getStats();
json_return_and_die($data);
}
private function getStats(): array {
$stats = [];
if (function_exists('sys_getloadavg')) {
$stats['loadavg'] = sys_getloadavg();
}
$stats['dbqueries'] = $this->getNumQueries();
$stats['outqueue'] = Queue::count();
$qwstats = new QueueWorkerStats();
$stats['queueworkers'] = $qwstats->active;
$stats['workqsz'] = $qwstats->size;
// Return a timestamp, so that it is possible to infer
// changes of the stats over time. A resolution of
// seconds should be good enough for our purposes.
$stats['ts'] = time();
return $stats;
}
private function getNumQueries(): int {
$stats = DbStats::getStats();
return $stats->getQueries();
}
}

View File

@@ -60,26 +60,16 @@ class Profile extends Controller {
'rel' => 'alternate',
'type' => 'application/atom+xml',
'title' => t('Posts and comments'),
'href' => z_root() . '/feed/' . $which
'href' => z_root() . '/feed/' . $which . '?top=0'
]);
head_add_link([
'rel' => 'alternate',
'type' => 'application/atom+xml',
'title' => t('Only posts'),
'href' => z_root() . '/feed/' . $which . '?f=&top=1'
'href' => z_root() . '/feed/' . $which . '?top=1'
]);
if (!$profile) {
$x = q("select channel_id as profile_uid from channel where channel_address = '%s' limit 1",
dbesc(argv(1))
);
if ($x) {
App::$profile = $x[0];
}
}
profile_load($which, $profile);
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Zotlabs\Module;
use DBA;
use Zotlabs\Lib\Config;
use Zotlabs\Lib\Libsync;
@@ -340,7 +341,7 @@ class Profiles extends \Zotlabs\Web\Controller {
$with = ((x($_POST,'with')) ? escape_tags(trim($_POST['with'])) : '');
if(! strlen($howlong))
$howlong = NULL_DATE;
$howlong = DBA::$dba->get_null_date();
else
$howlong = datetime_convert(date_default_timezone_get(),'UTC',$howlong);
@@ -696,10 +697,9 @@ class Profiles extends \Zotlabs\Web\Controller {
$show_presence = ['show_presence', t('Reveal my online status'), $show_presence_val, '', [t('No'), t('Yes')]];
}
$extra_fields = array();
$q = q("select * from profdef where true");
if($q) {
$extra_fields = array();
foreach($q as $qq) {
$mine = q("select v from profext where k = '%s' and hash = '%s' and channel_id = %d limit 1",
dbesc($qq['field_name']),
@@ -775,7 +775,7 @@ class Profiles extends \Zotlabs\Web\Controller {
'$marital' => marital_selector($r[0]['marital']),
'$marital_min' => marital_selector_min($r[0]['marital']),
'$with' => array('with', t("Who (if applicable)"), $r[0]['partner'], t('Examples: cathy123, Cathy Williams, cathy@example.com')),
'$howlong' => array('howlong', t('Since (date)'), ($r[0]['howlong'] <= NULL_DATE ? '' : datetime_convert('UTC',date_default_timezone_get(),$r[0]['howlong']))),
'$howlong' => array('howlong', t('Since (date)'), ($r[0]['howlong'] <= DBA::$dba->get_null_date() ? '' : datetime_convert('UTC',date_default_timezone_get(),$r[0]['howlong']))),
'$sexual' => sexpref_selector($r[0]['sexual']),
'$sexual_min' => sexpref_selector_min($r[0]['sexual']),
'$about' => array('about', t('Tell us about yourself'), $r[0]['about']),
@@ -833,6 +833,8 @@ class Profiles extends \Zotlabs\Web\Controller {
);
if($r) {
$profiles = '';
$tpl = get_markup_template('profile_entry.tpl');
foreach($r as $rr) {
$profiles .= replace_macros($tpl, array(

View File

@@ -44,6 +44,7 @@ class Profperm extends \Zotlabs\Web\Controller {
if($switchtotext === false)
$switchtotext = 400;
$change = 0;
if((argc() > 2) && intval(argv(1)) && intval(argv(2))) {
$r = q("SELECT abook_id FROM abook WHERE abook_id = %d and abook_channel = %d limit 1",
@@ -74,10 +75,11 @@ class Profperm extends \Zotlabs\Web\Controller {
dbesc($profile['profile_guid'])
);
$ingroup = array();
if($r)
$ingroup = [];
if($r) {
foreach($r as $member)
$ingroup[] = $member['abook_id'];
}
$members = $r;
@@ -104,68 +106,57 @@ class Profperm extends \Zotlabs\Web\Controller {
);
$members = $r;
$ingroup = array();
if(count($r))
$ingroup = [];
if($r) {
foreach($r as $member)
$ingroup[] = $member['abook_id'];
}
$o .= '<h2>' . t('Profile Visibility Editor') . '</h2>';
$o .= '<h3>' . t('Profile') . ' \'' . $profile['profile_name'] . '\'</h3>';
$o .= '<div id="prof-edit-desc">' . t('Click on a contact to add or remove.') . '</div>';
}
$o .= '<div id="prof-update-wrapper">';
if($change)
$o = '';
$o .= '<div id="prof-members-title">';
$o .= '<h3>' . t('Visible To') . '</h3>';
$o .= '</div>';
$o .= '<div id="prof-members">';
$textmode = (($switchtotext && (count($members) > $switchtotext)) ? true : false);
foreach($members as $member) {
if($member['xchan_url']) {
$member['click'] = 'profChangeMember(' . $profile['id'] . ',' . $member['abook_id'] . '); return false;';
$o .= micropro($member,true,'mpprof', $textmode);
}
}
$o .= '</div><div id="prof-members-end"></div>';
$o .= '<hr id="prof-separator" />';
$o .= '<div id="prof-all-contcts-title">';
$o .= '<h3>' . t("All Connections") . '</h3>';
$o .= '</div>';
$o .= '<div id="prof-all-contacts">';
$r = abook_connections(local_channel());
if($r) {
$textmode = (($switchtotext && (count($r) > $switchtotext)) ? true : false);
foreach($r as $member) {
if(! in_array($member['abook_id'],$ingroup)) {
$member['click'] = 'profChangeMember(' . $profile['id'] . ',' . $member['abook_id'] . '); return false;';
$o .= micropro($member,true,'mpprof',$textmode);
}
}
}
$o .= '</div><div id="prof-all-contacts-end"></div>';
$o .= '<h2>' . t('Profile Visibility Editor') . '</h2>';
$o .= '<h3>' . t('Profile') . ' \'' . $profile['profile_name'] . '\'</h3>';
$o .= '<div id="prof-edit-desc">' . t('Click on a contact to add or remove.') . '</div>';
}
// Build template data
$members_tpl = [];
$textmode = (($switchtotext && (count($members) > $switchtotext)) ? true : false);
if($members) {
foreach($members as $member) {
if($member['xchan_url']) {
$member['click'] = 'profChangeMember(' . $profile['id'] . ',' . $member['abook_id'] . '); return false;';
$members_tpl[] = [ 'micro' => micropro($member, true, 'mpprof', $textmode) ];
}
}
}
$all_members_tpl = [];
$r = abook_connections(local_channel());
if($r) {
$textmode = (($switchtotext && (count($r) > $switchtotext)) ? true : false);
foreach($r as $member) {
if(! in_array($member['abook_id'], $ingroup)) {
$member['click'] = 'profChangeMember(' . $profile['id'] . ',' . $member['abook_id'] . '); return false;';
$all_members_tpl[] = [ 'micro' => micropro($member, true, 'mpprof', $textmode) ];
}
}
}
// Use tpl for the inner part
$inner_html = replace_macros(get_markup_template('profile_members.tpl'), [
'$visible_to' => t('Visible To'),
'$all_connections' => t('All Connections'),
'$members' => $members_tpl,
'$all_members' => $all_members_tpl,
]);
if($change) {
echo $o;
echo $inner_html;
killme();
}
$o .= '</div>';
$o .= $inner_html;
return $o;
}
}

View File

@@ -247,11 +247,7 @@ class Pubstream extends \Zotlabs\Web\Controller {
if($r) {
$items = items_by_parent_ids($r, blog_mode: $blog_mode);
// use effective_uid param of xchan_query to help sort out comment permission
// for sys_channel owned items.
xchan_query($items, true, local_channel());
xchan_query($items);
$items = fetch_post_tags($items,true);
$items = conv_sort($items, $ordering);
}

View File

@@ -142,7 +142,15 @@ class Regate extends \Zotlabs\Web\Controller {
if (($flags & ACCOUNT_PENDING ) == ACCOUNT_PENDING) {
$nextpage = 'regate/' . bin2hex($did2) . $didx;
q("COMMIT");
$approve = send_reg_approval_email_from_register($r['reg_id']);
if ($approve['success']) {
q("COMMIT");
} else {
q("ROLLBACK");
$msg_code = 'ZAR1237E';
$msg = t('Account verification notify error');
zar_log($msg_code . ' ' . $msg . ': ' . print_r($approve, true));
}
}
elseif (($flags ^ REGISTER_AGREED) == 0) {

View File

@@ -1,6 +1,7 @@
<?php
namespace Zotlabs\Module;
use DBA;
class Removeaccount extends \Zotlabs\Web\Controller {
@@ -29,7 +30,7 @@ class Removeaccount extends \Zotlabs\Web\Controller {
if(! ($x && $x['account']))
return;
if($account['account_password_changed'] > NULL_DATE) {
if($account['account_password_changed'] > DBA::$dba->get_null_date()) {
$d1 = datetime_convert('UTC','UTC','now - 48 hours');
if($account['account_password_changed'] > $d1) {
notice( t('Account removals are not allowed within 48 hours of changing the account password.') . EOL);

View File

@@ -1,6 +1,7 @@
<?php
namespace Zotlabs\Module;
use DBA;
class Removeme extends \Zotlabs\Web\Controller {
@@ -29,7 +30,7 @@ class Removeme extends \Zotlabs\Web\Controller {
if(! ($x && $x['account']))
return;
if($account['account_password_changed'] > NULL_DATE) {
if($account['account_password_changed'] > DBA::$dba->get_null_date()) {
$d1 = datetime_convert('UTC','UTC','now - 48 hours');
if($account['account_password_changed'] > $d1) {
notice( t('Channel removals are not allowed within 48 hours of changing the account password.') . EOL);

View File

@@ -42,9 +42,7 @@ class Search extends Controller {
$observer = App::get_observer();
$observer_hash = (($observer) ? $observer['xchan_hash'] : '');
$o = '<div class="generic-content-wrapper-styled">' . "\r\n";
$o .= '<h2>' . t('Search') . '</h2>';
$title = t('Search');
if (x(App::$data, 'search'))
$search = trim(App::$data['search']);
@@ -57,7 +55,7 @@ class Search extends Controller {
$search = ((x($_GET, 'tag')) ? trim(escape_tags(rawurldecode($_GET['tag']))) : '');
}
$o .= search($search, 'search-box', '/search', ((local_channel()) ? true : false));
$searchbox = search($search, 'search-box', '/search', ((local_channel()) ? true : false));
if (local_channel() && str_starts_with($search, 'https://') && !$update && !$load) {
@@ -121,8 +119,17 @@ class Search extends Controller {
goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search);
}
if (!$search)
return $o;
if (!$search) {
$tpl = get_markup_template('search.tpl');
return replace_macros($tpl, [
'$title' => $title,
'$searchbox' => $searchbox,
'$livesearch' => '',
'$results_header' => '',
'$conversation' => '',
]);
}
if ($tag) {
$wildtag = str_replace('*', '%', $search);
@@ -143,14 +150,15 @@ class Search extends Controller {
// OR your own posts if you are a logged in member
// No items will be shown if the member has a blocked profile wall.
$livesearch = '';
if ((!$update) && (!$load)) {
// This is ugly, but we can't pass the profile_uid through the session to the ajax updater,
// because browser prefetching might change it on us. We have to deliver it with the page.
$o .= '<div id="live-search"></div>' . "\r\n";
$o .= "<script> var profile_uid = " . ((intval(local_channel())) ? local_channel() : (-1))
$livesearch .= '<div id="live-search"></div>' . "\r\n";
$livesearch .= "<script> var profile_uid = " . ((intval(local_channel())) ? local_channel() : (-1))
. "; var netargs = '?f='; var profile_page = " . App::$pager['page'] . "; </script>\r\n";
App::$page['htmlhead'] = replace_macros(get_markup_template("build_query.tpl"), [
@@ -254,15 +262,21 @@ class Search extends Controller {
}
if ($tag)
$o .= '<h2>' . sprintf(t('Items tagged with: %s'), $search) . '</h2>';
$results_header = sprintf(t('Items tagged with: %s'), $search);
else
$o .= '<h2>' . sprintf(t('Search results for: %s'), $search) . '</h2>';
$results_header = sprintf(t('Search results for: %s'), $search);
$o .= conversation($items, 'search', $update, 'client');
$conversation = conversation($items, 'search', $update, 'client');
$o .= '</div>';
$tpl = get_markup_template('search.tpl');
return $o;
return replace_macros($tpl, [
'$title' => $title,
'$searchbox' => $searchbox,
'$livesearch' => $livesearch,
'$results_header' => $results_header ?? '',
'$conversation' => $conversation,
]);
}

View File

@@ -17,6 +17,9 @@ class Siteinfo extends \Zotlabs\Web\Controller {
$federated = [];
call_hooks('federated_transports',$federated);
$themes = Config::Get('system', 'allowed_themes');
$themes = array_map('trim', explode(',', $themes));
$siteinfo = replace_macros(get_markup_template('siteinfo.tpl'),
[
'$title' => t('About this site'),
@@ -40,6 +43,7 @@ class Siteinfo extends \Zotlabs\Web\Controller {
'$prj_link' => \Zotlabs\Lib\System::get_project_link(),
'$prj_src' => \Zotlabs\Lib\System::get_project_srclink(),
'$addons' => array( t('Active addons'), \App::$plugins ),
'$themes' => array( t('Active themes'), $themes ),
'$blocked_sites' => array( t('Blocked sites'), \Zotlabs\Lib\Config::Get('system', 'blacklisted_sites') )
]
);

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Module;
use App;
use DBA;
use Zotlabs\Lib\Apps;
use Zotlabs\Lib\Config;
use Zotlabs\Web\Controller;
@@ -136,7 +137,7 @@ class Sse extends Controller {
session_reset();
XConfig::Set(self::$ob_hash, 'sse', 'timestamp', NULL_DATE);
XConfig::Set(self::$ob_hash, 'sse', 'timestamp', DBA::$dba->get_null_date());
XConfig::Set(self::$ob_hash, 'sse', 'notifications', []);
if (ob_get_length() > 0) {

View File

@@ -131,7 +131,7 @@ class Sse_bs extends Controller {
$str = '';
$slice = 0;
$mids_all = isset($_SESSION['sse_mids_all']) ? unserialise($_SESSION['sse_mids_all']) : [];
$mids_all = isset($_SESSION['sse_mids_all']) ? json_unserialize($_SESSION['sse_mids_all']) : [];
if (count($mids_all) > 3000) {
$slice = count($mids_all) - 3000;
@@ -177,7 +177,7 @@ class Sse_bs extends Controller {
$mids_all = array_merge($mids_all, $activities_arr);
}
$_SESSION['sse_mids_all'] = serialise(array_unique($mids_all));
$_SESSION['sse_mids_all'] = json_serialize(array_unique($mids_all));
if(! self::$uid) {
return;
@@ -502,7 +502,7 @@ class Sse_bs extends Controller {
$sql_extra2 = " AND CASE WHEN item.verb = '" . ACTIVITY_SHARE . "' THEN item.owner_xchan ELSE item.author_xchan END IN (" . self::$xchans . ") ";
$sql_extra3 = '';
$sse_mids_all = isset($_SESSION['sse_mids_all']) ? unserialise($_SESSION['sse_mids_all']) : [];
$sse_mids_all = isset($_SESSION['sse_mids_all']) ? json_unserialize($_SESSION['sse_mids_all']) : [];
if ($sse_mids_all) {
$sql_extra3 = " AND item.uuid NOT IN (" . protect_sprintf(implode(',', $sse_mids_all)) . ") ";
}

View File

@@ -3,21 +3,21 @@ namespace Zotlabs\Module;
require_once('include/event.php');
use DBA;
class Tasks extends \Zotlabs\Web\Controller {
function init() {
// logger('request: ' . print_r($_REQUEST,true));
$arr = array();
if(argc() > 1 && argv(1) === 'fetch') {
if(argc() > 1 && argv(1) === 'fetch') {
if(argc() > 2 && argv(2) === 'all')
$arr['all'] = 1;
$x = tasks_fetch($arr);
$x['html'] = '';
if($x['tasks']) {
@@ -53,7 +53,7 @@ class Tasks extends \Zotlabs\Web\Controller {
$event = $r[0];
if($event['event_status'] === 'COMPLETED') {
$event['event_status'] = 'IN-PROCESS';
$event['event_status_date'] = NULL_DATE;
$event['event_status_date'] = DBA::$dba->get_null_date();
$event['event_percent'] = 0;
$event['event_sequence'] = $event['event_sequence'] + 1;
$event['edited'] = datetime_convert();

View File

@@ -3,6 +3,7 @@
namespace Zotlabs\Module;
use App;
use DBA;
use Zotlabs\Web\Controller;
use Zotlabs\Lib\Apps;
use Zotlabs\Lib\AccessList;
@@ -84,7 +85,7 @@ class Tokens extends Controller {
if(trim($_POST['expires']))
$expires = datetime_convert(date_default_timezone_get(),'UTC',$_POST['expires']);
else
$expires = NULL_DATE;
$expires = DBA::$dba->get_null_date();
$max_atokens = service_class_fetch($channel['channel_id'],'access_tokens');
if($max_atokens) {
$r = q("select count(atoken_id) as total where atoken_uid = %d",
@@ -290,7 +291,7 @@ class Tokens extends Controller {
'$atoken' => $atoken,
'$name' => array('name', t('Login Name') . ' <span class="required">*</span>', $atoken['atoken_name'] ?? '',''),
'$token'=> array('token', t('Login Password') . ' <span class="required">*</span>', $atoken['atoken_token'] ?? new_token(), ''),
'$expires'=> array('expires', t('Expires (yyyy-mm-dd)'), ((isset($atoken['atoken_expires']) && $atoken['atoken_expires'] > NULL_DATE) ? datetime_convert('UTC',date_default_timezone_get(),$atoken['atoken_expires']) : ''), ''),
'$expires'=> array('expires', t('Expires (yyyy-mm-dd)'), ((isset($atoken['atoken_expires']) && $atoken['atoken_expires'] > DBA::$dba->get_null_date()) ? datetime_convert('UTC',date_default_timezone_get(),$atoken['atoken_expires']) : ''), ''),
'$submit' => t('Submit'),
'$delete' => t('Delete')
));

View File

@@ -1,7 +1,9 @@
<?php
namespace Zotlabs\Module;
use Zotlabs\Lib\Activity;
use Zotlabs\Lib\IConfig;
use Zotlabs\Lib\ObjCache;
class Viewsrc extends \Zotlabs\Web\Controller {
@@ -28,13 +30,16 @@ class Viewsrc extends \Zotlabs\Web\Controller {
$item_normal = item_normal_search();
if(local_channel() && $item_id) {
$r = q("select id, mid, uuid, item_flags, mimetype, item_obscured, body, llink, plink from item where uid in (%d , %d) and id = %d $item_normal limit 1",
$r = q("select * from item where uid in (%d , %d) and id = %d $item_normal limit 1",
intval(local_channel()),
intval($sys['channel_id']),
intval($item_id)
);
if($r) {
xchan_query($r, true);
$r = fetch_post_tags($r);
if(intval($r[0]['item_obscured']))
$dload = true;
@@ -45,15 +50,41 @@ class Viewsrc extends \Zotlabs\Web\Controller {
killme();
}
$cached = true;
$content = escape_tags($r[0]['body']);
$o = (($json) ? json_encode($content) : $content);
$obj = ObjCache::Get($r[0]['mid']);
if (!$obj) {
$obj = IConfig::Get($r[0], 'activitypub', 'rawmsg');
}
if (in_array($r[0]['owner']['xchan_network'], ['diaspora'])) {
$obj = ObjCache::Get($r[0]['mid'], 'diaspora');
if (!$obj) {
$obj = IConfig::Get($r[0], 'diaspora', 'fields');
}
}
if (!$obj) {
$cached = false;
$obj = Activity::encode_activity($r[0]);
}
if ($obj) {
$content = (($cached) ? 'Cached: ' : '') . '<pre>' . escape_tags(json_encode($obj, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '</pre>';
}
else {
$content = escape_tags($r[0]['body']);
}
$o = (($json) ? json_encode($content) : str_replace("\n", '<br>', $content));
}
}
if(is_ajax()) {
echo '<div class="p-1">';
echo '<div>id: ' . $r[0]['id'] . ' | <a href="' . $r[0]['plink'] . '" target="_blank">plink</a> | <a href="' . $r[0]['llink'] . '" target="_blank">llink</a><br>mid: ' . $r[0]['mid'] . '<br>uuid: ' . $r[0]['uuid'] . '</div>';
echo '<div>id: ' . $r[0]['id'] . ' | <a href="' . $r[0]['plink'] . '" target="_blank">plink</a> | <a href="' . $r[0]['llink'] . '" target="_blank">llink</a><br>mid: ' . $r[0]['mid'] . '<br>hashpath: ' . hash('sha256', $r[0]['mid']) . '<br>uuid: ' . $r[0]['uuid'] . '</div>';
echo '<hr>';
echo '<pre class="p-1">' . $o . '</pre>';
echo '</div>';

View File

@@ -115,9 +115,10 @@ abstract class PhotoDriver {
*/
public function __construct($data, $type = '') {
$this->types = $this->supportedTypes();
if(! array_key_exists($type, $this->types)) {
if(!$type || !array_key_exists($type, $this->types)) {
$type = 'image/jpeg';
}
$this->type = $type;
$this->valid = false;
$this->load($data, $type);

View File

@@ -63,10 +63,13 @@ class PhotoGd extends PhotoDriver {
return;
}
/**
* @brief GD imagedestroy() is deprected and noop since PHP version 8.0
*
* @return void
*/
protected function destroy() {
if($this->is_valid()) {
imagedestroy($this->image);
}
return;
}
/**
@@ -95,8 +98,6 @@ class PhotoGd extends PhotoDriver {
imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
if($this->image)
imagedestroy($this->image);
$this->image = $dest;
$this->setDimensions();
@@ -142,8 +143,6 @@ class PhotoGd extends PhotoDriver {
imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $maxx, $maxy, $w, $h);
if($this->image)
imagedestroy($this->image);
$this->image = $dest;
$this->setDimensions();

View File

@@ -72,7 +72,7 @@ class SmartyTemplate implements TemplateEngine {
if ($root != '' && substr($root,-1) != '/' ) {
$root .= '/';
}
foreach ( [ $root . "view/$lang/$file", $root . "view/en/$file", '' ] as $template_file) {
foreach ( [ $root . "view/lang/$lang/$file", $root . "view/lang/en/$file", '' ] as $template_file) {
if (is_file($template_file)) {
break;
}

View File

@@ -261,7 +261,7 @@ class Browser extends DAV\Browser\Plugin {
}
}
$display_path_encoded = Text::rawurlencode_parts($data['display_path']);
$display_path_encoded = Text::rawurlencode_parts($data['display_path'] ?? '');
$href_encoded = Text::rawurlencode_parts($href);
// put the array for this file together
@@ -272,7 +272,7 @@ class Browser extends DAV\Browser\Plugin {
$ft['rel_path'] = (($data) ? '/cloud/' . $nick .'/' . $display_path_encoded : $href_encoded);
$ft['full_path'] = z_root() . (($data) ? '/cloud/' . $nick .'/' . $display_path_encoded : $href_encoded);
$ft['name'] = $name;
$ft['type'] = $type;
$ft['type'] = $data['filetype'];
$ft['size'] = $size;
$ft['collection'] = (($type === 'Collection') ? true : false);
$ft['size_formatted'] = userReadableSize($size);

View File

@@ -519,8 +519,8 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo
* @return void
*/
function getDir() {
logger('GetDir: ' . $this->ext_path, LOGGER_DEBUG);
$this->auth->log();
$modulename = \App::$module;
@@ -538,7 +538,6 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo
$file = trim($file, '/');
$path_arr = explode('/', $file);
if (! $path_arr)
return;
@@ -558,9 +557,9 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo
$this->auth->owner_id = $channel_id;
$this->auth->owner_nick = $channel_name;
$path = '/' . $channel_name;
$folder = '';
$os_path = '';
$not_found = '';
for ($x = 1; $x < count($path_arr); $x++) {
$r = q("select id, hash, filename, flags, is_dir from attach where folder = '%s' and filename = '%s' and uid = %d and is_dir != 0",
@@ -568,15 +567,31 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo
dbesc($path_arr[$x]),
intval($channel_id)
);
if ($r && intval($r[0]['is_dir'])) {
$folder = $r[0]['hash'];
if (strlen($os_path))
$os_path .= '/';
$os_path .= $folder;
$path = $path . '/' . $r[0]['filename'];
if (strlen($os_path)) {
$os_path .= '/';
}
$os_path .= $folder;
}
else {
// if we got a bogus path collect the
if (strlen($not_found)) {
$not_found .= ',';
}
$not_found .= $path_arr[$x];
}
}
if ($not_found) {
throw new DAV\Exception\NotFound("Path $file does not exist. One or more directories could not be found: $not_found");
}
$this->folder_hash = $folder;
$this->os_path = $os_path;
}

View File

@@ -54,9 +54,6 @@ class Epubthumb {
imagecopyresampled($dest, $image, 0, 0, 0, 0, $width, $height, $srcwidth, $srcheight);
imagejpeg($dest, "{$file}.thumb");
imagedestroy($image);
imagedestroy($dest);
}
}

View File

@@ -36,7 +36,6 @@ class Mp3audio {
imagealphablending($dest, false);
imagesavealpha($dest, true);
imagecopyresampled($dest, $image, 0, 0, 0, 0, $width, $height, $srcwidth, $srcheight);
imagedestroy($image);
imagejpeg($dest,dbunescbin($attach['content']) . '.thumb');
}
}

30
Zotlabs/Update/_1265.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
namespace Zotlabs\Update;
class _1265 {
function run() {
dbq("START TRANSACTION");
if(ACTIVE_DBTYPE == DBTYPE_POSTGRES) {
$r = dbq("ALTER TABLE attach ALTER COLUMN filetype TYPE VARCHAR(128), ALTER COLUMN filetype SET NOT NULL, ALTER COLUMN filetype SET DEFAULT ''");
}
if(ACTIVE_DBTYPE == DBTYPE_MYSQL) {
$r = dbq("ALTER TABLE attach CHANGE filetype filetype CHAR(128) NOT NULL DEFAULT ''");
}
if($r) {
dbq("COMMIT");
return UPDATE_SUCCESS;
}
q("ROLLBACK");
return UPDATE_FAILED;
}
}

View File

@@ -9,6 +9,8 @@ namespace Zotlabs\Widget;
use App;
use Zotlabs\Lib\Apps;
use Zotlabs\Lib\Queue;
use Zotlabs\Lib\QueueWorkerStats;
class Channel_activities {
@@ -25,10 +27,9 @@ class Channel_activities {
self::$uid = local_channel();
self::$channel = App::get_channel();
$o = '<div id="channel-activities" class="d-none overflow-hidden">';
$o .= '<h2 class="mb-4">' . t('Welcome') . ' ' . self::$channel['channel_name'] . '!</h2>';
//$o .= 'Last login date: ' . get_pconfig(self::$uid, 'system', 'stored_login_date') . ' from ' . get_pconfig(self::$uid, 'system', 'stored_login_addr');
if (is_site_admin()) {
self::get_system_status();
}
self::get_photos_activity();
self::get_files_activity();
self::get_webpages_activity();
@@ -42,28 +43,36 @@ class Channel_activities {
call_hooks('channel_activities_widget', $hookdata);
if (!$hookdata['activities']) {
$o .= '<h3>' . t('No recent activities') . '</h3>';
$o .= '</div>';
return $o;
$activity_html = '';
if ($hookdata['activities']) {
$keys = array_column($hookdata['activities'], 'date');
array_multisort($keys, SORT_DESC, $hookdata['activities']);
foreach ($hookdata['activities'] as $a) {
$activity_html .= replace_macros(
get_markup_template($a['tpl']),
[
'$url' => $a['url'] ?? null,
'$icon' => $a['icon'],
'$label' => $a['label'],
'$items' => $a['items'],
'$labels' => $a['labels'] ?? [],
]
);
}
}
$keys = array_column($hookdata['activities'], 'date');
$tpl = get_markup_template('channel_activities_widget.tpl');
array_multisort($keys, SORT_DESC, $hookdata['activities']);
return replace_macros($tpl, [
'$welcome' => t('Welcome'),
'$channel_name' => self::$channel['channel_name'],
'$no_activities' => t('No recent activities'),
'$activities' => $hookdata['activities'],
'$activity_html' => $activity_html
]);
foreach($hookdata['activities'] as $a) {
$o .= replace_macros(get_markup_template($a['tpl']), [
'$url' => $a['url'],
'$icon' => $a['icon'],
'$label' => $a['label'],
'$items' => $a['items']
]);
}
$o .= '</div>';
return $o;
}
private static function get_photos_activity() {
@@ -220,11 +229,17 @@ class Channel_activities {
$footer .= intval($notices[0]['total']) . ' ' . tt('notice', 'notices', intval($notices[0]['total']), 'noun');
}
$tpl = get_markup_template('manage_channel_item.tpl');
$i[] = [
'url' => z_root() . '/manage/' . $rr['channel_id'],
'title' => '',
'summary' => '<div class="text-truncate lh-sm"><img src="' . $rr['xchan_photo_s'] . '" class="menu-img-2">' . '<strong>' . $rr['channel_name'] . '</strong><br><small class="text-body-secondary">' . $rr['xchan_addr'] . '</small></div>',
'footer' => $footer
'url' => z_root() . '/manage/' . $rr['channel_id'],
'title' => '',
'summary' => replace_macros($tpl, [
'$photo' => $rr['xchan_photo_s'],
'$name' => $rr['channel_name'],
'$addr' => $rr['xchan_addr'],
]),
'footer' => $footer
];
$channels_activity++;
@@ -246,5 +261,29 @@ class Channel_activities {
}
private static function get_system_status(): void {
self::$activities['status'] = [
'label' => t('System status'),
'icon' => 'gpu-card',
'date' => datetime_convert(),
'items' => [
'loadavg' => '0 / 0 / 0',
'dbqueries' => 0,
'outqueue' => 0,
'queueworkers' => 0,
'workqsz' => 0,
'ts' => time(),
],
'tpl' => 'system_status_widget.tpl',
'labels' => [
'loadavg' => t('Load average'),
'dbqueries' => t('DB queries/sec'),
'outqueue' => t('Output queue'),
'queueworkers' => t('Queue workers'),
'workqsz' => t('Work queue size'),
],
];
}
}

View File

@@ -8,15 +8,18 @@
namespace Zotlabs\Widget;
use App;
class Fullprofile {
function widget($arr) {
if(!(isset(\App::$profile['profile_uid']) && \App::$profile['profile_uid']))
if (empty(App::$profile['profile_uid'])) {
return;
}
$block = observer_prohibited();
return profile_sidebar(\App::$profile, $block, true, true);
return profile_sidebar(App::$profile, $block, true, true);
}
}

View File

@@ -312,10 +312,10 @@ class Messages {
$author_sql = " AND notify.url = '" . protect_sprintf(dbesc($author_url)) . "' ";
}
$notices = q("SELECT notify.*, xchan.xchan_addr FROM notify
LEFT JOIN xchan ON notify.url = xchan.xchan_url
WHERE uid = %d $author_sql
GROUP BY notify.id ORDER BY created DESC LIMIT $limit OFFSET $offset",
$notices = q("SELECT notify.*, max(hubloc.hubloc_addr) as hubloc_addr FROM notify
LEFT JOIN hubloc ON notify.url = hubloc.hubloc_id_url
WHERE notify.uid = %d $author_sql AND hubloc.hubloc_primary = 1
GROUP BY notify.id ORDER BY notify.created DESC LIMIT $limit OFFSET $offset",
intval(local_channel())
);
@@ -331,7 +331,7 @@ class Messages {
}
$entries[$i]['author_name'] = $notice['xname'];
$entries[$i]['author_addr'] = $notice['xchan_addr'];
$entries[$i]['author_addr'] = $notice['hubloc_addr'];
$entries[$i]['author_img'] = $notice['photo'];// $item['author']['xchan_photo_s'];
$entries[$i]['info'] = '';
$entries[$i]['created'] = datetime_convert('UTC', date_default_timezone_get(), $notice['created']);

View File

@@ -1,6 +1,7 @@
<?php
namespace Zotlabs\Widget;
use DBA;
use Zotlabs\Lib\Config;
/**
@@ -60,7 +61,7 @@ class Pinned {
$profile_link = chanlink_hash($item['author_xchan']);
$profile_name = $author['xchan_name'];
$commentable = ($item['item_nocomment'] == 0 && $item['comments_closed'] == NULL_DATE ? true : false);
$commentable = ($item['item_nocomment'] == 0 && $item['comments_closed'] == DBA::$dba->get_null_date() ? true : false);
$location = format_location($item);
$isevent = false;
@@ -131,7 +132,7 @@ class Pinned {
'isotime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'c'),
'localtime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'r'),
'editedtime' => (($item['edited'] != $item['created']) ? sprintf( t('last edited: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['edited'], 'r') ) : ''),
'expiretime' => ($item['expires'] > NULL_DATE ? sprintf( t('Expires: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['expires'], 'r') ) : ''),
'expiretime' => ($item['expires'] > DBA::$dba->get_null_date() ? sprintf( t('Expires: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['expires'], 'r') ) : ''),
'verified' => $verified,
'forged' => $forged,
'location' => $location,

View File

@@ -13,7 +13,7 @@ use App;
class Profile {
function widget($args) {
if(!isset(App::$profile['profile_uid'])) {
if (empty(App::$profile['profile_uid'])) {
return;
}

View File

@@ -70,10 +70,10 @@ require_once('include/security.php');
define('PLATFORM_NAME', 'hubzilla');
define('STD_VERSION', '10.6.1');
define('STD_VERSION', '11.2.1');
define('ZOT_REVISION', '6.0');
define('DB_UPDATE_VERSION', 1264);
define('DB_UPDATE_VERSION', 1265);
define('PROJECT_BASE', __DIR__);
@@ -1896,7 +1896,7 @@ function notice($s) {
$x = null;
$t = get_xconfig($hash, 'sse', 'timestamp', NULL_DATE);
$t = get_xconfig($hash, 'sse', 'timestamp', DBA::$dba->get_null_date());
if (datetime_convert('UTC', 'UTC', $t) < datetime_convert('UTC', 'UTC', '- 30 seconds')) {
set_xconfig($hash, 'sse', 'notifications', []);
@@ -1945,7 +1945,7 @@ function info($s) {
$x = null;
$t = get_xconfig($hash, 'sse', 'timestamp', NULL_DATE);
$t = get_xconfig($hash, 'sse', 'timestamp', DBA::$dba->get_null_date());
if (datetime_convert('UTC', 'UTC', $t) < datetime_convert('UTC', 'UTC', '- 30 seconds')) {
set_xconfig($hash, 'sse', 'notifications', []);

View File

@@ -23,7 +23,7 @@
"source": "https://framagit.org/hubzilla/core/"
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"ext-curl": "*",
"ext-iconv": "*",
"ext-intl": "*",
@@ -53,11 +53,12 @@
"chillerlan/php-qrcode": "^5.0.3",
"spomky-labs/otphp": "^11.1",
"patrickschur/language-detection": "^5.3",
"stephenhill/base58": "^1.1",
"stephenhill/base58": "^2.1",
"scssphp/scssphp": "^2.0.1",
"twbs/bootstrap-icons": "^1.11",
"macgirvin/http-message-signer": "^0.2.6",
"root23/php-json-canonicalization": "^1.0"
"root23/php-json-canonicalization": "^1.0",
"guzzlehttp/psr7": "^2.8"
},
"require-dev": {
"ext-yaml": "*",

527
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -186,7 +186,7 @@ function create_account_from_register($arr) {
if ( ! $register ) return $result;
// account
$expires = NULL_DATE;
$expires = DBA::$dba->get_null_date();
$default_service_class = Config::Get('system','default_service_class');
if($default_service_class === false)
@@ -264,9 +264,10 @@ function create_account_from_register($arr) {
$result['account']['parent'] = $result['account']['account_id'];
}
$result['success'] = true;
call_hooks('register_account',$result);
if ( send_reg_confirmation_email_from_register($arr['reg_id']) ) {
$result['success'] = true;
call_hooks('register_account',$result);
}
return $result;
}
@@ -311,11 +312,59 @@ function verify_email_address(string $email): bool {
pop_lang();
if(! $res)
logger("send_reg_approval_email: failed sending email to: {$email}");
logger("send_reg_verification_email: failed sending email to: {$email}");
return $res;
}
/**
* Send email to user confirming approved registration.
*
* @param int $reg_id The reg_id of the user (from register table)
*
* @return bool `true` if the email was sucessfully sent, otherwise `false`.
*/
function send_reg_confirmation_email_from_register(int $reg_id): bool {
$register = q("SELECT reg_email, reg_lang FROM register WHERE reg_id = %d", $reg_id);
if (empty($register)) {
logger('send_reg_confirmation_email_from_register: could not find email address for for reg_id ' . $reg_id);
return false;
} else {
logger('send_reg_confirmation_email_from_register: sending confirmation email to ' . $register[0]['reg_email']);
}
if(strlen($register['reg_lang'])) {
push_lang($register['reg_lang']);
} else {
push_lang('en');
}
$email_msg = replace_macros(get_intltext_template('register_approved_eml.tpl'), array(
'$sitename' => Config::Get('system','sitename'),
'$siteurl' => z_root(),
'$email' => $register[0]['reg_email'],
));
$res = z_mail(
[
'toEmail' => $register[0]['reg_email'],
'messageSubject' => sprintf( t('Registration approved at %s'), Config::Get('system','sitename')),
'textVersion' => $email_msg,
]
);
pop_lang();
if ($res) {
return true;
} else {
logger('send_reg_confirmation_email_from_register: failed to send confirmation email to ' . $register[0]['reg_email']);
return false;
}
}
function send_reg_approval_email($arr) {
@@ -392,6 +441,88 @@ function send_reg_approval_email($arr) {
return($delivered ? true : false);
}
/**
* send_reg_approval_email_from_register
* @author ltning
* @since 2026-01-25
*
* Account approval after verification based on table register.
* This function sends email to admin(s).
*
*/
function send_reg_approval_email_from_register(int $reg_id): array {
$result = array('success' => false, 'message' => 'rid:' . $reg_id);
$now = datetime_convert();
$register = q("SELECT * FROM register WHERE reg_id = %d",
intval($reg_id)
);
if (empty($register)) {
logger('send_reg_approval_email: could not find data for reg_id ' . $reg_id);
return $result;
}
$r = q("select * from account where (account_roles & %d) >= 4096",
intval(ACCOUNT_ROLE_ADMIN)
);
$admins = array();
foreach($r as $rr) {
if (strlen($rr['account_email'])) {
$admins[] = array('email' => $rr['account_email'], 'lang' => $rr['account_lang']);
}
}
if (empty($admins)) {
logger('send_reg_approval_email: could not find any admins to notify for reg_id ' . $reg_id);
return $result;
}
$delivered = 0;
foreach($admins as $admin) {
if (strlen($admin['lang'])) {
push_lang($admin['lang']);
} else {
push_lang('en');
}
$email_msg = replace_macros(get_intltext_template('register_verify_eml_no_links.tpl'), array(
'$sitename' => Config::Get('system','sitename'),
'$siteurl' => z_root(),
'$email' => $register[0]['reg_email'],
'$details' => $register[0]['reg_atip']
));
$res = z_mail(
[
'toEmail' => $admin['email'],
'messageSubject' => sprintf( t('Registration request at %s'), Config::Get('system','sitename')),
'textVersion' => $email_msg,
]
);
if ($res) {
$delivered ++;
} else {
logger('send_reg_approval_email: failed to ' . $admin['email'] . 'reg_email: ' . $register[0]['reg_email']);
}
pop_lang();
}
$result['delivered'] = $delivered;
if($delivered > 0) {
$result['success'] = true;
}
return $result;
}
function send_register_success_email($email,$password) {
$email_msg = replace_macros(get_intltext_template('register_open_eml.tpl'), array(
@@ -770,7 +901,7 @@ function downgrade_accounts() {
and account_expires > '%s'
and account_expires < %s ",
intval(ACCOUNT_EXPIRED),
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
db_getfunc('UTC_TIMESTAMP')
);
@@ -784,7 +915,7 @@ function downgrade_accounts() {
q("UPDATE account set account_service_class = '%s', account_expires = '%s'
where account_id = %d",
dbesc($basic),
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
intval($rr['account_id'])
);
$ret = array('account' => $rr);
@@ -1083,7 +1214,7 @@ function get_pending_accounts($get_all = false) {
function remove_expired_registrations() {
q("DELETE FROM register WHERE (reg_expires < '%s' OR reg_expires = '%s') AND (reg_flags & %d) > 0",
dbesc(datetime_convert()),
dbesc(NULL_DATE),
dbesc(DBA::$dba->get_null_date()),
dbesc(ACCOUNT_UNVERIFIED)
);
}

View File

@@ -105,7 +105,7 @@
$records = 10;
}
if(! $_REQUEST['since'])
$start = NULL_DATE;
$start = DBA::$dba->get_null_date();
else {
$start = datetime_convert(date_default_timezone_get(),'UTC', $_REQUEST['since']);
}
@@ -210,7 +210,7 @@
$start = ((array_key_exists('start',$_REQUEST)) ? intval($_REQUEST['start']) : 0);
$records = ((array_key_exists('records',$_REQUEST)) ? intval($_REQUEST['records']) : 0);
$since = ((array_key_exists('since',$_REQUEST)) ? datetime_convert(date_default_timezone_get(),'UTC',$_REQUEST['since']) : NULL_DATE);
$since = ((array_key_exists('since',$_REQUEST)) ? datetime_convert(date_default_timezone_get(),'UTC',$_REQUEST['since']) : DBA::$dba->get_null_date());
$until = ((array_key_exists('until',$_REQUEST)) ? datetime_convert(date_default_timezone_get(),'UTC',$_REQUEST['until']) : datetime_convert());
$x = attach_list_files(api_user(),get_observer_hash(),$hash,$filename,$filetype,'created asc',$start,$records, $since, $until);
@@ -461,11 +461,9 @@
function api_red_xchan($type) {
if(api_user() === false)
return false;
logger('api_xchan');
if($_SERVER['REQUEST_METHOD'] === 'POST') {
$r = xchan_store($_REQUEST);
}
$r = xchan_fetch($_REQUEST);
json_return_and_die($r);
};

View File

@@ -101,8 +101,10 @@ function z_mime_content_type($filename) {
// ms office
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'rtf' => 'application/rtf',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
// open office
@@ -224,7 +226,7 @@ function attach_list_files($channel_id, $observer, $hash = '', $filename = '', $
$limit = " LIMIT " . intval($entries) . " OFFSET " . intval($start) . " ";
if(! $since)
$since = NULL_DATE;
$since = DBA::$dba->get_null_date();
if(! $until)
$until = datetime_convert();
@@ -658,6 +660,7 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) {
dbesc($arr['hash']),
intval($channel_id)
);
if(! $x) {
logger('update file source not found');
$ret['message'] = t('Cannot locate file to revise/update');
@@ -727,14 +730,14 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) {
$direct = null;
if($pathname) {
$x = attach_mkdirp($channel, $observer_hash, $darr);
$folder_hash = (($x['success']) ? $x['data']['hash'] : '');
$direct = (($x['success']) ? $x['data'] : null);
$new_dir = attach_mkdirp($channel, $observer_hash, $darr);
$folder_hash = (($new_dir['success']) ? $new_dir['data']['hash'] : '');
$direct = (($new_dir['success']) ? $new_dir['data'] : null);
if((! $str_contact_allow) && (! $str_group_allow) && (! $str_contact_deny) && (! $str_group_deny)) {
$str_contact_allow = $x['data']['allow_cid'];
$str_group_allow = $x['data']['allow_gid'];
$str_contact_deny = $x['data']['deny_cid'];
$str_group_deny = $x['data']['deny_gid'];
$str_contact_allow = $new_dir['data']['allow_cid'];
$str_group_allow = $new_dir['data']['allow_gid'];
$str_contact_deny = $new_dir['data']['deny_cid'];
$str_group_deny = $new_dir['data']['deny_gid'];
}
}
else {
@@ -888,6 +891,18 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) {
else
$edited = $created;
// Until here we either used the provided mime type or set mimetype by extension.
// Both variants are inherently unsafe hence try to find and set the real mimetype before storage.
if (class_exists('finfo') && is_file($os_basepath . $os_relpath)) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimetype = $finfo->file($os_basepath . $os_relpath);
if ($mimetype === false) {
$mimetype = 'application/octet-stream';
}
}
if($options === 'replace') {
$r = q("update attach set filename = '%s', filetype = '%s', folder = '%s', filesize = %d, os_storage = %d, is_photo = %d, content = '%s', edited = '%s', os_path = '%s', display_path = '%s' where id = %d and uid = %d",
dbesc($filename),
@@ -930,12 +945,13 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) {
);
}
elseif($options === 'update') {
$r = q("update attach set filename = '%s', filetype = '%s', folder = '%s', edited = '%s', os_storage = %d, is_photo = %d, os_path = '%s',
$r = q("update attach set filename = '%s', filetype = '%s', filesize = %d, folder = '%s', edited = '%s', os_storage = %d, is_photo = %d, os_path = '%s',
display_path = '%s', allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' where id = %d and uid = %d",
dbesc((array_key_exists('filename',$arr)) ? $arr['filename'] : $x[0]['filename']),
dbesc((array_key_exists('filetype',$arr)) ? $arr['filetype'] : $x[0]['filetype']),
dbesc((array_key_exists('filesize',$arr)) ? $arr['filesize'] : $x[0]['filesize']),
dbesc(($folder_hash) ? $folder_hash : $x[0]['folder']),
dbesc($created),
dbesc($edited),
dbesc((array_key_exists('os_storage',$arr)) ? $arr['os_storage'] : $x[0]['os_storage']),
dbesc((array_key_exists('is_photo',$arr)) ? $arr['is_photo'] : $x[0]['is_photo']),
dbesc((array_key_exists('os_path',$arr)) ? $arr['os_path'] : $x[0]['os_path']),

View File

@@ -278,11 +278,6 @@ function bb_parse_crypt($match) {
$onclick = 'onclick="sodium_decrypt(\'' . $payload . '\',\'#' . $x . '\');"';
if (in_array($algorithm, ['AES-128-CCM', 'rot13', 'triple-rot13'])) {
// backwards compatibility
$onclick = 'onclick="hz_decrypt(\'' . $algorithm . '\',\'' . $hint . '\',\'' . $payload . '\',\'#' . $x . '\');"';
}
$label = t('Encrypted content');
$text = '<div id="' . $x . '" class="encrypted-content"><img class="cursor-pointer" src="' . z_root() . '/images/lock_icon.svg" ' . $onclick . ' alt="' . $label . '" title="' . $label . '" /></div>';
@@ -557,13 +552,6 @@ function bb_ShareAttributes($match) {
$rnd = mt_rand();
$reldate = '<span class="autotime" title="' . datetime_convert('UTC', date_default_timezone_get(), $posted, 'c') . '" >' . datetime_convert('UTC', date_default_timezone_get(), $posted, 'r') . '</span>';
$headline = '<div id="shared_container_' . $rnd . '" class="shared_container"> <div id="shared_header_' . $rnd . '" class="shared_header">';
if ($avatar != "")
$headline .= '<a href="' . (($auth) ? zid($profile) : $profile) . '" ><img src="' . $avatar . '" alt="' . $author . '" height="32" width="32" loading="lazy" /></a>';
if(strpos($link,'/cards/'))
$type = t('card');
elseif(strpos($link,'/articles/'))
@@ -571,19 +559,23 @@ function bb_ShareAttributes($match) {
else
$type = t('post');
// Bob Smith wrote the following post 2 hours ago
$author_url = (($auth) ? zid($profile) : $profile);
$post_url = (($auth) ? zid($link) : $link);
$reldate = datetime_convert('UTC', date_default_timezone_get(), $posted, 'r');
$reldate_iso = datetime_convert('UTC', date_default_timezone_get(), $posted, 'c');
$fmt = sprintf( t('%1$s wrote the following %2$s %3$s'),
'<a href="' . (($auth) ? zid($profile) : $profile) . '" ><bdi>' . $author . '</bdi></a>',
'<a href="' . (($auth) ? zid($link) : $link) . '" >' . $type . '</a>',
$reldate
);
$headline .= '<span>' . $fmt . '</span></div>';
$text = $headline . '<div id="reshared-content-' . $rnd . '" class="reshared-content">' . trim($match[2]) . '</div></div>';
return $text;
return replace_macros(get_markup_template('bb_share.tpl'), [
'$rnd' => $rnd,
'$avatar' => $avatar,
'$text' => t(' wrote the following '),
'$author' => $author,
'$author_url' => $author_url,
'$post_url' => $post_url,
'$type' => $type,
'$reldate' => $reldate,
'$reldate_iso' => $reldate_iso,
'$content' => trim($match[2]),
]);
}
function bb_location($match) {

View File

@@ -931,7 +931,7 @@ function identity_basic_export($channel_id, $sections = null, $zap_compat = fals
continue;
}
if (preg_match('|^a:[0-9]+:{.*}$|s', $abc['v'])) {
$abc['v'] = serialise(unserialize($abc['v']));
$abc['v'] = json_serialize(unserialize($abc['v']));
}
$newconfig[] = $abc;
}
@@ -988,7 +988,7 @@ function identity_basic_export($channel_id, $sections = null, $zap_compat = fals
if ($zap_compat) {
for($x = 0; $x < count($r); $x ++) {
if (preg_match('|^a:[0-9]+:{.*}$|s', $r[$x]['v'])) {
$r[$x]['v'] = serialise(unserialize($r[$x]['v']));
$r[$x]['v'] = json_serialize(unserialize($r[$x]['v']));
}
}
}
@@ -1302,7 +1302,7 @@ function channel_export_items_page($channel_id, $start, $finish, $page = 0, $lim
}
if(! $start)
$start = NULL_DATE;
$start = DBA::$dba->get_null_date();
else
$start = datetime_convert('UTC', 'UTC', $start);
@@ -1823,7 +1823,7 @@ function advanced_profile() {
if(App::$profile['partner'])
$profile['marital']['partner'] = zidify_links(bbcode(App::$profile['partner']));
if(strlen(App::$profile['howlong']) && App::$profile['howlong'] > NULL_DATE) {
if(strlen(App::$profile['howlong']) && App::$profile['howlong'] > DBA::$dba->get_null_date()) {
$profile['howlong'] = relative_date(App::$profile['howlong'], t('for %1$d %2$s'));
}
@@ -2649,10 +2649,10 @@ function channel_store_lowlevel($arr) {
'channel_eprvkey' => ((array_key_exists('channel_eprvkey',$arr)) ? $arr['channel_eprvkey'] : ''),
'channel_notifyflags' => ((array_key_exists('channel_notifyflags',$arr)) ? $arr['channel_notifyflags'] : '65535'),
'channel_pageflags' => ((array_key_exists('channel_pageflags',$arr)) ? $arr['channel_pageflags'] : '0'),
'channel_dirdate' => ((array_key_exists('channel_dirdate',$arr)) ? $arr['channel_dirdate'] : NULL_DATE),
'channel_lastpost' => ((array_key_exists('channel_lastpost',$arr)) ? $arr['channel_lastpost'] : NULL_DATE),
'channel_deleted' => ((array_key_exists('channel_deleted',$arr)) ? $arr['channel_deleted'] : NULL_DATE),
'channel_active' => ((array_key_exists('channel_active',$arr)) ? $arr['channel_active'] : NULL_DATE),
'channel_dirdate' => ((array_key_exists('channel_dirdate',$arr)) ? $arr['channel_dirdate'] : DBA::$dba->get_null_date()),
'channel_lastpost' => ((array_key_exists('channel_lastpost',$arr)) ? $arr['channel_lastpost'] : DBA::$dba->get_null_date()),
'channel_deleted' => ((array_key_exists('channel_deleted',$arr)) ? $arr['channel_deleted'] : DBA::$dba->get_null_date()),
'channel_active' => ((array_key_exists('channel_active',$arr)) ? $arr['channel_active'] : DBA::$dba->get_null_date()),
'channel_max_anon_mail' => ((array_key_exists('channel_max_anon_mail',$arr)) ? $arr['channel_max_anon_mail'] : '10'),
'channel_max_friend_req' => ((array_key_exists('channel_max_friend_req',$arr)) ? $arr['channel_max_friend_req'] : '10'),
'channel_expire_days' => ((array_key_exists('channel_expire_days',$arr)) ? $arr['channel_expire_days'] : '0'),
@@ -2695,7 +2695,7 @@ function profile_store_lowlevel($arr) {
'gender' => ((array_key_exists('gender',$arr)) ? $arr['gender'] : ''),
'marital' => ((array_key_exists('marital',$arr)) ? $arr['marital'] : ''),
'partner' => ((array_key_exists('partner',$arr)) ? $arr['partner'] : ''),
'howlong' => ((array_key_exists('howlong',$arr)) ? $arr['howlong'] : NULL_DATE),
'howlong' => ((array_key_exists('howlong',$arr)) ? $arr['howlong'] : DBA::$dba->get_null_date()),
'sexual' => ((array_key_exists('sexual',$arr)) ? $arr['sexual'] : ''),
'politic' => ((array_key_exists('politic',$arr)) ? $arr['politic'] : ''),
'religion' => ((array_key_exists('religion',$arr)) ? $arr['religion'] : ''),

View File

@@ -12,10 +12,10 @@ function abook_store_lowlevel($arr) {
'abook_my_perms' => ((array_key_exists('abook_my_perms',$arr)) ? $arr['abook_my_perms'] : 0),
'abook_their_perms' => ((array_key_exists('abook_their_perms',$arr)) ? $arr['abook_their_perms'] : 0),
'abook_closeness' => ((array_key_exists('abook_closeness',$arr)) ? $arr['abook_closeness'] : 99),
'abook_created' => ((array_key_exists('abook_created',$arr)) ? $arr['abook_created'] : NULL_DATE),
'abook_updated' => ((array_key_exists('abook_updated',$arr)) ? $arr['abook_updated'] : NULL_DATE),
'abook_connected' => ((array_key_exists('abook_connected',$arr)) ? $arr['abook_connected'] : NULL_DATE),
'abook_dob' => ((array_key_exists('abook_dob',$arr)) ? $arr['abook_dob'] : NULL_DATE),
'abook_created' => ((array_key_exists('abook_created',$arr)) ? $arr['abook_created'] : DBA::$dba->get_null_date()),
'abook_updated' => ((array_key_exists('abook_updated',$arr)) ? $arr['abook_updated'] : DBA::$dba->get_null_date()),
'abook_connected' => ((array_key_exists('abook_connected',$arr)) ? $arr['abook_connected'] : DBA::$dba->get_null_date()),
'abook_dob' => ((array_key_exists('abook_dob',$arr)) ? $arr['abook_dob'] : DBA::$dba->get_null_date()),
'abook_flags' => ((array_key_exists('abook_flags',$arr)) ? $arr['abook_flags'] : 0),
'abook_blocked' => ((array_key_exists('abook_blocked',$arr)) ? $arr['abook_blocked'] : 0),
'abook_ignored' => ((array_key_exists('abook_ignored',$arr)) ? $arr['abook_ignored'] : 0),

View File

@@ -630,7 +630,7 @@ function conversation($items, $mode, $update, $page_mode = 'traditional', $prepa
'isotime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'c'),
'localtime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'r'),
'editedtime' => (($item['edited'] != $item['created']) ? sprintf( t('last edited: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['edited'], 'r')) : ''),
'expiretime' => (($item['expires'] > NULL_DATE) ? sprintf( t('Expires: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['expires'], 'r')):''),
'expiretime' => (($item['expires'] > DBA::$dba->get_null_date()) ? sprintf( t('Expires: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['expires'], 'r')):''),
'location' => $location,
'divider' => false,
'indent' => '',
@@ -782,7 +782,7 @@ function thread_action_menu($item,$mode = '') {
$menu = [];
if((local_channel()) && local_channel() == $item['uid']) {
if(local_channel() || (local_channel() && App::$module === 'pubstream')) {
$menu[] = [
'menu' => 'view_source',
'title' => t('View Source'),
@@ -791,7 +791,7 @@ function thread_action_menu($item,$mode = '') {
'href' => '#'
];
if(!is_unthreaded($mode)) {
if(!is_unthreaded($mode) && local_channel() == $item['uid']) {
if($item['parent'] == $item['id'] && (get_observer_hash() != $item['author_xchan'])) {
$menu[] = [
'menu' => 'follow_thread',

View File

@@ -101,19 +101,13 @@ abstract class dba_driver {
public $error = false;
/**
* @brief Connect to the database.
* Connects to the database.
*
* This abstract function needs to be implemented in the real driver.
*
* @param string $server DB server name
* @param string $scheme DB scheme
* @param string $port DB port
* @param string $user DB username
* @param string $pass DB password
* @param string $db database name
* @return bool
*/
abstract function connect($server, $scheme, $port, $user, $pass, $db, $db_charset);
abstract function connect(): bool;
/**
* @brief Perform a DB query with the SQL statement $sql.
@@ -147,18 +141,27 @@ abstract class dba_driver {
*/
abstract function getdriver();
function __construct($server, $scheme, $port, $user,$pass,$db,$db_charset,$install = false) {
if(($install) && (! $this->install($server, $scheme, $port, $user, $pass, $db, $db_charset))) {
function __construct(
readonly string $server,
readonly string $scheme,
readonly string $port,
readonly string $user,
protected string $pass,
readonly string $dbname,
readonly string $db_charset,
$install = false)
{
if ($install && ! $this->install()) {
return;
}
$this->connect($server, $scheme, $port, $user, $pass, $db, $db_charset);
$this->connect();
}
function get_null_date() {
return \DBA::$null_date;
}
function get_install_script() {
public static function get_install_script() {
$platform_name = \Zotlabs\Lib\System::get_platform_name();
if(file_exists('install/' . $platform_name . '/' . \DBA::$install_script))
return 'install/' . $platform_name . '/' . \DBA::$install_script;
@@ -174,8 +177,8 @@ abstract class dba_driver {
return \DBA::$utc_now;
}
function install($server,$scheme,$port,$user,$pass,$db) {
if (!(strlen($server) && strlen($user))){
function install() {
if (!strlen($this->server) && strlen($this->user)) {
$this->connected = false;
$this->db = null;
return false;
@@ -269,7 +272,7 @@ function dbg($state) {
function dbesc($str) {
if(is_null_date($str))
$str = NULL_DATE;
$str = DBA::$dba->get_null_date();
if(\DBA::$dba && \DBA::$dba->connected)
return(\DBA::$dba->escape($str));
@@ -286,7 +289,7 @@ function dbunescbin($str) {
function dbescdate($date) {
if(is_null_date($date))
return \DBA::$dba->escape(NULL_DATE);
return \DBA::$dba->escape(DBA::$dba->get_null_date());
return \DBA::$dba->escape($date);
}
@@ -389,7 +392,7 @@ function dbq($sql) {
function dbesc_array_cb(&$item, $key) {
if(is_string($item)) {
if(is_null_date($item))
$item = NULL_DATE;
$item = DBA::$dba->get_null_date();
$item = dbesc($item);
}
}

View File

@@ -14,31 +14,35 @@ class dba_pdo extends dba_driver {
/**
* {@inheritDoc}
*
* @see dba_driver::connect()
*/
function connect($server, $scheme, $port, $user, $pass, $db, $db_charset) {
function connect(): bool {
$this->driver_dbtype = $scheme;
$this->driver_dbtype = $this->scheme;
if(strpbrk($server,':;')) {
$dsn = $this->driver_dbtype . ':unix_socket=' . trim($server, ':;');
if(strpbrk($this->server,':;')) {
$dsn = $this->driver_dbtype . ':unix_socket=' . trim($this->server, ':;');
}
else {
$dsn = $this->driver_dbtype . ':host=' . $server . (intval($port) ? ';port=' . $port : '');
$dsn = $this->driver_dbtype
. ':host='
. $this->server
. (intval($this->port) ? ';port=' . $this->port : '');
}
$dsn .= ';dbname=' . $db;
$dsn .= ';dbname=' . $this->dbname;
if ($this->driver_dbtype === 'mysql') {
$dsn .= ';charset=' . $db_charset;
$dsn .= ';charset=' . $this->db_charset;
}
else {
$dsn .= ";options='--client_encoding=" . $db_charset . "'";
$dsn .= ";options='--client_encoding=" . $this->db_charset . "'";
}
try {
$this->db = new PDO($dsn,$user,$pass);
$this->db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$this->db = new PDO($dsn, $this->user, $this->pass);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->server_version = $this->db->getAttribute(PDO::ATTR_SERVER_VERSION);
}
catch(PDOException $e) {
@@ -78,7 +82,7 @@ class dba_pdo extends dba_driver {
$result = false;
$this->error = '';
$select = stripos($sql, 'select') === 0 || stripos($sql, 'with') === 0 || stripos($sql, 'returning ') > 0;
$select = stripos($sql, 'select') === 0 || stripos($sql, 'with') === 0;
try {
$result = $this->db->query($sql, PDO::FETCH_ASSOC);
@@ -132,14 +136,11 @@ class dba_pdo extends dba_driver {
*
* @param string $table The table to insert the row into.
* @param array $data The data to insert as an array of column name => value pairs.
* @param string $idcol The column name for the primary key of the table. We need to
* specify this since we don't have a consistent naming of primary
* id for tables.
*
* @return array|bool The complete record as read back from the database, or false if we
* could not fetch it.
*/
public function insert(string $table, array $data, string $idcol): array|bool {
public function insert(string $table, array $data): array|bool {
$keys = array_keys($data);
$values = array_map(
fn ($v) => is_numeric($v) ? $v : "'" . dbesc($v) . "'",
@@ -150,39 +151,58 @@ class dba_pdo extends dba_driver {
. implode(', ', $keys) . ') VALUES ('
. implode(', ', $values) . ')';
// MySQL is the only supported DB that don't support the returning
// clause. Since the driver type is 'mysql' also for MariaDB, we need
// to check the actual server version to be sure we only exclude actual
// MySQL systems.
if ($this->driver_dbtype !== 'mysql' || stripos($this->server_version, 'mariadb') !== false) {
$query .= ' RETURNING *';
}
if ($this->is_mysql()) {
$this->db->exec($query);
$res = $this->q($query);
if (is_a($res, PDOStatement::class)) {
// MySQL don't support INSERT ... RETURNING, so we have to fetch
// the inserted data manually.
//
// Calling PDO::lastInsertId should be safe here.
// The last inserted id is kept for each connection, so we're not risking
// a race condition wrt inserts by other requests that happen simultaneously.
//
// The last inserted id is kept for each connection, so we're not
// risking a race condition wrt inserts by other requests that
// happen simultaneously.
//
$id = $this->db->lastInsertId($table);
$id_col = $this->get_id_col($table);
$res = $this->q("SELECT * FROM {$table} WHERE {$idcol} = {$id}");
if (is_a($res, PDOStatement::class)) {
db_logger('dba_pdo: PDOStatement returned, did not expect that.');
return false;
// LAST_INSERT_ID() will return 0 or null if the id column was
// specified in the INSERT statement. Use the specified id to
// reload from the db.
if (intval($id) == 0) {
$id = $data[$id_col];
}
$st = $this->db->prepare("SELECT * FROM {$table} WHERE {$id_col} = ?");
$st->execute([$id]);
} else {
// Postgres and MariaDB support retruning the data immediately, so
// add the RETURNING clause to the query
$query .= ' RETURNING *';
$st = $this->db->query($query);
}
if (is_array($res)) {
// Since we should never have more than one result, unwrap the array
// so we only have the resulting row.
$res = $res[0];
return $st->fetch(PDO::FETCH_ASSOC);
}
/**
* Return the name of the column for the primary key for a given table.
*
* @param string $table The table whose primary key column we want.
*
* @return string The name of the column for the primary key.
*/
private function get_id_col(string $table): string {
$id_col = '';
$st = $this->db->query("SHOW INDEX from {$table} WHERE key_name = 'PRIMARY'");
if ($st !== false) {
$res = $st->fetch(PDO::FETCH_ASSOC);
$id_col = $res['Column_name'];
}
return $res;
return $id_col;
}
/**
@@ -300,4 +320,24 @@ class dba_pdo extends dba_driver {
return 'pdo';
}
/**
* Return true if this we're running on a MySQL server.
*
* Note, this will return `false` on MariaDB.
*
* @return true if the database is a MySQL server instance.
*/
public function is_mysql(): bool {
return $this->driver_dbtype === 'mysql' &&
stripos($this->server_version, 'mariadb') === false;
}
/**
* Return true if we're running on a PostgreSQL server.
*
* @return true if the database is a PostgreSQL server instance.
*/
public function is_postgres(): bool {
return $this->driver_dbtype === 'pgsql';
}
}

View File

@@ -101,7 +101,6 @@ function format_event_obj($jobject) {
if (is_array($object) && (array_key_exists('summary', $object) || array_key_exists('name', $object))) {
$dtend = ((array_key_exists('endTime', $object)) ? $object['endTime'] : NULL_DATE);
$title = $object['name'] ?? '';
$content = html2bbcode($object['content']);
@@ -112,34 +111,46 @@ function format_event_obj($jobject) {
$content = $bbdescription[1];
}
// mobilizon sets a timezone in the object
// we will assume that events with an timezone should be adjusted
// We will assume that events with a timezone set should be adjusted except if the timezone is UTC in which case we will unset it.
$tz = $object['timezone'] ?? '';
// friendica has its own flag for adjust
$dfrn_adjust = $object['dfrn:adjust'] ?? '';
$adjust = ((strpos($object['startTime'], 'Z') !== false) || $tz || $dfrn_adjust);
$allday = (($adjust) ? false : true);
$dtstart = new DateTime($object['startTime']);
$dtend_obj = new DateTime($dtend);
$dtdiff = $dtstart->diff($dtend_obj);
$oneday = false;
if($allday && ($dtdiff->days < 2))
$oneday = true;
if($allday && !$oneday) {
// Subtract one day from the end date so we can use the "first day - last day" format for display.
$dtend_obj->modify('-1 day');
$dtend = datetime_convert('UTC', 'UTC', $dtend_obj->format('Y-m-d H:i:s'));
if ($tz === 'UTC') {
$tz = '';
}
// Friendica has its own flag for adjust
$dfrn_adjust = $object['dfrn:adjust'] ?? '';
$dtstart_obj = new DateTime($object['startTime']);
$adjust = str_contains($object['startTime'], 'Z') || $tz || $dfrn_adjust || $dtstart_obj->getOffset() || (!$dtstart_obj->getOffset() && !str_contains($object['startTime'], 'T00:00:00') && !str_contains($object['endTime'], 'T00:00:00'));
$allday = !$adjust;
$oneday = false;
$bd_format = (($allday) ? t('l F d, Y') : t('l F d, Y \@ g:i A')); // Friday January 18, 2011 @ 8:01 AM or Friday January 18, 2011 for allday events
$dtend_title = '';
$dtend_dt = '';
$dtend = $object['endTime'] ?? null;
if ($dtend) {
$dtend_obj = new DateTime($dtend);
$dtdiff = $dtstart_obj->diff($dtend_obj);
if($allday && ($dtdiff->days < 2))
$oneday = true;
if($allday && !$oneday) {
// Subtract one day from the end date so we can use the "first day - last day" format for display.
$dtend_obj->modify('-1 day');
$dtend = datetime_convert('UTC', 'UTC', $dtend_obj->format('Y-m-d H:i:s'));
}
$dtend_title = datetime_convert('UTC', 'UTC', $dtend, ((strpos($object['startTime'], 'Z')) ? ATOM_TIME : 'Y-m-d\TH:i:s' ));
$dtend_dt = (($adjust) ? day_translate(datetime_convert('UTC', date_default_timezone_get(), $dtend, $bd_format)) : day_translate(datetime_convert('UTC', 'UTC', $dtend, $bd_format)));
}
$event['header'] = replace_macros(get_markup_template('event_item_header.tpl'), array(
'$title' => $title,
'$dtstart_label' => t('Start:'),
@@ -147,11 +158,11 @@ function format_event_obj($jobject) {
'$dtstart_dt' => (($adjust) ? day_translate(datetime_convert('UTC', date_default_timezone_get(), $object['startTime'], $bd_format)) : day_translate(datetime_convert('UTC', 'UTC', $object['startTime'], $bd_format))),
'$finish' => ((array_key_exists('endTime', $object)) ? true : false),
'$dtend_label' => t('End:'),
'$dtend_title' => datetime_convert('UTC', 'UTC', $dtend, ((strpos($object['startTime'], 'Z')) ? ATOM_TIME : 'Y-m-d\TH:i:s' )),
'$dtend_dt' => (($adjust) ? day_translate(datetime_convert('UTC', date_default_timezone_get(), $dtend, $bd_format)) : day_translate(datetime_convert('UTC', 'UTC', $dtend, $bd_format))),
'$dtend_title' => $dtend_title,
'$dtend_dt' => $dtend_dt,
'$allday' => $allday,
'$oneday' => $oneday,
'$event_tz' => ['label' => t('Timezone'), 'value' => (($tz === date_default_timezone_get()) ? '' : $tz)]
'$tz' => ['label' => t('Timezone'), 'value' => (($tz && $tz !== date_default_timezone_get()) ? date_default_timezone_get() : '')]
));
$event['content'] = replace_macros(get_markup_template('event_item_content.tpl'), array(
@@ -526,14 +537,14 @@ function event_store_event($arr) {
$arr['deny_gid'] = $arr['deny_gid'] ?? '';
if (! $arr['dtend']) {
$arr['dtend'] = NULL_DATE;
$arr['dtend'] = DBA::$dba->get_null_date();
$arr['nofinish'] = 1;
}
if(array_key_exists('event_status_date',$arr))
$arr['event_status_date'] = datetime_convert('UTC','UTC', $arr['event_status_date']);
else
$arr['event_status_date'] = NULL_DATE;
$arr['event_status_date'] = DBA::$dba->get_null_date();
$existing_event = null;
@@ -1269,6 +1280,8 @@ function event_store_item($arr, $event, $deliver = true) {
$x = [
'type' => 'Event',
'id' => z_root() . '/event/' . $r[0]['resource_id'],
'uuid' => $r[0]['resource_id'],
'timezone' => $arr['timezone'],
'name' => $arr['summary'],
// 'summary' => bbcode($arr['summary']),
// RFC3339 Section 4.3
@@ -1395,6 +1408,8 @@ function event_store_item($arr, $event, $deliver = true) {
$y = [
'type' => 'Event',
'id' => z_root() . '/event/' . $event['event_hash'],
'uuid' => $event['event_hash'],
'timezone' => $arr['timezone'],
'name' => $arr['summary'],
// 'summary' => bbcode($arr['summary']),
// RFC3339 Section 4.3
@@ -1425,13 +1440,6 @@ function event_store_item($arr, $event, $deliver = true) {
];
$item_arr['tgt_type'] = 'Collection';
// propagate the event resource_id so that posts containing it are easily searchable in downstream copies
// of the item which have not stored the actual event. Required for Diaspora event federation as Diaspora
// event_participation messages refer to the event resource_id as a parent, while out own event attendance
// activities refer to the item message_id as the parent.
set_iconfig($item_arr, 'system', 'event_id', $event['event_hash'], true);
$post = item_store($item_arr, deliver: $deliver);
/**

View File

@@ -5,6 +5,7 @@
*/
use Zotlabs\Lib\Config;
use Zotlabs\Lib\MessageFilter;
/**
* @brief Return an Atom feed for channel.
@@ -24,7 +25,7 @@ function get_public_feed($channel, $params) {
$params = [];
$params['type'] = ((x($params,'type')) ? $params['type'] : 'xml');
$params['begin'] = ((x($params,'begin')) ? $params['begin'] : NULL_DATE);
$params['begin'] = ((x($params,'begin')) ? $params['begin'] : DBA::$dba->get_null_date());
$params['end'] = ((x($params,'end')) ? $params['end'] : datetime_convert('UTC','UTC','now'));
$params['start'] = ((x($params,'start')) ? $params['start'] : 0);
$params['records'] = ((x($params,'records')) ? $params['records'] : 40);
@@ -160,6 +161,10 @@ function get_feed_for($channel, $observer_hash, $params) {
if($item['item_private'])
continue;
if (in_array($item['verb'], ['Add', 'Remove'])) {
continue;
}
$atom .= atom_entry($item, $type, null, $channel, true, '', $params['compat']);
}
}
@@ -1297,7 +1302,7 @@ function consume_feed($xml, $importer, &$contact, $pass = 0) {
if($r) {
$parent_item = $r[0];
if(intval($parent_item['item_nocomment']) || $parent_item['comment_policy'] === 'none'
|| ($parent_item['comments_closed'] > NULL_DATE && $parent_item['comments_closed'] < datetime_convert())) {
|| ($parent_item['comments_closed'] > DBA::$dba->get_null_date() && $parent_item['comments_closed'] < datetime_convert())) {
logger('comments disabled for post ' . $parent_item['mid']);
continue;
}
@@ -1343,8 +1348,18 @@ function consume_feed($xml, $importer, &$contact, $pass = 0) {
// but save the thread_parent in case we need to refer to it later.
if($importer['channel_system']) {
if( ! \Zotlabs\Lib\MessageFilter::evaluate($datarray,Config::Get('system','pubstream_incl'),Config::Get('system','pubstream_excl'))) {
continue;
$incl = Config::Get('system','pubstream_incl', '');
$excl = Config::Get('system','pubstream_excl', '');
if ($incl || $excl) {
$plaintext = prepare_text($datarray['body'], ((isset($datarray['mimetype'])) ? $datarray['mimetype'] : 'text/bbcode'));
$plaintext = html2plain((isset($datarray['summary']) && $datarray['summary']) ? $datarray['summary'] . ' ' . $plaintext : $plaintext);
$plaintext = html2plain((isset($datarray['title']) && $datarray['title']) ? $datarray['title'] . ' ' . $plaintext : $plaintext);
if (!(new MessageFilter($datarray, html_entity_decode($incl), html_entity_decode($excl), ['plaintext' => $plaintext]))->evaluate()) {
logger('post is filtered');
continue;
}
}
}
@@ -1444,7 +1459,7 @@ function consume_feed($xml, $importer, &$contact, $pass = 0) {
$datarray['owner_xchan'] = $contact['xchan_hash'];
if(array_key_exists('created',$datarray) && $datarray['created'] > NULL_DATE && $expire_days) {
if(array_key_exists('created',$datarray) && $datarray['created'] > DBA::$dba->get_null_date() && $expire_days) {
$t1 = $datarray['created'];
$t2 = datetime_convert('UTC','UTC','now - ' . $expire_days . 'days');
if($t1 < $t2) {
@@ -1504,8 +1519,18 @@ function consume_feed($xml, $importer, &$contact, $pass = 0) {
}
if($importer['channel_system']) {
if( ! \Zotlabs\Lib\MessageFilter::evaluate($datarray,Config::Get('system','pubstream_incl'),Config::Get('system','pubstream_excl'))) {
continue;
$incl = Config::Get('system','pubstream_incl', '');
$excl = Config::Get('system','pubstream_excl', '');
if ($incl || $excl) {
$plaintext = prepare_text($datarray['body'], ((isset($datarray['mimetype'])) ? $datarray['mimetype'] : 'text/bbcode'));
$plaintext = html2plain((isset($datarray['summary']) && $datarray['summary']) ? $datarray['summary'] . ' ' . $plaintext : $plaintext);
$plaintext = html2plain((isset($datarray['title']) && $datarray['title']) ? $datarray['title'] . ' ' . $plaintext : $plaintext);
if (!(new MessageFilter($datarray, html_entity_decode($incl), html_entity_decode($excl), ['plaintext' => $plaintext]))->evaluate()) {
logger('post is filtered');
continue;
}
}
}

View File

@@ -35,8 +35,8 @@ function hubloc_store_lowlevel($arr) {
'hubloc_callback' => ((array_key_exists('hubloc_callback',$arr)) ? $arr['hubloc_callback'] : ''),
'hubloc_connect' => ((array_key_exists('hubloc_connect',$arr)) ? $arr['hubloc_connect'] : ''),
'hubloc_sitekey' => ((array_key_exists('hubloc_sitekey',$arr)) ? $arr['hubloc_sitekey'] : ''),
'hubloc_updated' => ((array_key_exists('hubloc_updated',$arr)) ? $arr['hubloc_updated'] : NULL_DATE),
'hubloc_connected' => ((array_key_exists('hubloc_connected',$arr)) ? $arr['hubloc_connected'] : NULL_DATE),
'hubloc_updated' => ((array_key_exists('hubloc_updated',$arr)) ? $arr['hubloc_updated'] : DBA::$dba->get_null_date()),
'hubloc_connected' => ((array_key_exists('hubloc_connected',$arr)) ? $arr['hubloc_connected'] : DBA::$dba->get_null_date()),
'hubloc_primary' => ((array_key_exists('hubloc_primary',$arr)) ? $arr['hubloc_primary'] : 0),
'hubloc_orphancheck' => ((array_key_exists('hubloc_orphancheck',$arr)) ? $arr['hubloc_orphancheck'] : 0),
'hubloc_error' => ((array_key_exists('hubloc_error',$arr)) ? $arr['hubloc_error'] : 0),
@@ -52,9 +52,9 @@ function site_store_lowlevel($arr) {
'site_url' => ((array_key_exists('site_url',$arr)) ? $arr['site_url'] : ''),
'site_access' => ((array_key_exists('site_access',$arr)) ? $arr['site_access'] : 0),
'site_flags' => ((array_key_exists('site_flags',$arr)) ? $arr['site_flags'] : 0),
'site_update' => ((array_key_exists('site_update',$arr)) ? $arr['site_update'] : NULL_DATE),
'site_pull' => ((array_key_exists('site_pull',$arr)) ? $arr['site_pull'] : NULL_DATE),
'site_sync' => ((array_key_exists('site_sync',$arr)) ? $arr['site_sync'] : NULL_DATE),
'site_update' => ((array_key_exists('site_update',$arr)) ? $arr['site_update'] : DBA::$dba->get_null_date()),
'site_pull' => ((array_key_exists('site_pull',$arr)) ? $arr['site_pull'] : DBA::$dba->get_null_date()),
'site_sync' => ((array_key_exists('site_sync',$arr)) ? $arr['site_sync'] : DBA::$dba->get_null_date()),
'site_directory' => ((array_key_exists('site_directory',$arr)) ? $arr['site_directory'] : ''),
'site_register' => ((array_key_exists('site_register',$arr)) ? $arr['site_register'] : 0),
'site_sellpage' => ((array_key_exists('site_sellpage',$arr)) ? $arr['site_sellpage'] : ''),
@@ -362,7 +362,7 @@ function z6_discover() {
// find unregistered zot6 clone hublocs
$c = q("select channel_hash, channel_portable_id from channel where channel_deleted = '%s'",
dbesc(NULL_DATE)
dbesc(DBA::$dba->get_null_date())
);
if ($c) {
foreach ($c as $entry) {

View File

@@ -1,5 +1,6 @@
<?php
use DBA;
use Zotlabs\Lib\Apps;
use Zotlabs\Lib\Config;
use Zotlabs\Lib\IConfig;
@@ -532,9 +533,9 @@ function sync_apps($channel, $apps) {
);
}
if((! $app['app_created']) || ($app['app_created'] <= NULL_DATE))
if((! $app['app_created']) || ($app['app_created'] <= DBA::$dba->get_null_date()))
$app['app_created'] = datetime_convert();
if((! $app['app_edited']) || ($app['app_edited'] <= NULL_DATE))
if((! $app['app_edited']) || ($app['app_edited'] <= DBA::$dba->get_null_date()))
$app['app_edited'] = datetime_convert();
$app['app_channel'] = $channel['channel_id'];
@@ -746,9 +747,9 @@ function sync_chatrooms($channel, $chatrooms) {
unset($chatroom['cr_aid']);
unset($chatroom['cr_uid']);
if((! $chatroom['cr_created']) || ($chatroom['cr_created'] <= NULL_DATE))
if((! $chatroom['cr_created']) || ($chatroom['cr_created'] <= DBA::$dba->get_null_date()))
$chatroom['cr_created'] = datetime_convert();
if((! $chatroom['cr_edited']) || ($chatroom['cr_edited'] <= NULL_DATE))
if((! $chatroom['cr_edited']) || ($chatroom['cr_edited'] <= DBA::$dba->get_null_date()))
$chatroom['cr_edited'] = datetime_convert();
$chatroom['cr_aid'] = $channel['channel_account_id'];

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