diff options
-rw-r--r-- | Zotlabs/Module/Item.php | 455 | ||||
-rw-r--r-- | tests/unit/Module/ItemTest.php | 56 |
2 files changed, 287 insertions, 224 deletions
diff --git a/Zotlabs/Module/Item.php b/Zotlabs/Module/Item.php index d96cfd822..fba16fbe1 100644 --- a/Zotlabs/Module/Item.php +++ b/Zotlabs/Module/Item.php @@ -44,233 +44,11 @@ class Item extends Controller { function init() { if (Libzot::is_zot_request()) { - - $item_id = argv(1); - - if (!$item_id) - http_status_exit(404, 'Not found'); - - $portable_id = EMPTY_STR; - - $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", - dbesc(ACTIVITY_FOLLOW), - dbesc(ACTIVITY_UNFOLLOW) - ); - - $item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 $item_normal_extra "; - - $i = null; - - // do we have the item (at all)? - - $r = q("select parent_mid from item where uuid = '%s' $item_normal limit 1", - dbesc($item_id) - ); - - if (!$r) { - http_status_exit(404, 'Not found'); - } - - // process an authenticated fetch - - $sigdata = HTTPSig::verify(($_SERVER['REQUEST_METHOD'] === 'POST') ? file_get_contents('php://input') : EMPTY_STR); - if ($sigdata['portable_id'] && $sigdata['header_valid']) { - $portable_id = $sigdata['portable_id']; - if (!check_channelallowed($portable_id)) { - http_status_exit(403, 'Permission denied'); - } - if (!check_siteallowed($sigdata['signer'])) { - http_status_exit(403, 'Permission denied'); - } - observer_auth($portable_id); - - $i = q("select id as item_id, uid from item where mid = '%s' $item_normal and owner_xchan = '%s' limit 1", - dbesc($r[0]['parent_mid']), - dbesc($portable_id) - ); - } - elseif (Config::get('system', 'require_authenticated_fetch', false)) { - http_status_exit(403, 'Permission denied'); - } - - // if we don't have a parent id belonging to the signer see if we can obtain one as a visitor that we have permission to access - // with a bias towards those items owned by channels on this site (item_wall = 1) - - $sql_extra = item_permissions_sql(0); - - if (!$i) { - $i = q("select id as item_id, uid, item_private from item where mid = '%s' $item_normal $sql_extra order by item_wall desc limit 1", - dbesc($r[0]['parent_mid']) - ); - } - - if (!$i) { - http_status_exit(403, 'Forbidden'); - } - - $chan = channelx_by_n($i[0]['uid']); - - if (!$chan) { - http_status_exit(404, 'Not found'); - } - - if (!perm_is_allowed($chan['channel_id'], get_observer_hash(), 'view_stream')) { - http_status_exit(403, 'Forbidden'); - } - - $parents_str = ids_to_querystr($i, 'item_id'); - - // We won't need to check for privacy mismatches if the verified observer is also owner - $parent_item_private = ((isset($i[0]['item_private'])) ? " and item_private = " . intval($i[0]['item_private']) . " " : ''); - - $total = q("SELECT count(*) AS count FROM item WHERE parent = %d $parent_item_private $item_normal ", - intval($parents_str) - ); - - App::set_pager_total($total[0]['count']); - App::set_pager_itemspage(30); - - if (App::$pager['total'] > App::$pager['itemspage']) { - // let mod conversation handle this request - App::$query_string = str_replace('item', 'conversation', App::$query_string); - $i = Activity::paged_collection_init(App::$pager['total'], App::$query_string); - as_return_and_die($i ,$chan); - } - else { - $items = q("SELECT item.*, item.id AS item_id FROM item WHERE item.parent = %d $parent_item_private $item_normal ORDER BY item.id", - intval($parents_str) - ); - - xchan_query($items, true); - $items = fetch_post_tags($items, true); - - $i = Activity::encode_item_collection($items, App::$query_string, 'OrderedCollection', App::$pager['total']); - } - - if ($portable_id && (!intval($items[0]['item_private']))) { - $c = q("select abook_id from abook where abook_channel = %d and abook_xchan = '%s'", - intval($items[0]['uid']), - dbesc($portable_id) - ); - if (!$c) { - ThreadListener::store(z_root() . '/item/' . $item_id, $portable_id); - } - } - - as_return_and_die($i ,$chan); + $this->init_zot_request(); } if (ActivityStreams::is_as_request()) { - - $item_id = argv(1); - if (!$item_id) - http_status_exit(404, 'Not found'); - - $portable_id = EMPTY_STR; - - $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", - dbesc(ACTIVITY_FOLLOW), - dbesc(ACTIVITY_UNFOLLOW) - ); - - $item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 $item_normal_extra "; - - $i = null; - - // do we have the item (at all)? - // add preferential bias to item owners (item_wall = 1) - - $r = q("select * from item where uuid = '%s' $item_normal order by item_wall desc limit 1", - dbesc($item_id) - ); - - if (!$r) { - http_status_exit(404, 'Not found'); - } - - // process an authenticated fetch - - $sigdata = HTTPSig::verify(EMPTY_STR); - if ($sigdata['portable_id'] && $sigdata['header_valid']) { - $portable_id = $sigdata['portable_id']; - if (!check_channelallowed($portable_id)) { - http_status_exit(403, 'Permission denied'); - } - if (!check_siteallowed($sigdata['signer'])) { - http_status_exit(403, 'Permission denied'); - } - observer_auth($portable_id); - - $i = q("select id as item_id from item where mid = '%s' $item_normal and owner_xchan = '%s' limit 1 ", - dbesc($r[0]['parent_mid']), - dbesc($portable_id) - ); - } - elseif (Config::get('system', 'require_authenticated_fetch', false)) { - http_status_exit(403, 'Permission denied'); - } - - // if we don't have a parent id belonging to the signer see if we can obtain one as a visitor that we have permission to access - // with a bias towards those items owned by channels on this site (item_wall = 1) - - $sql_extra = item_permissions_sql(0); - - if (!$i) { - $i = q("select id as item_id from item where mid = '%s' $item_normal $sql_extra order by item_wall desc limit 1", - dbesc($r[0]['parent_mid']) - ); - } - - $bear = Activity::token_from_request(); - if ($bear) { - logger('bear: ' . $bear, LOGGER_DEBUG); - if (!$i) { - $t = q("select * from iconfig where cat = 'ocap' and k = 'relay' and v = '%s'", - dbesc($bear) - ); - if ($t) { - $i = q("select id as item_id from item where uuid = '%s' and id = %d $item_normal limit 1", - dbesc($item_id), - intval($t[0]['iid']) - ); - } - } - } - - if (!$i) { - http_status_exit(403, 'Forbidden'); - } - - // If we get to this point we have determined we can access the original in $r (fetched much further above), so use it. - - xchan_query($r, true); - $items = fetch_post_tags($r, false); - - $chan = channelx_by_n($items[0]['uid']); - - if (!$chan) - http_status_exit(404, 'Not found'); - - if (!perm_is_allowed($chan['channel_id'], get_observer_hash(), 'view_stream')) - http_status_exit(403, 'Forbidden'); - - $i = Activity::encode_item($items[0]); - - if (!$i) - http_status_exit(404, 'Not found'); - - if ($portable_id && (!intval($items[0]['item_private']))) { - $c = q("select abook_id from abook where abook_channel = %d and abook_xchan = '%s'", - intval($items[0]['uid']), - dbesc($portable_id) - ); - if (!$c) { - ThreadListener::store(z_root() . '/item/' . $item_id, $portable_id); - } - } - - as_return_and_die($i ,$chan); - + $this->init_as_request(); } @@ -1672,5 +1450,234 @@ class Item extends Controller { } } + private function init_zot_request() { + + $item_id = argv(1); + + if (!$item_id) + http_status_exit(404, 'Not found'); + + $portable_id = EMPTY_STR; + + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", + dbesc(ACTIVITY_FOLLOW), + dbesc(ACTIVITY_UNFOLLOW) + ); + + $item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 $item_normal_extra "; + + $i = null; + + // do we have the item (at all)? + + $r = q("select parent_mid from item where uuid = '%s' $item_normal limit 1", + dbesc($item_id) + ); + + if (!$r) { + http_status_exit(404, 'Not found'); + } + + // process an authenticated fetch + + $sigdata = HTTPSig::verify(($_SERVER['REQUEST_METHOD'] === 'POST') ? file_get_contents('php://input') : EMPTY_STR); + if ($sigdata['portable_id'] && $sigdata['header_valid']) { + $portable_id = $sigdata['portable_id']; + if (!check_channelallowed($portable_id)) { + http_status_exit(403, 'Permission denied'); + } + if (!check_siteallowed($sigdata['signer'])) { + http_status_exit(403, 'Permission denied'); + } + observer_auth($portable_id); + + $i = q("select id as item_id, uid from item where mid = '%s' $item_normal and owner_xchan = '%s' limit 1", + dbesc($r[0]['parent_mid']), + dbesc($portable_id) + ); + } + elseif (Config::get('system', 'require_authenticated_fetch', false)) { + http_status_exit(403, 'Permission denied'); + } + + // if we don't have a parent id belonging to the signer see if we can obtain one as a visitor that we have permission to access + // with a bias towards those items owned by channels on this site (item_wall = 1) + + $sql_extra = item_permissions_sql(0); + + if (!$i) { + $i = q("select id as item_id, uid, item_private from item where mid = '%s' $item_normal $sql_extra order by item_wall desc limit 1", + dbesc($r[0]['parent_mid']) + ); + } + + if (!$i) { + http_status_exit(403, 'Forbidden'); + } + + $chan = channelx_by_n($i[0]['uid']); + + if (!$chan) { + http_status_exit(404, 'Not found'); + } + + if (!perm_is_allowed($chan['channel_id'], get_observer_hash(), 'view_stream')) { + http_status_exit(403, 'Forbidden'); + } + + $parents_str = ids_to_querystr($i, 'item_id'); + + // We won't need to check for privacy mismatches if the verified observer is also owner + $parent_item_private = ((isset($i[0]['item_private'])) ? " and item_private = " . intval($i[0]['item_private']) . " " : ''); + + $total = q("SELECT count(*) AS count FROM item WHERE parent = %d $parent_item_private $item_normal ", + intval($parents_str) + ); + + App::set_pager_total($total[0]['count']); + App::set_pager_itemspage(30); + + if (App::$pager['total'] > App::$pager['itemspage']) { + // let mod conversation handle this request + App::$query_string = str_replace('item', 'conversation', App::$query_string); + $i = Activity::paged_collection_init(App::$pager['total'], App::$query_string); + as_return_and_die($i ,$chan); + } + else { + $items = q("SELECT item.*, item.id AS item_id FROM item WHERE item.parent = %d $parent_item_private $item_normal ORDER BY item.id", + intval($parents_str) + ); + + xchan_query($items, true); + $items = fetch_post_tags($items, true); + + $i = Activity::encode_item_collection($items, App::$query_string, 'OrderedCollection', App::$pager['total']); + } + + if ($portable_id && (!intval($items[0]['item_private']))) { + $c = q("select abook_id from abook where abook_channel = %d and abook_xchan = '%s'", + intval($items[0]['uid']), + dbesc($portable_id) + ); + if (!$c) { + ThreadListener::store(z_root() . '/item/' . $item_id, $portable_id); + } + } + + as_return_and_die($i ,$chan); + } + + private function init_as_request() { + + $item_id = argv(1); + if (!$item_id) + http_status_exit(404, 'Not found'); + + $portable_id = EMPTY_STR; + + $item_normal_extra = sprintf(" and not verb in ('Follow', 'Ignore', '%s', '%s') ", + dbesc(ACTIVITY_FOLLOW), + dbesc(ACTIVITY_UNFOLLOW) + ); + + $item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 $item_normal_extra "; + + $i = null; + + // do we have the item (at all)? + // add preferential bias to item owners (item_wall = 1) + + $r = q("select * from item where uuid = '%s' $item_normal order by item_wall desc limit 1", + dbesc($item_id) + ); + + if (!$r) { + http_status_exit(404, 'Not found'); + } + + // process an authenticated fetch + + $sigdata = HTTPSig::verify(EMPTY_STR); + if ($sigdata['portable_id'] && $sigdata['header_valid']) { + $portable_id = $sigdata['portable_id']; + if (!check_channelallowed($portable_id)) { + http_status_exit(403, 'Permission denied'); + } + if (!check_siteallowed($sigdata['signer'])) { + http_status_exit(403, 'Permission denied'); + } + observer_auth($portable_id); + + $i = q("select id as item_id from item where mid = '%s' $item_normal and owner_xchan = '%s' limit 1 ", + dbesc($r[0]['parent_mid']), + dbesc($portable_id) + ); + } + elseif (Config::get('system', 'require_authenticated_fetch', false)) { + http_status_exit(403, 'Permission denied'); + } + + // if we don't have a parent id belonging to the signer see if we can obtain one as a visitor that we have permission to access + // with a bias towards those items owned by channels on this site (item_wall = 1) + + $sql_extra = item_permissions_sql(0); + + if (!$i) { + $i = q("select id as item_id from item where mid = '%s' $item_normal $sql_extra order by item_wall desc limit 1", + dbesc($r[0]['parent_mid']) + ); + } + + $bear = Activity::token_from_request(); + if ($bear) { + logger('bear: ' . $bear, LOGGER_DEBUG); + if (!$i) { + $t = q("select * from iconfig where cat = 'ocap' and k = 'relay' and v = '%s'", + dbesc($bear) + ); + if ($t) { + $i = q("select id as item_id from item where uuid = '%s' and id = %d $item_normal limit 1", + dbesc($item_id), + intval($t[0]['iid']) + ); + } + } + } + + if (!$i) { + http_status_exit(403, 'Forbidden'); + } + + // If we get to this point we have determined we can access the original in $r (fetched much further above), so use it. + + xchan_query($r, true); + $items = fetch_post_tags($r, false); + + $chan = channelx_by_n($items[0]['uid']); + + if (!$chan) + http_status_exit(404, 'Not found'); + + if (!perm_is_allowed($chan['channel_id'], get_observer_hash(), 'view_stream')) + http_status_exit(403, 'Forbidden'); + + $i = Activity::encode_item($items[0]); + + if (!$i) + http_status_exit(404, 'Not found'); + + if ($portable_id && (!intval($items[0]['item_private']))) { + $c = q("select abook_id from abook where abook_channel = %d and abook_xchan = '%s'", + intval($items[0]['uid']), + dbesc($portable_id) + ); + if (!$c) { + ThreadListener::store(z_root() . '/item/' . $item_id, $portable_id); + } + } + + as_return_and_die($i ,$chan); + + } } diff --git a/tests/unit/Module/ItemTest.php b/tests/unit/Module/ItemTest.php new file mode 100644 index 000000000..b461a3685 --- /dev/null +++ b/tests/unit/Module/ItemTest.php @@ -0,0 +1,56 @@ +<?php +/* + * SPDX-FileCopyrightText: 2024 Hubzilla Community + * SPDX-FileContributor: Harald Eilertsen + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Tests\Unit\Module; + +use PHPUnit\Framework\Attributes\TestWith; + +class ItemTest extends TestCase { + + #[TestWith(['application/x-zot+json'])] + #[TestWith(['application/x-zot-activity+json'])] + public function test_request_with_no_args_return_404(string $type): void { + $this->expect_status(404, 'Not found'); + + $_SERVER['HTTP_ACCEPT'] = $type; + $this->get('item'); + } + + #[TestWith(['application/x-zot+json'])] + #[TestWith(['application/x-zot-activity+json'])] + public function test_request_with_non_exiting_idem_id(string $type): void { + $this->expect_status(404, 'Not found'); + + $_SERVER['HTTP_ACCEPT'] = $type; + $this->get('item/non-existing-id'); + } + + /** + * Helper function to mock the `http_status_exit` function. + * + * The request will be terminated by throwing an exception, which + * will also terminate the test case. Iow. control will not return + * to the test case after the request has been made. + * + * @param int $status The expected HTTP status code. + * @param string $description The expected HTTP status description + */ + private function expect_status(int $status, string $description): void { + $this->getFunctionMock('Zotlabs\Module', 'http_status_exit') + ->expects($this->once()) + ->with($this->identicalTo($status), $this->identicalTo($description)) + ->willReturnCallback( + function () { + throw new KillmeException(); + } + ); + + $this->expectException(KillmeException::class); + + } +} |