Merge remote-tracking branch 'upstream/develop' into update

This commit is contained in:
Michael 2021-05-14 06:37:24 +00:00
commit 4d0e6305dd
148 changed files with 25160 additions and 21008 deletions

63
.drone.yml Normal file
View file

@ -0,0 +1,63 @@
kind: pipeline
type: docker
name: Check messages.po
steps:
- name: Run default Xgettext
image: friendicaci/transifex
commands:
- ./bin/run_xgettext.sh
- name: Check default
image: friendicaci/transifex
commands:
- /check-messages.sh
---
kind: pipeline
type: docker
name: php7.3-lint
steps:
- name: Test
image: php:7.3
commands:
- ./bin/composer.phar run lint
---
kind: pipeline
type: docker
name: php7.4-lint
steps:
- name: Test
image: php:7.4
commands:
- ./bin/composer.phar run lint
---
kind: pipeline
type: docker
name: php8.0-lint
steps:
- name: Test
image: php:8.0
commands:
- ./bin/composer.phar run lint
---
kind: pipeline
type: docker
name: php-cs check
trigger:
event:
- pull_request
steps:
- name: Install dependencies
image: composer
commands:
- ./bin/composer.phar run cs:install
- name: Run coding standards check
image: friendicaci/php-cs
commands:
- export CHANGED_FILES="$(git diff --name-status ${DRONE_COMMIT_BEFORE}..${DRONE_COMMIT_AFTER} | grep ^A | cut -f2)"
- /check-php-cs.sh

View file

@ -1,42 +0,0 @@
name: Lint
on: pull_request
jobs:
php-linters:
name: php${{ matrix.php-versions }} lint
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.0']
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up php${{ matrix.php-versions }}
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php-versions }}
coverage: none
- name: Lint
run: bin/composer.phar run lint
php-cs-fixer:
name: php-cs check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get changed files
id: changes
uses: jitterbit/get-changed-files@v1
- name: Set up php
uses: shivammathur/setup-php@master
with:
php-version: 7.2
coverage: none
- name: Install dependencies
run: bin/composer.phar run cs:install
- name: Run coding standards check
run: |
if ! echo "${{ steps.changes.outputs.added }}" | grep -qE "^(\\.php_cs(\\.dist)?|composer\\.lock)$"; then EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "${{ steps.changes.outputs.added }}"); else EXTRA_ARGS=''; fi
bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer fix --config=.php_cs.dist -v --diff --diff-format=udiff --dry-run --stop-on-violation --using-cache=no ${EXTRA_ARGS}
shell: bash

67
.github/workflows/releases.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: Generate release packages
on:
release:
types: [published]
jobs:
release:
name: Create release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP, with composer and extensions
uses: shivammathur/setup-php@v2
with:
tools: pecl, composer:v1
- name: Retrieve version
id: release
run: echo "::set-output name=version::$(cat VERSION)"
- name: Validate composer.json and composer.lock
run: composer validate
- name: Get composer cache directory
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Create artifact
id: artifact
run: |
ARTIFACT="friendica-full-${{ steps.release.outputs.version }}.tar.gz"
echo "::set-output name=name::${ARTIFACT}"
mkdir build
tar -cvjf build/${ARTIFACT} -T mods/release-list.txt
- name: SHA256
id: sha256
run: |
cd build
ARTIFACT=${{ steps.artifact.outputs.name }}
sha256sum "${ARTIFACT}" > "${ARTIFACT}.sha256"
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: friendica
path: build/
- name: Upload to release
uses: softprops/action-gh-release@v1
with:
files: |
build/${{ steps.artifact.outputs.name }}.tar.gz
build/${{ steps.artifact.outputs.name }}.tar.gz.sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,30 +0,0 @@
name: Transifex
on: [push, pull_request]
jobs:
messages:
name: Check messages.po changes
runs-on: ubuntu-latest
steps:
- name: Install gettext
run: sudo apt-get install gettext
- name: Checkout
uses: actions/checkout@v2
- name: Run Xgettext
run: ./bin/run_xgettext.sh
- name: Check if messages.po needs an update
run: |
echo "::group::Check messages.po"
# Skip first 4 lines in possible diff, because they're header
# Skip all lines of the git diff starting with "@@" or comments or starting "POT-Creation-Date"
if [[ $(git diff -U0 ./view/lang/C/messages.po | awk '!/@@|-"POT-Creation-Date|+"POT-Creation-Date|-#|+#/{print }' | wc -l) > 4 ]]; then
echo "::error file=messages.po::messages.po is out of date"
exit 1
else
echo "Nothing to update"
fi
echo "::endgroup::"
shell: bash

View file

@ -61,7 +61,7 @@ KEYWORDS="-k -kt -ktt:1,2"
echo "Extract strings to $OUTFILE.." echo "Extract strings to $OUTFILE.."
rm "$OUTFILE"; touch "$OUTFILE" rm "$OUTFILE"; touch "$OUTFILE"
find_result=$(find "$FINDSTARTDIR" $FINDOPTS -name "*.php" -type f | sort) find_result=$(find "$FINDSTARTDIR" $FINDOPTS -name "*.php" -type f | LC_ALL=C sort --stable)
total_files=$(wc -l <<< "${find_result}") total_files=$(wc -l <<< "${find_result}")

10
composer.lock generated
View file

@ -889,16 +889,16 @@
}, },
{ {
"name": "level-2/dice", "name": "level-2/dice",
"version": "4.0.2", "version": "4.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Level-2/Dice.git", "url": "https://github.com/Level-2/Dice.git",
"reference": "b9336d9200d0165c31e982374dc5d8d2552807bc" "reference": "3e9a8548398c01e2527110c916a93f6efa17ac9c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Level-2/Dice/zipball/b9336d9200d0165c31e982374dc5d8d2552807bc", "url": "https://api.github.com/repos/Level-2/Dice/zipball/3e9a8548398c01e2527110c916a93f6efa17ac9c",
"reference": "b9336d9200d0165c31e982374dc5d8d2552807bc", "reference": "3e9a8548398c01e2527110c916a93f6efa17ac9c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -931,7 +931,7 @@
"di", "di",
"ioc" "ioc"
], ],
"time": "2020-01-28T13:47:49+00:00" "time": "2021-04-20T14:06:06+00:00"
}, },
{ {
"name": "lightopenid/lightopenid", "name": "lightopenid/lightopenid",

View file

@ -1,6 +1,6 @@
-- ------------------------------------------ -- ------------------------------------------
-- Friendica 2021.06-dev (Siberian Iris) -- Friendica 2021.06-dev (Siberian Iris)
-- DB_UPDATE_VERSION 1414 -- DB_UPDATE_VERSION 1417
-- ------------------------------------------ -- ------------------------------------------
@ -364,6 +364,43 @@ CREATE TABLE IF NOT EXISTS `apcontact` (
FOREIGN KEY (`gsid`) REFERENCES `gserver` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT FOREIGN KEY (`gsid`) REFERENCES `gserver` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='ActivityPub compatible contacts - used in the ActivityPub implementation'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='ActivityPub compatible contacts - used in the ActivityPub implementation';
--
-- TABLE application
--
CREATE TABLE IF NOT EXISTS `application` (
`id` int unsigned NOT NULL auto_increment COMMENT 'generated index',
`client_id` varchar(64) NOT NULL COMMENT '',
`client_secret` varchar(64) NOT NULL COMMENT '',
`name` varchar(255) NOT NULL COMMENT '',
`redirect_uri` varchar(255) NOT NULL COMMENT '',
`website` varchar(255) COMMENT '',
`scopes` varchar(255) COMMENT '',
`read` boolean COMMENT 'Read scope',
`write` boolean COMMENT 'Write scope',
`follow` boolean COMMENT 'Follow scope',
PRIMARY KEY(`id`),
UNIQUE INDEX `client_id` (`client_id`)
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='OAuth application';
--
-- TABLE application-token
--
CREATE TABLE IF NOT EXISTS `application-token` (
`application-id` int unsigned NOT NULL COMMENT '',
`uid` mediumint unsigned NOT NULL COMMENT 'Owner User id',
`code` varchar(64) NOT NULL COMMENT '',
`access_token` varchar(64) NOT NULL COMMENT '',
`created_at` datetime NOT NULL COMMENT 'creation time',
`scopes` varchar(255) COMMENT '',
`read` boolean COMMENT 'Read scope',
`write` boolean COMMENT 'Write scope',
`follow` boolean COMMENT 'Follow scope',
PRIMARY KEY(`application-id`,`uid`),
INDEX `uid_id` (`uid`,`application-id`),
FOREIGN KEY (`application-id`) REFERENCES `application` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='OAuth user token';
-- --
-- TABLE attach -- TABLE attach
-- --
@ -1470,6 +1507,28 @@ CREATE TABLE IF NOT EXISTS `workerqueue` (
INDEX `done_pid_priority_created` (`done`,`pid`,`priority`,`created`) INDEX `done_pid_priority_created` (`done`,`pid`,`priority`,`created`)
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Background tasks queue entries'; ) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Background tasks queue entries';
--
-- VIEW application-view
--
DROP VIEW IF EXISTS `application-view`;
CREATE VIEW `application-view` AS SELECT
`application`.`id` AS `id`,
`application-token`.`uid` AS `uid`,
`application`.`name` AS `name`,
`application`.`redirect_uri` AS `redirect_uri`,
`application`.`website` AS `website`,
`application`.`client_id` AS `client_id`,
`application`.`client_secret` AS `client_secret`,
`application-token`.`code` AS `code`,
`application-token`.`access_token` AS `access_token`,
`application-token`.`created_at` AS `created_at`,
`application-token`.`scopes` AS `scopes`,
`application-token`.`read` AS `read`,
`application-token`.`write` AS `write`,
`application-token`.`follow` AS `follow`
FROM `application-token`
INNER JOIN `application` ON `application-token`.`application-id` = `application`.`id`;
-- --
-- VIEW post-user-view -- VIEW post-user-view
-- --

View file

@ -9,19 +9,43 @@ Friendica provides the following endpoints defined in [the official Mastodon API
Authentication is the same as described in [Using the APIs](help/api#Authentication). Authentication is the same as described in [Using the APIs](help/api#Authentication).
## Clients
Supported mobile apps:
- Tusky
- Husky
- twitlatte
- AndStatus
- Twidere
- Subway Tooter
Unsupported mobile apps:
- [Mammut](https://github.com/jamiesanson/Mammut) There are problems with the token request, see issue https://github.com/jamiesanson/Mammut/issues/19
- [Fedilab](https://framagit.org/tom79/fedilab) Automatically uses the legacy API, see issue: https://framagit.org/tom79/fedilab/-/issues/520
## Entities ## Entities
These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/entities/). These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/entities/).
## Implemented endpoints ## Implemented endpoints
- [`GET /api/v1//accounts/:id`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information) - [`GET /api/v1/accounts/:id`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information)
- [`GET /api/v1//accounts/:id/statuses`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information) - [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/methods/accounts/#retrieve-information)
- [`GET /api/v1/accounts/:id/followers`](https://docs.joinmastodon.org/methods/accounts/)
- [`GET /api/v1/accounts/:id/following`](https://docs.joinmastodon.org/methods/accounts/)
- [`GET /api/v1/accounts/:id/lists`](https://docs.joinmastodon.org/methods/accounts/)
- [`GET /api/v1/accounts/search`](https://docs.joinmastodon.org/methods/accounts)
- [`GET /api/v1/accounts/verify_credentials`](https://docs.joinmastodon.org/methods/accounts)
- [`GET /api/v1/blocks`](https://docs.joinmastodon.org/methods/accounts/blocks/)
- [`GET /api/v1/bookmarks`](https://docs.joinmastodon.org/methods/accounts/bookmarks/)
- [`GET /api/v1/custom_emojis`](https://docs.joinmastodon.org/methods/instance/custom_emojis/) - [`GET /api/v1/custom_emojis`](https://docs.joinmastodon.org/methods/instance/custom_emojis/)
- Doesn't return unicode emojis since they aren't using an image URL - Doesn't return unicode emojis since they aren't using an image URL
- [`GET /api/v1/directory`](https://docs.joinmastodon.org/methods/instance/directory/) - [`GET /api/v1/directory`](https://docs.joinmastodon.org/methods/instance/directory/)
- [`GET /api/v1/favourites`](https://docs.joinmastodon.org/methods/accounts/favourites/)
- [`GET /api/v1/follow_requests`](https://docs.joinmastodon.org/methods/accounts/follow_requests#pending-follows) - [`GET /api/v1/follow_requests`](https://docs.joinmastodon.org/methods/accounts/follow_requests#pending-follows)
- Returned IDs are specific to follow requests - Returned IDs are specific to follow requests
- [`POST /api/v1/follow_requests/:id/authorize`](https://docs.joinmastodon.org/methods/accounts/follow_requests#accept-follow) - [`POST /api/v1/follow_requests/:id/authorize`](https://docs.joinmastodon.org/methods/accounts/follow_requests#accept-follow)
@ -36,7 +60,23 @@ These endpoints use the [Mastodon API entities](https://docs.joinmastodon.org/en
- [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance#fetch-instance) - [`GET /api/v1/instance`](https://docs.joinmastodon.org/methods/instance#fetch-instance)
- [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains) - [`GET /api/v1/instance/peers`](https://docs.joinmastodon.org/methods/instance#list-of-connected-domains)
- [`GET /api/v1/lists`](https://docs.joinmastodon.org/methods/timelines/lists/)
- [`GET /api/v1/lists/:id`](https://docs.joinmastodon.org/methods/timelines/lists/)
- [`GET /api/v1/lists/:id/accounts`](https://docs.joinmastodon.org/methods/timelines/lists/)
- [`GET /api/v1/media/:id`](https://docs.joinmastodon.org/methods/statuses/media/)
- [`GET /api/v1/mutes`](https://docs.joinmastodon.org/methods/accounts/mutes/)
- [`GET /api/v1/notifications`](https://docs.joinmastodon.org/methods/notifications/)
- [`GET /api/v1/notifications/:id`](https://docs.joinmastodon.org/methods/notifications/)
- [`GET /api/v1/preferences`](https://docs.joinmastodon.org/methods/accounts/preferences/)
- [`GET /api/v1/statuses/:id`](https://docs.joinmastodon.org/methods/statuses/)
- [`GET /api/v1/statuses/:id/context`](https://docs.joinmastodon.org/methods/statuses/)
- [`GET /api/v1/statuses/:id/reblogged_by`](https://docs.joinmastodon.org/methods/statuses/)
- [`GET /api/v1/statuses/:id/favourited_by`](https://docs.joinmastodon.org/methods/statuses/)
- [`GET /api/v1/suggestions`](https://docs.joinmastodon.org/methods/accounts/suggestions/)
- [`GET /api/v1/timelines/home`](https://docs.joinmastodon.org/methods/timelines/)
- [`GET /api/v1/timelines/list/:id`](https://docs.joinmastodon.org/methods/timelines/)
- [`GET /api/v1/timelines/public`](https://docs.joinmastodon.org/methods/timelines/) - [`GET /api/v1/timelines/public`](https://docs.joinmastodon.org/methods/timelines/)
- [`GET /api/v1/timelines/tag/:hashtag`](https://docs.joinmastodon.org/methods/timelines/)
- [`GET /api/v1/trends`](https://docs.joinmastodon.org/methods/instance/trends/) - [`GET /api/v1/trends`](https://docs.joinmastodon.org/methods/instance/trends/)
## Non-implemented endpoints ## Non-implemented endpoints

View file

@ -175,6 +175,7 @@ function api_register_func($path, $func, $auth = false, $method = API_METHOD_ANY
* Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part * Simple Auth allow username in form of <pre>user@server</pre>, ignoring server part
* *
* @param App $a App * @param App $a App
* @param bool $do_login try to log in when not logged in, otherwise quit silently
* @throws ForbiddenException * @throws ForbiddenException
* @throws InternalServerErrorException * @throws InternalServerErrorException
* @throws UnauthorizedException * @throws UnauthorizedException
@ -185,8 +186,10 @@ function api_register_func($path, $func, $auth = false, $method = API_METHOD_ANY
* 'authenticated' => return status, * 'authenticated' => return status,
* 'user_record' => return authenticated user record * 'user_record' => return authenticated user record
*/ */
function api_login(App $a) function api_login(App $a, bool $do_login = true)
{ {
$_SESSION["allow_api"] = false;
// workaround for HTTP-auth in CGI mode // workaround for HTTP-auth in CGI mode
if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) { if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
$userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6)); $userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
@ -216,6 +219,10 @@ function api_login(App $a)
Logger::warning(API_LOG_PREFIX . 'OAuth error', ['module' => 'api', 'action' => 'login', 'exception' => $e->getMessage()]); Logger::warning(API_LOG_PREFIX . 'OAuth error', ['module' => 'api', 'action' => 'login', 'exception' => $e->getMessage()]);
} }
if (!$do_login) {
return;
}
Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]); Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]);
header('WWW-Authenticate: Basic realm="Friendica"'); header('WWW-Authenticate: Basic realm="Friendica"');
throw new UnauthorizedException("This API requires login"); throw new UnauthorizedException("This API requires login");
@ -247,7 +254,7 @@ function api_login(App $a)
*/ */
Hook::callAll('authenticate', $addon_auth); Hook::callAll('authenticate', $addon_auth);
if ($addon_auth['authenticated'] && count($addon_auth['user_record'])) { if ($addon_auth['authenticated'] && !empty($addon_auth['user_record'])) {
$record = $addon_auth['user_record']; $record = $addon_auth['user_record'];
} else { } else {
$user_id = User::authenticate(trim($user), trim($password), true); $user_id = User::authenticate(trim($user), trim($password), true);
@ -257,6 +264,9 @@ function api_login(App $a)
} }
if (!DBA::isResult($record)) { if (!DBA::isResult($record)) {
if (!$do_login) {
return;
}
Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]); Logger::debug(API_LOG_PREFIX . 'failed', ['module' => 'api', 'action' => 'login', 'parameters' => $_SERVER]);
header('WWW-Authenticate: Basic realm="Friendica"'); header('WWW-Authenticate: Basic realm="Friendica"');
//header('HTTP/1.0 401 Unauthorized'); //header('HTTP/1.0 401 Unauthorized');
@ -1021,7 +1031,7 @@ function api_statuses_mediap($type)
$_REQUEST['profile_uid'] = api_user(); $_REQUEST['profile_uid'] = api_user();
$_REQUEST['api_source'] = true; $_REQUEST['api_source'] = true;
$txt = requestdata('status'); $txt = requestdata('status') ?? '';
/// @TODO old-lost code? /// @TODO old-lost code?
//$txt = urldecode(requestdata('status')); //$txt = urldecode(requestdata('status'));
@ -1076,7 +1086,7 @@ function api_statuses_update($type)
// convert $_POST array items to the form we use for web posts. // convert $_POST array items to the form we use for web posts.
if (requestdata('htmlstatus')) { if (requestdata('htmlstatus')) {
$txt = requestdata('htmlstatus'); $txt = requestdata('htmlstatus') ?? '';
if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) { if ((strpos($txt, '<') !== false) || (strpos($txt, '>') !== false)) {
$txt = HTML::toBBCodeVideo($txt); $txt = HTML::toBBCodeVideo($txt);
@ -1157,30 +1167,56 @@ function api_statuses_update($type)
} }
} }
if (!empty($_FILES['media'])) { if (requestdata('media_ids')) {
$ids = explode(',', requestdata('media_ids') ?? '');
} elseif (!empty($_FILES['media'])) {
// upload the image if we have one // upload the image if we have one
$picture = wall_upload_post($a, false); $picture = wall_upload_post($a, false);
if (is_array($picture)) { if (is_array($picture)) {
$_REQUEST['body'] .= "\n\n" . '[url=' . $picture["albumpage"] . '][img]' . $picture["preview"] . "[/img][/url]"; $ids[] = $picture['id'];
} }
} }
if (requestdata('media_ids')) { $attachments = [];
$ids = explode(',', requestdata('media_ids')); $ressources = [];
if (!empty($ids)) {
foreach ($ids as $id) { foreach ($ids as $id) {
$r = q( $media = DBA::toArray(DBA::p("SELECT `resource-id`, `scale`, `nickname`, `type`, `desc`, `filename`, `datasize`, `width`, `height` FROM `photo`
"SELECT `resource-id`, `scale`, `nickname`, `type`, `desc` FROM `photo` INNER JOIN `user` ON `user`.`uid` = `photo`.`uid` WHERE `resource-id` IN (SELECT `resource-id` FROM `photo` WHERE `id` = %d) AND `scale` > 0 AND `photo`.`uid` = %d ORDER BY `photo`.`width` DESC LIMIT 1", INNER JOIN `user` ON `user`.`uid` = `photo`.`uid` WHERE `resource-id` IN
intval($id), (SELECT `resource-id` FROM `photo` WHERE `id` = ?) AND `photo`.`uid` = ?
api_user() ORDER BY `photo`.`width` DESC LIMIT 2", $id, api_user()));
);
if (DBA::isResult($r)) { if (!empty($media)) {
$ressources[] = $media[0]['resource-id'];
$phototypes = Images::supportedTypes(); $phototypes = Images::supportedTypes();
$ext = $phototypes[$r[0]['type']]; $ext = $phototypes[$media[0]['type']];
$description = $r[0]['desc'] ?? '';
$_REQUEST['body'] .= "\n\n" . '[url=' . DI::baseUrl() . '/photos/' . $r[0]['nickname'] . '/image/' . $r[0]['resource-id'] . ']'; $attachment = ['type' => Post\Media::IMAGE, 'mimetype' => $media[0]['type'],
$_REQUEST['body'] .= '[img=' . DI::baseUrl() . '/photo/' . $r[0]['resource-id'] . '-' . $r[0]['scale'] . '.' . $ext . ']' . $description . '[/img][/url]'; 'url' => DI::baseUrl() . '/photo/' . $media[0]['resource-id'] . '-' . $media[0]['scale'] . '.' . $ext,
'size' => $media[0]['datasize'],
'name' => $media[0]['filename'] ?: $media[0]['resource-id'],
'description' => $media[0]['desc'] ?? '',
'width' => $media[0]['width'],
'height' => $media[0]['height']];
if (count($media) > 1) {
$attachment['preview'] = DI::baseUrl() . '/photo/' . $media[1]['resource-id'] . '-' . $media[1]['scale'] . '.' . $ext;
$attachment['preview-width'] = $media[1]['width'];
$attachment['preview-height'] = $media[1]['height'];
}
$attachments[] = $attachment;
} }
} }
// We have to avoid that the post is rejected because of an empty body
if (empty($_REQUEST['body'])) {
$_REQUEST['body'] = '[hr]';
}
}
if (!empty($attachments)) {
$_REQUEST['attachments'] = $attachments;
} }
// set this so that the item_post() function is quiet and doesn't redirect or emit json // set this so that the item_post() function is quiet and doesn't redirect or emit json
@ -1194,6 +1230,13 @@ function api_statuses_update($type)
// call out normal post function // call out normal post function
$item_id = item_post($a); $item_id = item_post($a);
if (!empty($ressources) && !empty($item_id)) {
$item = Post::selectFirst(['uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid'], ['id' => $item_id]);
foreach ($ressources as $ressource) {
Photo::setPermissionForRessource($ressource, api_user(), $item['allow_cid'], $item['allow_gid'], $item['deny_cid'], $item['deny_gid']);
}
}
// output the post that we just posted. // output the post that we just posted.
return api_status_show($type, $item_id); return api_status_show($type, $item_id);
} }
@ -2534,7 +2577,7 @@ function api_convert_item($item)
$statustext = mb_substr($statustext, 0, 1000) . "... \n" . ($item['plink'] ?? ''); $statustext = mb_substr($statustext, 0, 1000) . "... \n" . ($item['plink'] ?? '');
} }
$statushtml = BBCode::convert(BBCode::removeAttachment($body), false); $statushtml = BBCode::convert(BBCode::removeAttachment($body), false, BBCode::API, true);
// Workaround for clients with limited HTML parser functionality // Workaround for clients with limited HTML parser functionality
$search = ["<br>", "<blockquote>", "</blockquote>", $search = ["<br>", "<blockquote>", "</blockquote>",
@ -2585,25 +2628,7 @@ function api_convert_item($item)
*/ */
function api_add_attachments_to_body(array $item) function api_add_attachments_to_body(array $item)
{ {
$body = $item['body']; $body = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']);
foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::IMAGE, Post\Media::AUDIO, Post\Media::VIDEO]) as $media) {
if (Item::containsLink($item['body'], $media['url'])) {
continue;
}
if ($media['type'] == Post\Media::IMAGE) {
if (!empty($media['description'])) {
$body .= "\n[img=" . $media['url'] . ']' . $media['description'] .'[/img]';
} else {
$body .= "\n[img]" . $media['url'] .'[/img]';
}
} elseif ($media['type'] == Post\Media::AUDIO) {
$body .= "\n[audio]" . $media['url'] . "[/audio]\n";
} elseif ($media['type'] == Post\Media::VIDEO) {
$body .= "\n[video]" . $media['url'] . "[/video]\n";
}
}
if (strpos($body, '[/img]') !== false) { if (strpos($body, '[/img]') !== false) {
return $body; return $body;

View file

@ -888,7 +888,8 @@ function conversation_fetch_items(array $parent, array $items, array $condition,
return $items; return $items;
} }
function item_photo_menu($item) { function item_photo_menu($item)
{
$sub_link = ''; $sub_link = '';
$poke_link = ''; $poke_link = '';
$contact_url = ''; $contact_url = '';
@ -929,8 +930,8 @@ function item_photo_menu($item) {
if (!empty($pcid)) { if (!empty($pcid)) {
$contact_url = 'contact/' . $pcid; $contact_url = 'contact/' . $pcid;
$posts_link = $contact_url . '/posts'; $posts_link = $contact_url . '/posts';
$block_link = $contact_url . '/block'; $block_link = $item['self'] ? '' : $contact_url . '/block';
$ignore_link = $contact_url . '/ignore'; $ignore_link = $item['self'] ? '' : $contact_url . '/ignore';
} }
if ($cid && !$item['self']) { if ($cid && !$item['self']) {
@ -983,7 +984,7 @@ function item_photo_menu($item) {
if (strpos($v, 'javascript:') === 0) { if (strpos($v, 'javascript:') === 0) {
$v = substr($v, 11); $v = substr($v, 11);
$o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL; $o .= '<li role="menuitem"><a onclick="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
} elseif ($v!='') { } elseif ($v) {
$o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL; $o .= '<li role="menuitem"><a href="' . $v . '">' . $k . '</a></li>' . PHP_EOL;
} }
} }
@ -1211,6 +1212,7 @@ function status_editor(App $a, $x, $notes_cid = 0, $popup = false)
'$edimg' => DI::l10n()->t('Image'), '$edimg' => DI::l10n()->t('Image'),
'$edurl' => DI::l10n()->t('Link'), '$edurl' => DI::l10n()->t('Link'),
'$edattach' => DI::l10n()->t('Link or Media'), '$edattach' => DI::l10n()->t('Link or Media'),
'$edvideo' => DI::l10n()->t('Video'),
'$setloc' => DI::l10n()->t('Set your location'), '$setloc' => DI::l10n()->t('Set your location'),
'$shortsetloc' => DI::l10n()->t('set location'), '$shortsetloc' => DI::l10n()->t('set location'),
'$noloc' => DI::l10n()->t('Clear browser location'), '$noloc' => DI::l10n()->t('Clear browser location'),

View file

@ -41,7 +41,7 @@ use Friendica\Protocol\Activity;
* type, event, otype, activity, verb, uid, cid, origin_cid, item, link, * type, event, otype, activity, verb, uid, cid, origin_cid, item, link,
* source_name, source_mail, source_nick, source_link, source_photo, * source_name, source_mail, source_nick, source_link, source_photo,
* show_in_notification_page * show_in_notification_page
* *
* @return bool * @return bool
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
@ -273,7 +273,7 @@ function notification($params)
$epreamble = $l10n->t('%1$s [url=%2$s]shared a post[/url] from %3$s.', $epreamble = $l10n->t('%1$s [url=%2$s]shared a post[/url] from %3$s.',
'[url='.$params['source_link'].']'.$params['source_name'].'[/url]', '[url='.$params['source_link'].']'.$params['source_name'].'[/url]',
$params['link'], '[url='.$params['origin_link'].']'.$params['origin_name'].'[/url]' $params['link'], '[url='.$params['origin_link'].']'.$params['origin_name'].'[/url]'
); );
} }
$sitelink = $l10n->t('Please visit %s to view and/or reply to the conversation.'); $sitelink = $l10n->t('Please visit %s to view and/or reply to the conversation.');

View file

@ -181,7 +181,7 @@ function dfrn_confirm_post(App $a, $handsfree = null)
* random key which is encrypted with their site public key. * random key which is encrypted with their site public key.
*/ */
$src_aes_key = openssl_random_pseudo_bytes(64); $src_aes_key = random_bytes(64);
$result = ''; $result = '';
openssl_private_encrypt($dfrn_id, $result, $user['prvkey']); openssl_private_encrypt($dfrn_id, $result, $user['prvkey']);

View file

@ -614,7 +614,8 @@ function item_post(App $a) {
$datarray['origin'] = $origin; $datarray['origin'] = $origin;
$datarray['object'] = $object; $datarray['object'] = $object;
$datarray["uri-id"] = ItemURI::getIdByURI($datarray["uri"]); $datarray['uri-id'] = ItemURI::getIdByURI($datarray['uri']);
$datarray['attachments'] = $_REQUEST['attachments'] ?? [];
/* /*
* These fields are for the convenience of addons... * These fields are for the convenience of addons...

View file

@ -139,7 +139,7 @@ function ping_init(App $a)
local_user(), Verb::getID(Activity::FOLLOW)]; local_user(), Verb::getID(Activity::FOLLOW)];
$items = Post::selectForUser(local_user(), ['wall', 'uid', 'uri-id'], $condition); $items = Post::selectForUser(local_user(), ['wall', 'uid', 'uri-id'], $condition);
if (DBA::isResult($items)) { if (DBA::isResult($items)) {
$items_unseen = Post::toArray($items); $items_unseen = Post::toArray($items, false);
$arr = ['items' => $items_unseen]; $arr = ['items' => $items_unseen];
Hook::callAll('network_ping', $arr); Hook::callAll('network_ping', $arr);

View file

@ -500,77 +500,26 @@ function settings_content(App $a)
} }
if (($a->argc > 1) && ($a->argv[1] === 'oauth')) { if (($a->argc > 1) && ($a->argv[1] === 'oauth')) {
if (($a->argc > 2) && ($a->argv[2] === 'add')) {
$tpl = Renderer::getMarkupTemplate('settings/oauth_edit.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$form_security_token' => BaseModule::getFormSecurityToken("settings_oauth"),
'$title' => DI::l10n()->t('Add application'),
'$submit' => DI::l10n()->t('Save Settings'),
'$cancel' => DI::l10n()->t('Cancel'),
'$name' => ['name', DI::l10n()->t('Name'), '', ''],
'$key' => ['key', DI::l10n()->t('Consumer Key'), '', ''],
'$secret' => ['secret', DI::l10n()->t('Consumer Secret'), '', ''],
'$redirect' => ['redirect', DI::l10n()->t('Redirect'), '', ''],
'$icon' => ['icon', DI::l10n()->t('Icon url'), '', ''],
]);
return $o;
}
if (($a->argc > 3) && ($a->argv[2] === 'edit')) {
$r = q("SELECT * FROM clients WHERE client_id='%s' AND uid=%d",
DBA::escape($a->argv[3]),
local_user());
if (!DBA::isResult($r)) {
notice(DI::l10n()->t("You can't edit this application."));
return;
}
$app = $r[0];
$tpl = Renderer::getMarkupTemplate('settings/oauth_edit.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$form_security_token' => BaseModule::getFormSecurityToken("settings_oauth"),
'$title' => DI::l10n()->t('Add application'),
'$submit' => DI::l10n()->t('Update'),
'$cancel' => DI::l10n()->t('Cancel'),
'$name' => ['name', DI::l10n()->t('Name'), $app['name'] , ''],
'$key' => ['key', DI::l10n()->t('Consumer Key'), $app['client_id'], ''],
'$secret' => ['secret', DI::l10n()->t('Consumer Secret'), $app['pw'], ''],
'$redirect' => ['redirect', DI::l10n()->t('Redirect'), $app['redirect_uri'], ''],
'$icon' => ['icon', DI::l10n()->t('Icon url'), $app['icon'], ''],
]);
return $o;
}
if (($a->argc > 3) && ($a->argv[2] === 'delete')) { if (($a->argc > 3) && ($a->argv[2] === 'delete')) {
BaseModule::checkFormSecurityTokenRedirectOnError('/settings/oauth', 'settings_oauth', 't'); BaseModule::checkFormSecurityTokenRedirectOnError('/settings/oauth', 'settings_oauth', 't');
DBA::delete('clients', ['client_id' => $a->argv[3], 'uid' => local_user()]); DBA::delete('application-token', ['application-id' => $a->argv[3], 'uid' => local_user()]);
DI::baseUrl()->redirect('settings/oauth/', true); DI::baseUrl()->redirect('settings/oauth/', true);
return; return;
} }
/// @TODO validate result with DBA::isResult() $applications = DBA::selectToArray('application-view', ['id', 'uid', 'name', 'website', 'scopes', 'created_at'], ['uid' => local_user()]);
$r = q("SELECT clients.*, tokens.id as oauth_token, (clients.uid=%d) AS my
FROM clients
LEFT JOIN tokens ON clients.client_id=tokens.client_id
WHERE clients.uid IN (%d, 0)",
local_user(),
local_user());
$tpl = Renderer::getMarkupTemplate('settings/oauth.tpl'); $tpl = Renderer::getMarkupTemplate('settings/oauth.tpl');
$o .= Renderer::replaceMacros($tpl, [ $o .= Renderer::replaceMacros($tpl, [
'$form_security_token' => BaseModule::getFormSecurityToken("settings_oauth"), '$form_security_token' => BaseModule::getFormSecurityToken("settings_oauth"),
'$baseurl' => DI::baseUrl()->get(true), '$baseurl' => DI::baseUrl()->get(true),
'$title' => DI::l10n()->t('Connected Apps'), '$title' => DI::l10n()->t('Connected Apps'),
'$add' => DI::l10n()->t('Add application'), '$name' => DI::l10n()->t('Name'),
'$edit' => DI::l10n()->t('Edit'), '$website' => DI::l10n()->t('Home Page'),
'$delete' => DI::l10n()->t('Delete'), '$created_at' => DI::l10n()->t('Created'),
'$consumerkey' => DI::l10n()->t('Client key starts with'), '$delete' => DI::l10n()->t('Remove authorization'),
'$noname' => DI::l10n()->t('No name'), '$apps' => $applications,
'$remove' => DI::l10n()->t('Remove authorization'),
'$apps' => $r,
]); ]);
return $o; return $o;
} }

View file

@ -1,495 +0,0 @@
# Drone.io test YML-File, currently disabled
# See https://github.com/friendica/friendica/pull/7643 for further infos
kind: pipeline
name: mysql8.0-php7.1
steps:
- name: mysql8.0-php7.1
image: friendicaci/php7.1:php7.1.32
commands:
- NOCOVERAGE=true ./autotest.sh mysql
environment:
MYSQL_USERNAME: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
MYSQL_HOST: mysql
node:
test: db
services:
- name: mysql
image: mysql:8.0
command: [ "--default-authentication-plugin=mysql_native_password" ]
environment:
MYSQL_ROOT_PASSWORD: friendica
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
volumes:
- name: cache
path: /var/lib/mysql
volumes:
- name: cache
temp: {}
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: mysql8.0-php7.2
steps:
- name: mysql8.0-php7.2
image: friendicaci/php7.2:php7.2.22
commands:
- NOCOVERAGE=true ./autotest.sh mysql
environment:
MYSQL_USERNAME: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
MYSQL_HOST: mysql
node:
test: db
services:
- name: mysql
image: mysql:8.0
command: [ "--default-authentication-plugin=mysql_native_password" ]
environment:
MYSQL_ROOT_PASSWORD: friendica
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
volumes:
- name: cache
path: /var/lib/mysql
volumes:
- name: cache
temp: {}
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: mysql8.0-php7.3
steps:
- name: mysql8.0-php7.3
image: friendicaci/php7.3:php7.3.9
commands:
- NOCOVERAGE=true ./autotest.sh mysql
environment:
MYSQL_USERNAME: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
MYSQL_HOST: mysql
node:
test: db
services:
- name: mysql
image: mysql:8.0
command: [ "--default-authentication-plugin=mysql_native_password" ]
environment:
MYSQL_ROOT_PASSWORD: friendica
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
volumes:
- name: cache
path: /var/lib/mysql
volumes:
- name: cache
temp: {}
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: mariadb10.1-php7.1
steps:
- name: mariadb10.1-php7.1
image: friendicaci/php7.1:php7.1.32
commands:
- phpenmod xdebug
- sleep 20
- ./autotest.sh mariadb
- wget https://codecov.io/bash -O codecov.sh
- sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
- sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
environment:
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
MYSQL_HOST: mariadb
node:
test: db
services:
- name: mariadb
image: mariadb:10.1
environment:
MYSQL_ROOT_PASSWORD: friendica
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
volumes:
- name: cache
path: /var/lib/mysql
volumes:
- name: cache
temp: {}
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: mariadb10.1-php7.2
steps:
- name: mariadb10.1-php7.2
image: friendicaci/php7.2:php7.2.22
commands:
- NOCOVERAGE=true ./autotest.sh mariadb
environment:
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
MYSQL_HOST: mariadb
node:
test: db
services:
- name: mariadb
image: mariadb:10.1
environment:
MYSQL_ROOT_PASSWORD: friendica
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
volumes:
- name: cache
path: /var/lib/mysql
volumes:
- name: cache
temp: {}
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: mariadb10.1-php7.3
steps:
- name: mariadb10.1-php7.3
image: friendicaci/php7.3:php7.3.9
commands:
- NOCOVERAGE=true ./autotest.sh mariadb
environment:
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
MYSQL_HOST: mariadb
node:
test: db
services:
- name: mariadb
image: mariadb:10.1
environment:
MYSQL_ROOT_PASSWORD: friendica
MYSQL_USER: friendica
MYSQL_PASSWORD: friendica
MYSQL_DATABASE: friendica
volumes:
- name: cache
path: /var/lib/mysql
volumes:
- name: cache
temp: {}
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: redis-php7.1
steps:
- name: redis-php7.1
image: friendicaci/php7.1:php7.1.32
commands:
- phpenmod xdebug
- sleep 20
- NOINSTALL=true TEST_SELECTION=REDIS ./autotest.sh mysql
- wget https://codecov.io/bash -O codecov.sh
- sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
- sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
environment:
REDIS_HOST: redis
services:
- name: redis
image: redis
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: redis-php7.2
steps:
- name: redis-php7.2
image: friendicaci/php7.2:php7.2.22
commands:
- NOCOVERAGE=true NOINSTALL=true TEST_SELECTION=REDIS ./autotest.sh mysql
environment:
REDIS_HOST: redis
services:
- name: redis
image: redis
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: redis-php7.3
steps:
- name: redis-php7.3
image: friendicaci/php7.3:php7.3.9
commands:
- NOCOVERAGE=true NOINSTALL=true TEST_SELECTION=REDIS ./autotest.sh mysql
environment:
REDIS_HOST: redis
services:
- name: redis
image: redis
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: memcache-php7.1
steps:
- name: memcache-php7.1
image: friendicaci/php7.1:php7.1.32
commands:
- phpenmod xdebug
- sleep 20
- NOINSTALL=true TEST_SELECTION=MEMCACHE ./autotest.sh mysql
- wget https://codecov.io/bash -O codecov.sh
- sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
- sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
environment:
MEMCACHE_HOST: memcached
services:
- name: memcached
image: memcached
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: memcache-php7.2
steps:
- name: memcache-php7.2
image: friendicaci/php7.2:php7.2.22
commands:
- NOCOVERAGE=true NOINSTALL=true TEST_SELECTION=MEMCACHE ./autotest.sh mysql
environment:
MEMCACHE_HOST: memcached
services:
- name: memcached
image: memcached
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: memcache-php7.3
steps:
- name: memcache-php7.3
image: friendicaci/php7.3:php7.3.9
commands:
- NOCOVERAGE=true NOINSTALL=true TEST_SELECTION=MEMCACHE ./autotest.sh mysql
environment:
MEMCACHE_HOST: memcached
services:
- name: memcached
image: memcached
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: memcached-php7.1
steps:
- name: memcached-php7.1
image: friendicaci/php7.1:php7.1.32
commands:
- phpenmod xdebug
- sleep 20
- NOINSTALL=true TEST_SELECTION=MEMCACHED ./autotest.sh mysql
- wget https://codecov.io/bash -O codecov.sh
- sh -c "if [ '$DRONE_BUILD_EVENT' = 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -P $DRONE_PULL_REQUEST -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
- sh -c "if [ '$DRONE_BUILD_EVENT' != 'pull_request' ]; then bash codecov.sh -B $DRONE_BRANCH -C $DRONE_COMMIT -t 5ce7d64e-07b4-4adf-8700-e2eae27e14ec -f tests/autotest-clover.xml; fi"
environment:
MEMCACHED_HOST: memcached
services:
- name: memcached
image: memcached
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: memcached-php7.2
steps:
- name: memcached-php7.2
image: friendicaci/php7.2:php7.2.22
commands:
- NOCOVERAGE=true NOINSTALL=true TEST_SELECTION=MEMCACHED ./autotest.sh mysql
environment:
MEMCACHED_HOST: memcached
services:
- name: memcached
image: memcached
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push
---
kind: pipeline
name: memcached-php7.3
steps:
- name: memcached-php7.3
image: friendicaci/php7.3:php7.3.9
commands:
- NOCOVERAGE=true NOINSTALL=true TEST_SELECTION=MEMCACHED ./autotest.sh mysql
environment:
MEMCACHED_HOST: memcached
services:
- name: memcached
image: memcached
trigger:
branch:
# - stable
- develop
# - "*-rc"
# event:
# - pull_request
# - push

29
mods/release-list.txt Normal file
View file

@ -0,0 +1,29 @@
bin/auth_ejabberd.php
bin/console
bin/console.bat
bin/console.php
bin/daemon.php
bin/testargs.php
bin/worker.php
config/
doc/
images/
include/
mod/
mods/
spec/
src/
static/
vendor/
view/
.htaccess-dist
boot.php
CHANGELOG
CREDITS.txt
database.sql
index.php
LICENSE
README.md
security.txt
update.php
VERSION

View file

@ -276,11 +276,23 @@ class Module
$profiler->set(microtime(true) - $timestamp, 'init'); $profiler->set(microtime(true) - $timestamp, 'init');
if ($server['REQUEST_METHOD'] === 'POST') { if ($server['REQUEST_METHOD'] === Router::DELETE) {
call_user_func([$this->module_class, 'delete'], $this->module_parameters);
}
if ($server['REQUEST_METHOD'] === Router::PATCH) {
call_user_func([$this->module_class, 'patch'], $this->module_parameters);
}
if ($server['REQUEST_METHOD'] === Router::POST) {
Core\Hook::callAll($this->module . '_mod_post', $post); Core\Hook::callAll($this->module . '_mod_post', $post);
call_user_func([$this->module_class, 'post'], $this->module_parameters); call_user_func([$this->module_class, 'post'], $this->module_parameters);
} }
if ($server['REQUEST_METHOD'] === Router::PUT) {
call_user_func([$this->module_class, 'put'], $this->module_parameters);
}
Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder); Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder);
call_user_func([$this->module_class, 'afterpost'], $this->module_parameters); call_user_func([$this->module_class, 'afterpost'], $this->module_parameters);

View file

@ -72,6 +72,26 @@ abstract class BaseModule
return $o; return $o;
} }
/**
* Module DELETE method to process submitted data
*
* Extend this method if the module is supposed to process DELETE requests.
* Doesn't display any content
*/
public static function delete(array $parameters = [])
{
}
/**
* Module PATCH method to process submitted data
*
* Extend this method if the module is supposed to process PATCH requests.
* Doesn't display any content
*/
public static function patch(array $parameters = [])
{
}
/** /**
* Module POST method to process submitted data * Module POST method to process submitted data
* *
@ -92,6 +112,16 @@ abstract class BaseModule
{ {
} }
/**
* Module PUT method to process submitted data
*
* Extend this method if the module is supposed to process PUT requests.
* Doesn't display any content
*/
public static function put(array $parameters = [])
{
}
/* /*
* Functions used to protect against Cross-Site Request Forgery * Functions used to protect against Cross-Site Request Forgery
* The security token has to base on at least one value that an attacker can't know - here it's the session ID and the private key. * The security token has to base on at least one value that an attacker can't know - here it's the session ID and the private key.

View file

@ -128,6 +128,10 @@ HELP;
throw new RuntimeException("$cat.$key is an array and can't be set using this command."); throw new RuntimeException("$cat.$key is an array and can't be set using this command.");
} }
if ($this->config->get($cat, $key) === $value) {
throw new RuntimeException("$cat.$key already set to $value.");
}
$result = $this->config->set($cat, $key, $value); $result = $this->config->set($cat, $key, $value);
if ($result) { if ($result) {
$this->out("{$cat}.{$key} <= " . $this->out("{$cat}.{$key} <= " .

287
src/Console/Contact.php Normal file
View file

@ -0,0 +1,287 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Console;
use Console_Table;
use Friendica\App;
use Friendica\Model\Contact as ContactModel;
use Friendica\Model\User as UserModel;
use Friendica\Util\Temporal;
use RuntimeException;
use Seld\CliPrompt\CliPrompt;
/**
* tool to manage contacts of users of the current node
*/
class Contact extends \Asika\SimpleConsole\Console
{
protected $helpOptions = ['h', 'help', '?'];
/**
* @var App\Mode
*/
private $appMode;
/**
* @var IPConfig
*/
private $pConfig;
protected function getHelp()
{
$help = <<<HELP
console contact - Modify contact settings per console commands.
Usage
bin/console contact add <user nick> <URL> [<network>] [-h|--help|-?] [-v]
bin/console contact remove <CID> [-h|--help|-?] [-v]
bin/console contact search id <CID> [-h|--help|-?] [-v]
bin/console contact search url <user nick> <URL> [-h|--help|-?] [-v]
bin/console contact terminate <CID> [-h|--help|-?] [-v]
Description
Modify contact settings per console commands.
Options
-h|--help|-? Show help information
-v Show more debug information
-y Non-interactive mode, assume "yes" as answer to the user deletion prompt
HELP;
return $help;
}
public function __construct(App\Mode $appMode, array $argv = null)
{
parent::__construct($argv);
$this->appMode = $appMode;
}
protected function doExecute()
{
if ($this->getOption('v')) {
$this->out('Class: ' . __CLASS__);
$this->out('Arguments: ' . var_export($this->args, true));
$this->out('Options: ' . var_export($this->options, true));
}
if (count($this->args) == 0) {
$this->out($this->getHelp());
return 0;
}
if ($this->appMode->isInstall()) {
throw new RuntimeException('Database isn\'t ready or populated yet');
}
$command = $this->getArgument(0);
switch ($command) {
case 'add':
return $this->addContact();
case 'remove':
return $this->removeContact();
case 'search':
return $this->searchContact();
case 'terminate':
return $this->terminateContact();
default:
throw new \Asika\SimpleConsole\CommandArgsException('Wrong command.');
}
}
/**
* Retrieves the user from a nick supplied as an argument or from a prompt
*
* @param int $arg_index Index of the nick argument in the arguments list
*
* @return array|boolean User record with uid field, or false if user is not found
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private function getUserByNick($arg_index)
{
$nick = $this->getArgument($arg_index);
if (empty($nick)) {
$this->out('Enter user nickname: ');
$nick = CliPrompt::prompt();
if (empty($nick)) {
throw new RuntimeException('A user nickname must be specified.');
}
}
$user = UserModel::getByNickname($nick, ['uid', 'nickname']);
if (empty($user)) {
throw new RuntimeException('User not found');
}
return $user;
}
/**
* Adds a contact to a user from a URL
*
* @return bool True, if the command was successful
*/
private function addContact()
{
$user = $this->getUserByNick(1);
$url = $this->getArgument(2);
if (empty($url)) {
$this->out('Enter contact URL: ');
$url = CliPrompt::prompt();
if (empty($url)) {
throw new RuntimeException('A contact URL must be specified.');
}
}
$contact = ContactModel::getByURLForUser($url, $user['uid']);
if (!empty($contact)) {
throw new RuntimeException('Contact already exists');
}
$network = $this->getArgument(3);
if ($network === null) {
$this->out('Enter network, or leave blank: ');
$network = CliPrompt::prompt();
}
$result = ContactModel::createFromProbe($user, $url, false, $network);
if ($result['success']) {
$this->out('User ' . $user['nickname'] . ' now connected to ' . $url . ', contact ID ' . $result['cid']);
} else {
throw new RuntimeException($result['message']);
}
}
/**
* Sends an unfriend message. Does not remove the contact
*
* @return bool True, if the command was successful
*/
private function terminateContact()
{
$cid = $this->getArgument(1);
if (empty($cid)) {
$this->out('Enter contact ID: ');
$cid = CliPrompt::prompt();
if (empty($cid)) {
throw new RuntimeException('A contact ID must be specified.');
}
}
$contact = ContactModel::getById($cid);
if (empty($contact)) {
throw new RuntimeException('Contact not found');
}
$user = UserModel::getById($contact['uid']);
$result = ContactModel::terminateFriendship($user, $contact);
}
/**
* Marks a contact for removal
*
* @return bool True, if the command was successful
*/
private function removeContact()
{
$cid = $this->getArgument(1);
if (empty($cid)) {
$this->out('Enter contact ID: ');
$cid = CliPrompt::prompt();
if (empty($cid)) {
throw new RuntimeException('A contact ID must be specified.');
}
}
$result = ContactModel::remove($cid);
}
/**
* Returns a contact based on search parameter
*
* @return bool True, if the command was successful
*/
private function searchContact()
{
$fields = [
'id',
'uid',
'network',
'name',
'nick',
'url',
'addr',
'created',
'updated',
'blocked',
'deleted',
];
$subCmd = $this->getArgument(1);
$table = new Console_Table();
$table->setHeaders(['ID', 'UID', 'Network', 'Name', 'Nick', 'URL', 'E-Mail', 'Created', 'Updated', 'Blocked', 'Deleted']);
$addRow = function ($row) use (&$table) {
$table->addRow([
$row['id'],
$row['uid'],
$row['network'],
$row['name'],
$row['nick'],
$row['url'],
$row['addr'],
Temporal::getRelativeDate($row['created']),
Temporal::getRelativeDate($row['updated']),
$row['blocked'],
$row['deleted'],
]);
};
switch ($subCmd) {
case 'id':
$cid = $this->getArgument(2);
$contact = ContactModel::getById($cid, $fields);
if (!empty($contact)) {
$addRow($contact);
}
break;
case 'url':
$user = $this->getUserByNick(2);
$url = $this->getArgument(3);
$contact = ContactModel::getByURLForUser($url, $user['uid'], false, $fields);
if (!empty($contact)) {
$addRow($contact);
}
break;
default:
$this->out($this->getHelp());
return false;
}
$this->out($table->getTable());
return true;
}
}

View file

@ -26,7 +26,6 @@ use Friendica\App;
use Friendica\Content\Pager; use Friendica\Content\Pager;
use Friendica\Core\L10n; use Friendica\Core\L10n;
use Friendica\Core\PConfig\IPConfig; use Friendica\Core\PConfig\IPConfig;
use Friendica\Database\Database;
use Friendica\Model\Register; use Friendica\Model\Register;
use Friendica\Model\User as UserModel; use Friendica\Model\User as UserModel;
use Friendica\Util\Temporal; use Friendica\Util\Temporal;
@ -49,9 +48,9 @@ class User extends \Asika\SimpleConsole\Console
*/ */
private $l10n; private $l10n;
/** /**
* @var Database * @var IPConfig
*/ */
private $dba; private $pConfig;
protected function getHelp() protected function getHelp()
{ {
@ -89,13 +88,13 @@ HELP;
return $help; return $help;
} }
public function __construct(App\Mode $appMode, L10n $l10n, Database $dba, array $argv = null) public function __construct(App\Mode $appMode, L10n $l10n, IPConfig $pConfig, array $argv = null)
{ {
parent::__construct($argv); parent::__construct($argv);
$this->appMode = $appMode; $this->appMode = $appMode;
$this->l10n = $l10n; $this->l10n = $l10n;
$this->dba = $dba; $this->pConfig = $pConfig;
} }
protected function doExecute() protected function doExecute()
@ -171,15 +170,15 @@ HELP;
* *
* @param int $arg_index Index of the nick argument in the arguments list * @param int $arg_index Index of the nick argument in the arguments list
* *
* @return mixed user data or dba failure result * @return array|boolean User record with uid field, or false if user is not found
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
private function getUserByNick($arg_index) private function getUserByNick($arg_index)
{ {
$nick = $this->getNick($arg_index); $nick = $this->getNick($arg_index);
$user = $this->dba->selectFirst('user', ['uid'], ['nickname' => $nick]); $user = UserModel::getByNickname($nick, ['uid']);
if (!$this->dba->isResult($user)) { if (empty($user)) {
throw new RuntimeException($this->l10n->t('User not found')); throw new RuntimeException($this->l10n->t('User not found'));
} }
@ -207,7 +206,7 @@ HELP;
try { try {
$result = UserModel::updatePassword($user['uid'], $password); $result = UserModel::updatePassword($user['uid'], $password);
if (!$this->dba->isResult($result)) { if (empty($result)) {
throw new \Exception($this->l10n->t('Password update failed. Please try again.')); throw new \Exception($this->l10n->t('Password update failed. Please try again.'));
} }
@ -338,8 +337,8 @@ HELP;
private function listUser() private function listUser()
{ {
$subCmd = $this->getArgument(1); $subCmd = $this->getArgument(1);
$start = $this->getOption(['s', 'start'], 0); $start = $this->getOption(['s', 'start'], 0);
$count = $this->getOption(['c', 'count'], Pager::ITEMS_PER_PAGE); $count = $this->getOption(['c', 'count'], Pager::ITEMS_PER_PAGE);
$table = new Console_Table(); $table = new Console_Table();
@ -403,7 +402,7 @@ HELP;
]; ];
$subCmd = $this->getArgument(1); $subCmd = $this->getArgument(1);
$param = $this->getArgument(2); $param = $this->getArgument(2);
$table = new Console_Table(); $table = new Console_Table();
$table->setHeaders(['UID', 'GUID', 'Name', 'Nick', 'E-Mail', 'Register', 'Login', 'Verified', 'Blocked']); $table->setHeaders(['UID', 'GUID', 'Name', 'Nick', 'E-Mail', 'Register', 'Login', 'Verified', 'Blocked']);
@ -426,7 +425,9 @@ HELP;
return false; return false;
} }
$table->addRow($user); if (!empty($user)) {
$table->addRow($user);
}
$this->out($table->getTable()); $this->out($table->getTable());
return true; return true;
@ -463,8 +464,7 @@ HELP;
} }
} }
$pconfig = \Friendica\DI::pConfig(); $values = $this->pConfig->load($user['uid'], $category);
$values = $pconfig->load($user['uid'], $category);
switch ($subCmd) { switch ($subCmd) {
case 'list': case 'list':
@ -485,7 +485,7 @@ HELP;
throw new RuntimeException('Key does not exist'); throw new RuntimeException('Key does not exist');
} }
$this->out($pconfig->get($user['uid'], $category, $key)); $this->out($this->pConfig->get($user['uid'], $category, $key));
break; break;
case 'set': case 'set':
$value = $this->getArgument(5); $value = $this->getArgument(5);
@ -499,12 +499,12 @@ HELP;
} }
if (array_key_exists($category, $values) and if (array_key_exists($category, $values) and
array_key_exists($key, $values[$category]) and array_key_exists($key, $values[$category]) and
$values[$category][$key] == $value) { $values[$category][$key] == $value) {
throw new RuntimeException('Value not changed'); throw new RuntimeException('Value not changed');
} }
$pconfig->set($user['uid'], $category, $key, $value); $this->pConfig->set($user['uid'], $category, $key, $value);
break; break;
case 'delete': case 'delete':
if (!array_key_exists($category, $values)) { if (!array_key_exists($category, $values)) {
@ -514,7 +514,7 @@ HELP;
throw new RuntimeException('Key does not exist'); throw new RuntimeException('Key does not exist');
} }
$pconfig->delete($user['uid'], $category, $key); $this->pConfig->delete($user['uid'], $category, $key);
break; break;
default: default:
$this->out($this->getHelp()); $this->out($this->getHelp());

View file

@ -144,9 +144,9 @@ class Nav
* array 'userinfo' => Array of user information (name, icon) * array 'userinfo' => Array of user information (name, icon)
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
private static function getInfo(App $a) private static function getInfo(App $a): array
{ {
$ssl_state = ((local_user()) ? true : false); $ssl_state = (bool) local_user();
/* /*
* Our network is distributed, and as you visit friends some of the * Our network is distributed, and as you visit friends some of the
@ -158,13 +158,27 @@ class Nav
$sitelocation = $myident . substr(DI::baseUrl()->get($ssl_state), strpos(DI::baseUrl()->get($ssl_state), '//') + 2); $sitelocation = $myident . substr(DI::baseUrl()->get($ssl_state), strpos(DI::baseUrl()->get($ssl_state), '//') + 2);
// nav links: array of array('href', 'text', 'extra css classes', 'title') $nav = [
$nav = []; 'admin' => null,
'apps' => null,
'community' => null,
'home' => null,
'events' => null,
'login' => null,
'logout' => null,
'langselector' => null,
'messages' => null,
'network' => null,
'notifications' => null,
'remote' => null,
'search' => null,
'usermenu' => [],
];
// Display login or logout // Display login or logout
$nav['usermenu'] = [];
$userinfo = null; $userinfo = null;
// nav links: array of array('href', 'text', 'extra css classes', 'title')
if (Session::isAuthenticated()) { if (Session::isAuthenticated()) {
$nav['logout'] = ['logout', DI::l10n()->t('Logout'), '', DI::l10n()->t('End this session')]; $nav['logout'] = ['logout', DI::l10n()->t('Logout'), '', DI::l10n()->t('End this session')];
} else { } else {
@ -297,13 +311,15 @@ class Nav
$banner = '<a href="https://friendi.ca"><img id="logo-img" src="images/friendica-32.png" alt="logo" /></a><span id="logo-text"><a href="https://friendi.ca">Friendica</a></span>'; $banner = '<a href="https://friendi.ca"><img id="logo-img" src="images/friendica-32.png" alt="logo" /></a><span id="logo-text"><a href="https://friendi.ca">Friendica</a></span>';
} }
Hook::callAll('nav_info', $nav); $nav_info = [
'banner' => $banner,
return [ 'nav' => $nav,
'sitelocation' => $sitelocation, 'sitelocation' => $sitelocation,
'nav' => $nav, 'userinfo' => $userinfo,
'banner' => $banner,
'userinfo' => $userinfo,
]; ];
Hook::callAll('nav_info', $nav_info);
return $nav_info;
} }
} }

View file

@ -253,10 +253,15 @@ class PageInfo
// Fix for Mastodon where the mentions are in a different format // Fix for Mastodon where the mentions are in a different format
$body = preg_replace("~\[url=($URLSearchString)]([#!@])(.*?)\[/url]~is", '$2[url=$1]$3[/url]', $body); $body = preg_replace("~\[url=($URLSearchString)]([#!@])(.*?)\[/url]~is", '$2[url=$1]$3[/url]', $body);
preg_match("~(?<![!#@])\[url]($URLSearchString)\[/url]$~is", $body, $matches); // Remove all hashtags and mentions
$body = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '', $body);
// Search for pure links
preg_match("/\[url\](https?:.*?)\[\/url\]/ism", $body, $matches);
if (!$matches) { if (!$matches) {
preg_match("~(?<![!#@])\[url=($URLSearchString)].*\[/url]$~is", $body, $matches); // Search for links with descriptions
preg_match("/\[url\=(https?:.*?)\].*?\[\/url\]/ism", $body, $matches);
} }
if (!$matches && $searchNakedUrls) { if (!$matches && $searchNakedUrls) {

View file

@ -50,9 +50,10 @@ use Friendica\Util\XML;
class BBCode class BBCode
{ {
// Update this value to the current date whenever changes are made to BBCode::convert // Update this value to the current date whenever changes are made to BBCode::convert
const VERSION = '2021-04-24'; const VERSION = '2021-05-01';
const INTERNAL = 0; const INTERNAL = 0;
const EXTERNAL = 1;
const API = 2; const API = 2;
const DIASPORA = 3; const DIASPORA = 3;
const CONNECTORS = 4; const CONNECTORS = 4;
@ -61,7 +62,8 @@ class BBCode
const BACKLINK = 8; const BACKLINK = 8;
const ACTIVITYPUB = 9; const ACTIVITYPUB = 9;
const ANCHOR = '<br class="anchor">'; const TOP_ANCHOR = '<br class="top-anchor">';
const BOTTOM_ANCHOR = '<br class="button-anchor">';
/** /**
* Fetches attachment data that were generated the old way * Fetches attachment data that were generated the old way
* *
@ -408,7 +410,7 @@ class BBCode
*/ */
public static function removeAttachment($body, $no_link_desc = false) public static function removeAttachment($body, $no_link_desc = false)
{ {
return preg_replace_callback("/\s*\[attachment (.*)\](.*?)\[\/attachment\]\s*/ism", return preg_replace_callback("/\s*\[attachment (.*?)\](.*?)\[\/attachment\]\s*/ism",
function ($match) use ($no_link_desc) { function ($match) use ($no_link_desc) {
$attach_data = self::getAttachmentData($match[0]); $attach_data = self::getAttachmentData($match[0]);
if (empty($attach_data['url'])) { if (empty($attach_data['url'])) {
@ -1084,12 +1086,12 @@ class BBCode
'$avatar' => $attributes['avatar'], '$avatar' => $attributes['avatar'],
'$author' => $attributes['author'], '$author' => $attributes['author'],
'$link' => $attributes['link'], '$link' => $attributes['link'],
'$link_title' => DI::l10n()->t('link to source'), '$link_title' => DI::l10n()->t('Link to source'),
'$posted' => $attributes['posted'], '$posted' => $attributes['posted'],
'$guid' => $attributes['guid'], '$guid' => $attributes['guid'],
'$network_name' => ContactSelector::networkToName($network, $attributes['profile']), '$network_name' => ContactSelector::networkToName($network, $attributes['profile']),
'$network_icon' => ContactSelector::networkToIcon($network, $attributes['profile']), '$network_icon' => ContactSelector::networkToIcon($network, $attributes['profile']),
'$content' => self::setMentions(trim($content), 0, $network) . self::ANCHOR, '$content' => self::TOP_ANCHOR . self::setMentions(trim($content), 0, $network) . self::BOTTOM_ANCHOR,
]); ]);
break; break;
} }
@ -1103,20 +1105,14 @@ class BBCode
$text = DI::cache()->get($cache_key); $text = DI::cache()->get($cache_key);
if (is_null($text)) { if (is_null($text)) {
$a = DI::app(); $curlResult = DI::httpRequest()->head($match[1], ['timeout' => DI::config()->get('system', 'xrd_timeout')]);
if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getHeader('Content-Type');
} else {
$mimetype = '';
}
$stamp1 = microtime(true); if (substr($mimetype, 0, 6) == 'image/') {
$ch = @curl_init($match[1]);
@curl_setopt($ch, CURLOPT_NOBODY, true);
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@curl_setopt($ch, CURLOPT_USERAGENT, DI::httpRequest()->getUserAgent());
@curl_exec($ch);
$curl_info = @curl_getinfo($ch);
DI::profiler()->saveTimestamp($stamp1, "network");
if (substr($curl_info['content_type'], 0, 6) == 'image/') {
$text = "[url=" . $match[1] . ']' . $match[1] . "[/url]"; $text = "[url=" . $match[1] . ']' . $match[1] . "[/url]";
} else { } else {
$text = "[url=" . $match[2] . ']' . $match[2] . "[/url]"; $text = "[url=" . $match[2] . ']' . $match[2] . "[/url]";
@ -1180,20 +1176,15 @@ class BBCode
return $text; return $text;
} }
// Only fetch the header, not the content $curlResult = DI::httpRequest()->head($match[1], ['timeout' => DI::config()->get('system', 'xrd_timeout')]);
$stamp1 = microtime(true); if ($curlResult->isSuccess()) {
$mimetype = $curlResult->getHeader('Content-Type');
$ch = @curl_init($match[1]); } else {
@curl_setopt($ch, CURLOPT_NOBODY, true); $mimetype = '';
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); }
@curl_setopt($ch, CURLOPT_USERAGENT, DI::httpRequest()->getUserAgent());
@curl_exec($ch);
$curl_info = @curl_getinfo($ch);
DI::profiler()->saveTimestamp($stamp1, "network");
// if its a link to a picture then embed this picture // if its a link to a picture then embed this picture
if (substr($curl_info['content_type'], 0, 6) == 'image/') { if (substr($mimetype, 0, 6) == 'image/') {
$text = '[img]' . $match[1] . '[/img]'; $text = '[img]' . $match[1] . '[/img]';
} else { } else {
if (!empty($match[3])) { if (!empty($match[3])) {
@ -1396,7 +1387,7 @@ class BBCode
// Handle attached links or videos // Handle attached links or videos
if ($simple_html == self::ACTIVITYPUB) { if ($simple_html == self::ACTIVITYPUB) {
$text = self::removeAttachment($text); $text = self::removeAttachment($text);
} elseif (!in_array($simple_html, [self::INTERNAL, self::CONNECTORS])) { } elseif (!in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::CONNECTORS])) {
$text = self::removeAttachment($text, true); $text = self::removeAttachment($text, true);
} else { } else {
$text = self::convertAttachment($text, $simple_html, $try_oembed); $text = self::convertAttachment($text, $simple_html, $try_oembed);
@ -1406,9 +1397,9 @@ class BBCode
$text = str_replace('[nosmile]', '', $text); $text = str_replace('[nosmile]', '', $text);
// Replace non graphical smilies for external posts // Replace non graphical smilies for external posts
if (!$nosmile && !$for_plaintext) { if (!$nosmile) {
$text = self::performWithEscapedTags($text, ['img'], function ($text) { $text = self::performWithEscapedTags($text, ['img'], function ($text) use ($simple_html, $for_plaintext) {
return Smilies::replace($text); return Smilies::replace($text, ($simple_html != self::INTERNAL) || $for_plaintext);
}); });
} }
@ -1730,7 +1721,7 @@ class BBCode
$text); $text);
} elseif (!$simple_html) { } elseif (!$simple_html) {
$text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", $text = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'$1<a href="$2" class="userinfo mention" title="$3">$3</a>', '$1<a href="$2" class="userinfo mention" title="$3"><bdi>$3</bdi></a>',
$text); $text);
} }
@ -1902,7 +1893,7 @@ class BBCode
$text = HTML::purify($text, $allowedIframeDomains); $text = HTML::purify($text, $allowedIframeDomains);
return $text; return trim($text);
} }
/** /**

View file

@ -47,6 +47,7 @@ Commands:
addon Addon management addon Addon management
cache Manage node cache cache Manage node cache
config Edit site config config Edit site config
contact Contact management
createdoxygen Generate Doxygen headers createdoxygen Generate Doxygen headers
dbstructure Do database updates dbstructure Do database updates
docbloxerrorchecker Check the file tree for DocBlox errors docbloxerrorchecker Check the file tree for DocBlox errors
@ -78,6 +79,7 @@ HELP;
'addon' => Friendica\Console\Addon::class, 'addon' => Friendica\Console\Addon::class,
'cache' => Friendica\Console\Cache::class, 'cache' => Friendica\Console\Cache::class,
'config' => Friendica\Console\Config::class, 'config' => Friendica\Console\Config::class,
'contact' => Friendica\Console\Contact::class,
'createdoxygen' => Friendica\Console\CreateDoxygen::class, 'createdoxygen' => Friendica\Console\CreateDoxygen::class,
'docbloxerrorchecker' => Friendica\Console\DocBloxErrorChecker::class, 'docbloxerrorchecker' => Friendica\Console\DocBloxErrorChecker::class,
'dbstructure' => Friendica\Console\DatabaseStructure::class, 'dbstructure' => Friendica\Console\DatabaseStructure::class,

View file

@ -35,7 +35,7 @@ interface IPConfig
* @param int $uid The user_id * @param int $uid The user_id
* @param string $cat The category of the configuration value * @param string $cat The category of the configuration value
* *
* @return void * @return array The loaded config array
* @see Cache * @see Cache
* *
*/ */

View file

@ -68,6 +68,8 @@ class PreloadPConfig extends BasePConfig
// load the whole category out of the DB into the cache // load the whole category out of the DB into the cache
$this->configCache->load($uid, $config); $this->configCache->load($uid, $config);
return $config;
} }
/** /**

View file

@ -134,7 +134,7 @@ class Search
$profiles = $results['profiles'] ?? []; $profiles = $results['profiles'] ?? [];
foreach ($profiles as $profile) { foreach ($profiles as $profile) {
$profile_url = $profile['url'] ?? ''; $profile_url = $profile['profile_url'] ?? '';
$contactDetails = Contact::getByURLForUser($profile_url, local_user()); $contactDetails = Contact::getByURLForUser($profile_url, local_user());
$result = new ContactResult( $result = new ContactResult(

View file

@ -239,6 +239,14 @@ abstract class DI
return self::$dice->create(Factory\Api\Mastodon\Account::class); return self::$dice->create(Factory\Api\Mastodon\Account::class);
} }
/**
* @return Factory\Api\Mastodon\Application
*/
public static function mstdnApplication()
{
return self::$dice->create(Factory\Api\Mastodon\Application::class);
}
/** /**
* @return Factory\Api\Mastodon\Attachment * @return Factory\Api\Mastodon\Attachment
*/ */
@ -247,6 +255,14 @@ abstract class DI
return self::$dice->create(Factory\Api\Mastodon\Attachment::class); return self::$dice->create(Factory\Api\Mastodon\Attachment::class);
} }
/**
* @return Factory\Api\Mastodon\Card
*/
public static function mstdnCard()
{
return self::$dice->create(Factory\Api\Mastodon\Card::class);
}
/** /**
* @return Factory\Api\Mastodon\Emoji * @return Factory\Api\Mastodon\Emoji
*/ */
@ -295,6 +311,14 @@ abstract class DI
return self::$dice->create(Factory\Api\Mastodon\Status::class); return self::$dice->create(Factory\Api\Mastodon\Status::class);
} }
/**
* @return Factory\Api\Mastodon\ListEntity
*/
public static function mstdnList()
{
return self::$dice->create(Factory\Api\Mastodon\ListEntity::class);
}
/** /**
* @return Factory\Api\Mastodon\Mention * @return Factory\Api\Mastodon\Mention
*/ */
@ -303,6 +327,14 @@ abstract class DI
return self::$dice->create(Factory\Api\Mastodon\Mention::class); return self::$dice->create(Factory\Api\Mastodon\Mention::class);
} }
/**
* @return Factory\Api\Mastodon\Notification
*/
public static function mstdnNotification()
{
return self::$dice->create(Factory\Api\Mastodon\Notification::class);
}
/** /**
* @return Factory\Api\Mastodon\Tag * @return Factory\Api\Mastodon\Tag
*/ */

View file

@ -0,0 +1,49 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory;
use Friendica\Database\DBA;
class Application extends BaseFactory
{
/**
* @param int $id Application ID
*/
public function createFromApplicationId(int $id)
{
$application = DBA::selectFirst('application', ['client_id', 'client_secret', 'id', 'name', 'redirect_uri', 'website'], ['id' => $id]);
if (!DBA::isResult($application)) {
return [];
}
$object = new \Friendica\Object\Api\Mastodon\Application(
$application['name'],
$application['client_id'],
$application['client_secret'],
$application['id'],
$application['redirect_uri'],
$application['website']);
return $object->toArray();
}
}

View file

@ -23,9 +23,12 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\App\BaseURL; use Friendica\App\BaseURL;
use Friendica\BaseFactory; use Friendica\BaseFactory;
use Friendica\DI;
use Friendica\Model\Photo;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Repository\ProfileField; use Friendica\Repository\ProfileField;
use Friendica\Util\Images;
use Friendica\Util\Proxy; use Friendica\Util\Proxy;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -56,7 +59,7 @@ class Attachment extends BaseFactory
public function createFromUriId(int $uriId) public function createFromUriId(int $uriId)
{ {
$attachments = []; $attachments = [];
foreach (Post\Media::getByURIId($uriId) as $attachment) { foreach (Post\Media::getByURIId($uriId, [Post\Media::AUDIO, Post\Media::VIDEO, Post\Media::IMAGE]) as $attachment) {
$filetype = !empty($attachment['mimetype']) ? strtolower(substr($attachment['mimetype'], 0, strpos($attachment['mimetype'], '/'))) : ''; $filetype = !empty($attachment['mimetype']) ? strtolower(substr($attachment['mimetype'], 0, strpos($attachment['mimetype'], '/'))) : '';
@ -93,4 +96,36 @@ class Attachment extends BaseFactory
return $attachments; return $attachments;
} }
/**
* @param int $id id of the photo
* @return array
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public function createFromPhoto(int $id)
{
$photo = Photo::selectFirst(['resource-id', 'uid', 'id', 'title', 'type'], ['id' => $id]);
if (empty($photo)) {
return null;
}
$attachment = ['id' => $photo['id'], 'description' => $photo['title']];
$phototypes = Images::supportedTypes();
$ext = $phototypes[$photo['type']];
$url = DI::baseUrl() . '/photo/' . $photo['resource-id'] . '-0.' . $ext;
$preview = Photo::selectFirst(['scale'], ["`resource-id` = ? AND `uid` = ? AND `scale` > ?", $photo['resource-id'], $photo['uid'], 0], ['order' => ['scale']]);
if (empty($scale)) {
$preview_url = DI::baseUrl() . '/photo/' . $photo['resource-id'] . '-' . $preview['scale'] . '.' . $ext;
} else {
$preview_url = '';
}
$object = new \Friendica\Object\Api\Mastodon\Attachment($attachment, 'image', $url, $preview_url, '');
return $object->toArray();
}
} }

View file

@ -0,0 +1,80 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory;
use Friendica\Content\Text\BBCode;
use Friendica\Model\Post;
use Friendica\Network\HTTPException;
use Friendica\Util\Strings;
class Card extends BaseFactory
{
/**
* @param int $uriId Uri-ID of the item
* @return \Friendica\Object\Api\Mastodon\Card
* @throws HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public function createFromUriId(int $uriId)
{
$item = Post::selectFirst(['body'], ['uri-id' => $uriId]);
if (!empty($item['body'])) {
$data = BBCode::getAttachmentData($item['body']);
} else {
$data = [];
}
foreach (Post\Media::getByURIId($uriId, [Post\Media::HTML]) as $attached) {
if ((empty($data['url']) || Strings::compareLink($data['url'], $attached['url'])) &&
(!empty($attached['description']) || !empty($attached['image']) || !empty($attached['preview']))) {
$parts = parse_url($attached['url']);
if (!empty($parts['scheme']) && !empty($parts['host'])) {
if (empty($attached['publisher-name'])) {
$attached['publisher-name'] = $parts['host'];
}
if (empty($attached['publisher-url']) || empty(parse_url($attached['publisher-url'], PHP_URL_SCHEME))) {
$attached['publisher-url'] = $parts['scheme'] . '://' . $parts['host'];
if (!empty($parts['port'])) {
$attached['publisher-url'] .= ':' . $parts['port'];
}
}
}
$data['url'] = $attached['url'];
$data['title'] = $attached['name'];
$data['description'] = $attached['description'];
$data['type'] = 'link';
$data['author_name'] = $attached['author-name'];
$data['author_url'] = $attached['author-url'];
$data['provider_name'] = $attached['publisher-name'];
$data['provider_url'] = $attached['publisher-url'];
$data['image'] = $attached['preview'];
$data['width'] = $attached['preview-width'];
$data['height'] = $attached['preview-height'];
}
}
return new \Friendica\Object\Api\Mastodon\Card($data);
}
}

View file

@ -35,4 +35,31 @@ class Error extends BaseFactory
System::jsonError(404, $errorobj->toArray()); System::jsonError(404, $errorobj->toArray());
} }
public function UnprocessableEntity(string $error = '')
{
$error = $error ?: DI::l10n()->t('Unprocessable Entity');
$error_description = '';
$errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
System::jsonError(422, $errorobj->toArray());
}
public function Unauthorized(string $error = '')
{
$error = $error ?: DI::l10n()->t('Unauthorized');
$error_description = '';
$errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
System::jsonError(401, $errorobj->toArray());
}
public function InternalError(string $error = '')
{
$error = $error ?: DI::l10n()->t('Internal Server Error');
$error_description = '';
$errorobj = New \Friendica\Object\Api\Mastodon\Error($error, $error_description);
System::jsonError(500, $errorobj->toArray());
}
} }

View file

@ -0,0 +1,34 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory;
use Friendica\Database\DBA;
class ListEntity extends BaseFactory
{
public function createFromGroupId(int $id)
{
$group = DBA::selectFirst('group', ['name'], ['id' => $id, 'deleted' => false]);
return new \Friendica\Object\Api\Mastodon\ListEntity($id, $group['name'] ?? '', 'list');
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Factory\Api\Mastodon;
use Friendica\BaseFactory;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Notification as ModelNotification;
class Notification extends BaseFactory
{
public function createFromNotifyId(int $id)
{
$notification = DBA::selectFirst('notify', [], ['id' => $id]);
if (!DBA::isResult($notification)) {
return null;
}
$cid = Contact::getIdForURL($notification['url'], 0, false);
if (empty($cid)) {
return null;
}
/*
follow = Someone followed you
follow_request = Someone requested to follow you
mention = Someone mentioned you in their status
reblog = Someone boosted one of your statuses
favourite = Someone favourited one of your statuses
poll = A poll you have voted in or created has ended
status = Someone you enabled notifications for has posted a status
*/
switch ($notification['type']) {
case ModelNotification\Type::INTRO:
$type = 'follow_request';
break;
case ModelNotification\Type::WALL:
case ModelNotification\Type::COMMENT:
case ModelNotification\Type::MAIL:
case ModelNotification\Type::TAG_SELF:
case ModelNotification\Type::POKE:
$type = 'mention';
break;
case ModelNotification\Type::SHARE:
$type = 'status';
break;
default:
return null;
}
$account = DI::mstdnAccount()->createFromContactId($cid);
if (!empty($notification['uri-id'])) {
try {
$status = DI::mstdnStatus()->createFromUriId($notification['uri-id'], $notification['uid']);
} catch (\Throwable $th) {
$status = null;
}
} else {
$status = null;
}
return new \Friendica\Object\Api\Mastodon\Notification($id, $type, $notification['date'], $account, $status);
}
}

View file

@ -23,6 +23,7 @@ namespace Friendica\Factory\Api\Mastodon;
use Friendica\App\BaseURL; use Friendica\App\BaseURL;
use Friendica\BaseFactory; use Friendica\BaseFactory;
use Friendica\Content\ContactSelector;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
@ -60,9 +61,9 @@ class Status extends BaseFactory
*/ */
public function createFromUriId(int $uriId, $uid = 0) public function createFromUriId(int $uriId, $uid = 0)
{ {
$fields = ['uri-id', 'uid', 'author-id', 'starred', 'app', 'title', 'body', 'raw-body', 'created', $fields = ['uri-id', 'uid', 'author-id', 'author-link', 'starred', 'app', 'title', 'body', 'raw-body', 'created', 'network',
'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity']; 'thr-parent-id', 'parent-author-id', 'language', 'uri', 'plink', 'private', 'vid', 'gravity'];
$item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => $uid]); $item = Post::selectFirst($fields, ['uri-id' => $uriId, 'uid' => [0, $uid]], ['order' => ['uid' => true]]);
if (!$item) { if (!$item) {
throw new HTTPException\NotFoundException('Item with URI ID ' . $uriId . 'not found' . ($uid ? ' for user ' . $uid : '.')); throw new HTTPException\NotFoundException('Item with URI ID ' . $uriId . 'not found' . ($uid ? ' for user ' . $uid : '.'));
} }
@ -70,32 +71,46 @@ class Status extends BaseFactory
$account = DI::mstdnAccount()->createFromContactId($item['author-id']); $account = DI::mstdnAccount()->createFromContactId($item['author-id']);
$counts = new \Friendica\Object\Api\Mastodon\Status\Counts( $counts = new \Friendica\Object\Api\Mastodon\Status\Counts(
Post::count(['thr-parent-id' => $uriId, 'uid' => $uid, 'gravity' => GRAVITY_COMMENT]), Post::count(['thr-parent-id' => $uriId, 'gravity' => GRAVITY_COMMENT], [], false),
Post::count(['thr-parent-id' => $uriId, 'uid' => $uid, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE)]), Post::count(['thr-parent-id' => $uriId, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE)], [], false),
Post::count(['thr-parent-id' => $uriId, 'uid' => $uid, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE)]) Post::count(['thr-parent-id' => $uriId, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE)], [], false)
); );
$userAttributes = new \Friendica\Object\Api\Mastodon\Status\UserAttributes( $userAttributes = new \Friendica\Object\Api\Mastodon\Status\UserAttributes(
Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE)]), Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::LIKE)]),
Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE)]), Post::exists(['thr-parent-id' => $uriId, 'uid' => $uid, 'origin' => true, 'gravity' => GRAVITY_ACTIVITY, 'vid' => Verb::getID(Activity::ANNOUNCE)]),
Post\ThreadUser::getIgnored($uriId, $item['uid']), Post\ThreadUser::getIgnored($uriId, $uid),
(bool)$item['starred'], (bool)$item['starred'],
Post\ThreadUser::getPinned($uriId, $item['uid']) Post\ThreadUser::getPinned($uriId, $uid)
); );
$sensitive = DBA::exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw']); $sensitive = DBA::exists('tag-view', ['uri-id' => $uriId, 'name' => 'nsfw']);
$application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?? ''); $application = new \Friendica\Object\Api\Mastodon\Application($item['app'] ?: ContactSelector::networkToName($item['network'], $item['author-link']));
$mentions = DI::mstdnMention()->createFromUriId($uriId);
$tags = DI::mstdnTag()->createFromUriId($uriId);
$data = BBCode::getAttachmentData($item['body']);
$card = new \Friendica\Object\Api\Mastodon\Card($data);
$mentions = DI::mstdnMention()->createFromUriId($uriId);
$tags = DI::mstdnTag()->createFromUriId($uriId);
$card = DI::mstdnCard()->createFromUriId($uriId);
$attachments = DI::mstdnAttachment()->createFromUriId($uriId); $attachments = DI::mstdnAttachment()->createFromUriId($uriId);
$shared = BBCode::fetchShareAttributes($item['body']);
if (!empty($shared['guid'])) {
$shared_item = Post::selectFirst(['uri-id', 'plink'], ['guid' => $shared['guid']]);
$shared_uri_id = $shared_item['uri-id'] ?? 0;
$mentions = array_merge($mentions, DI::mstdnMention()->createFromUriId($shared_uri_id));
$tags = array_merge($tags, DI::mstdnTag()->createFromUriId($shared_uri_id));
$attachments = array_merge($attachments, DI::mstdnAttachment()->createFromUriId($shared_uri_id));
if (empty($card->toArray())) {
$card = DI::mstdnCard()->createFromUriId($shared_uri_id);
}
}
if ($item['vid'] == Verb::getID(Activity::ANNOUNCE)) { if ($item['vid'] == Verb::getID(Activity::ANNOUNCE)) {
$reshare = $this->createFromUriId($item['thr-parent-id'], $uid)->toArray(); $reshare = $this->createFromUriId($item['thr-parent-id'], $uid)->toArray();
$reshared_item = Post::selectFirst(['title', 'body'], ['uri-id' => $item['thr-parent-id'], 'uid' => $uid]); $reshared_item = Post::selectFirst(['title', 'body'], ['uri-id' => $item['thr-parent-id'], 'uid' => [0, $uid]]);
$item['title'] = $reshared_item['title'] ?? $item['title']; $item['title'] = $reshared_item['title'] ?? $item['title'];
$item['body'] = $reshared_item['body'] ?? $item['body']; $item['body'] = $reshared_item['body'] ?? $item['body'];
} else { } else {

View file

@ -235,7 +235,7 @@ class APContact
unset($parts['path']); unset($parts['path']);
if (empty($apcontact['addr'])) { if (empty($apcontact['addr'])) {
if (!empty($apcontact['nick'])) { if (!empty($apcontact['nick']) && is_array($parts)) {
$apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); $apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts));
} else { } else {
$apcontact['addr'] = ''; $apcontact['addr'] = '';

View file

@ -264,7 +264,7 @@ class Contact
$condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, $uid]; $condition = ['`alias` IN (?, ?, ?) AND `uid` = ? AND NOT `deleted`', $url, Strings::normaliseLink($url), $ssl_url, $uid];
$contact = DBA::selectFirst('contact', $fields, $condition, $options); $contact = DBA::selectFirst('contact', $fields, $condition, $options);
} }
if (!DBA::isResult($contact)) { if (!DBA::isResult($contact)) {
return []; return [];
} }
@ -307,7 +307,7 @@ class Contact
} }
$contact = self::getByURL($url, $update, $fields); $contact = self::getByURL($url, $update, $fields);
if (!empty($contact['id'])) { if (!empty($contact['id'])) {
$contact['cid'] = 0; $contact['cid'] = 0;
$contact['zid'] = $contact['id']; $contact['zid'] = $contact['id'];
} }
@ -1274,7 +1274,7 @@ class Contact
* *
* @param string $contact_url Contact URL * @param string $contact_url Contact URL
* @param bool $thread_mode * @param bool $thread_mode
* @param int $update Update mode * @param int $update Update mode
* @param int $parent Item parent ID for the update mode * @param int $parent Item parent ID for the update mode
* @return string posts in HTML * @return string posts in HTML
* @throws \Exception * @throws \Exception
@ -1289,7 +1289,7 @@ class Contact
* *
* @param int $cid Contact ID * @param int $cid Contact ID
* @param bool $thread_mode * @param bool $thread_mode
* @param int $update Update mode * @param int $update Update mode
* @param int $parent Item parent ID for the update mode * @param int $parent Item parent ID for the update mode
* @return string posts in HTML * @return string posts in HTML
* @throws \Exception * @throws \Exception
@ -1347,7 +1347,7 @@ class Contact
$o = ''; $o = '';
} }
if ($thread_mode) { if ($thread_mode) {
$items = Post::toArray(Post::selectForUser(local_user(), ['uri-id', 'gravity', 'parent-uri-id', 'thr-parent-id', 'author-id'], $condition, $params)); $items = Post::toArray(Post::selectForUser(local_user(), ['uri-id', 'gravity', 'parent-uri-id', 'thr-parent-id', 'author-id'], $condition, $params));
$o .= conversation($a, $items, 'contacts', $update, false, 'commented', local_user()); $o .= conversation($a, $items, 'contacts', $update, false, 'commented', local_user());
@ -1607,12 +1607,12 @@ class Contact
$avatar['size'] = 48; $avatar['size'] = 48;
$default = self::DEFAULT_AVATAR_MICRO; $default = self::DEFAULT_AVATAR_MICRO;
break; break;
case Proxy::SIZE_THUMB: case Proxy::SIZE_THUMB:
$avatar['size'] = 80; $avatar['size'] = 80;
$default = self::DEFAULT_AVATAR_THUMB; $default = self::DEFAULT_AVATAR_THUMB;
break; break;
case Proxy::SIZE_SMALL: case Proxy::SIZE_SMALL:
default: default:
$avatar['size'] = 300; $avatar['size'] = 300;
@ -1720,7 +1720,7 @@ class Contact
$contact['thumb'] ?? '', $contact['thumb'] ?? '',
$contact['micro'] ?? '', $contact['micro'] ?? '',
]; ];
foreach ($data as $image_uri) { foreach ($data as $image_uri) {
$image_rid = Photo::ridFromURI($image_uri); $image_rid = Photo::ridFromURI($image_uri);
if ($image_rid && !Photo::exists(['resource-id' => $image_rid, 'uid' => $uid])) { if ($image_rid && !Photo::exists(['resource-id' => $image_rid, 'uid' => $uid])) {
@ -2027,7 +2027,7 @@ class Contact
if (Contact\Relation::isDiscoverable($ret['url'])) { if (Contact\Relation::isDiscoverable($ret['url'])) {
Worker::add(PRIORITY_LOW, 'ContactDiscovery', $ret['url']); Worker::add(PRIORITY_LOW, 'ContactDiscovery', $ret['url']);
} }
// Update the public contact // Update the public contact
if ($uid != 0) { if ($uid != 0) {
$contact = self::getByURL($ret['url'], false, ['id']); $contact = self::getByURL($ret['url'], false, ['id']);
@ -2763,11 +2763,12 @@ class Contact
* *
* @param string $search Name or nick * @param string $search Name or nick
* @param string $mode Search mode (e.g. "community") * @param string $mode Search mode (e.g. "community")
* @param int $uid User ID
* *
* @return array with search results * @return array with search results
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function searchByName($search, $mode = '') public static function searchByName(string $search, string $mode = '', int $uid = 0)
{ {
if (empty($search)) { if (empty($search)) {
return []; return [];
@ -2800,7 +2801,7 @@ class Contact
NOT `failed` AND `uid` = ? AND NOT `failed` AND `uid` = ? AND
(`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql
ORDER BY `nurl` DESC LIMIT 1000", ORDER BY `nurl` DESC LIMIT 1000",
Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, 0, $search, $search, $search Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, $uid, $search, $search, $search
); );
$contacts = DBA::toArray($results); $contacts = DBA::toArray($results);

View file

@ -21,7 +21,6 @@
namespace Friendica\Model; namespace Friendica\Model;
use Friendica\Content\PageInfo;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML; use Friendica\Content\Text\HTML;
use Friendica\Core\Hook; use Friendica\Core\Hook;
@ -173,7 +172,7 @@ class Item
Logger::info('Updating per single row method', ['fields' => $fields, 'condition' => $condition]); Logger::info('Updating per single row method', ['fields' => $fields, 'condition' => $condition]);
$items = Post::select(['id', 'origin', 'uri-id', 'uid'], $condition); $items = Post::select(['id', 'origin', 'uri-id', 'uid', 'author-network'], $condition);
$notify_items = []; $notify_items = [];
@ -181,9 +180,14 @@ class Item
if (!empty($fields['body'])) { if (!empty($fields['body'])) {
Post\Media::insertFromAttachmentData($item['uri-id'], $fields['body']); Post\Media::insertFromAttachmentData($item['uri-id'], $fields['body']);
if ($item['author-network'] != Protocol::DFRN) {
Post\Media::insertFromRelevantUrl($item['uri-id'], $fields['body']);
}
$content_fields = ['raw-body' => trim($fields['raw-body'] ?? $fields['body'])]; $content_fields = ['raw-body' => trim($fields['raw-body'] ?? $fields['body'])];
// Remove all media attachments from the body and store them in the post-media table // Remove all media attachments from the body and store them in the post-media table
// @todo On shared postings (Diaspora style and commented reshare) don't fetch content from the shared part
$content_fields['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']); $content_fields['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $content_fields['raw-body']);
$content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']); $content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']);
} }
@ -959,8 +963,20 @@ class Item
self::setOwnerforResharedItem($item); self::setOwnerforResharedItem($item);
} }
if (isset($item['attachments'])) {
foreach ($item['attachments'] as $attachment) {
$attachment['uri-id'] = $item['uri-id'];
Post\Media::insert($attachment);
}
unset($item['attachments']);
}
Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']); Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']);
if (!DBA::exists('contact', ['id' => $item['author-id'], 'network' => Protocol::DFRN])) {
Post\Media::insertFromRelevantUrl($item['uri-id'], $item['body']);
}
// Remove all media attachments from the body and store them in the post-media table // Remove all media attachments from the body and store them in the post-media table
$item['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $item['raw-body']); $item['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $item['raw-body']);
$item['raw-body'] = self::setHashtags($item['raw-body']); $item['raw-body'] = self::setHashtags($item['raw-body']);
@ -1706,7 +1722,7 @@ class Item
return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]"); return ("[bookmark=" . str_replace("#", "&num;", $match[1]) . "]" . str_replace("#", "&num;", $match[2]) . "[/bookmark]");
}, $body); }, $body);
$body = preg_replace_callback("/\[attachment (.*)\](.*?)\[\/attachment\]/ism", $body = preg_replace_callback("/\[attachment (.*?)\](.*?)\[\/attachment\]/ism",
function ($match) { function ($match) {
return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]"); return ("[attachment " . str_replace("#", "&num;", $match[1]) . "]" . $match[2] . "[/attachment]");
}, $body); }, $body);
@ -2639,10 +2655,10 @@ class Item
unset($hook_data); unset($hook_data);
} }
$orig_body = $item['body']; $body = $item['body'] ?? '';
$item['body'] = preg_replace("/\s*\[attachment .*\].*?\[\/attachment\]\s*/ism", '', $item['body']); $item['body'] = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", "\n", $item['body']);
self::putInCache($item); self::putInCache($item);
$item['body'] = $orig_body; $item['body'] = $body;
$s = $item["rendered-html"]; $s = $item["rendered-html"];
$hook_data = [ $hook_data = [
@ -2666,19 +2682,23 @@ class Item
if (!empty($shared['guid'])) { if (!empty($shared['guid'])) {
$shared_item = Post::selectFirst(['uri-id', 'plink'], ['guid' => $shared['guid']]); $shared_item = Post::selectFirst(['uri-id', 'plink'], ['guid' => $shared['guid']]);
$shared_uri_id = $shared_item['uri-id'] ?? 0; $shared_uri_id = $shared_item['uri-id'] ?? 0;
$shared_plink = $shared_item['plink'] ?? ''; $shared_links = [strtolower($shared_item['plink'] ?? '')];
$attachments = Post\Media::splitAttachments($shared_uri_id, $shared['guid']); $attachments = Post\Media::splitAttachments($shared_uri_id, $shared['guid']);
$s = self::addVisualAttachments($attachments, $item, $s, true); $s = self::addVisualAttachments($attachments, $item, $s, true);
$s = self::addLinkAttachment($attachments, $item, $s, true, ''); $s = self::addLinkAttachment($attachments, $body, $s, true, []);
$s = self::addNonVisualAttachments($attachments, $item, $s, true); $s = self::addNonVisualAttachments($attachments, $item, $s, true);
$shared_links = array_merge($shared_links, array_column($attachments['visual'], 'url'));
$shared_links = array_merge($shared_links, array_column($attachments['link'], 'url'));
$shared_links = array_merge($shared_links, array_column($attachments['additional'], 'url'));
$body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
} else { } else {
$shared_uri_id = 0; $shared_uri_id = 0;
$shared_plink = ''; $shared_links = [];
} }
$attachments = Post\Media::splitAttachments($item['uri-id'], $item['guid']); $attachments = Post\Media::splitAttachments($item['uri-id'], $item['guid'], $shared_links);
$s = self::addVisualAttachments($attachments, $item, $s, false); $s = self::addVisualAttachments($attachments, $item, $s, false);
$s = self::addLinkAttachment($attachments, $item, $s, false, $shared_plink); $s = self::addLinkAttachment($attachments, $body, $s, false, $shared_links);
$s = self::addNonVisualAttachments($attachments, $item, $s, false); $s = self::addNonVisualAttachments($attachments, $item, $s, false);
// Map. // Map.
@ -2712,6 +2732,12 @@ class Item
*/ */
public static function containsLink(string $body, string $url) public static function containsLink(string $body, string $url)
{ {
// Make sure that for example site parameters aren't used when testing if the link is contained in the body
$urlparts = parse_url($url);
unset($urlparts['query']);
unset($urlparts['fragment']);
$url = Network::unparseURL($urlparts);
if (strpos($body, $url)) { if (strpos($body, $url)) {
return true; return true;
} }
@ -2738,6 +2764,7 @@ class Item
$leading = ''; $leading = '';
$trailing = ''; $trailing = '';
// @todo In the future we should make a single for the template engine with all media in it. This allows more flexibilty.
foreach ($attachments['visual'] as $attachment) { foreach ($attachments['visual'] as $attachment) {
if (self::containsLink($item['body'], $attachment['url'])) { if (self::containsLink($item['body'], $attachment['url'])) {
continue; continue;
@ -2794,13 +2821,18 @@ class Item
'attachment' => $attachment, 'attachment' => $attachment,
], ],
]); ]);
$trailing .= $media; // On Diaspora posts the attached pictures are leading
if ($item['network'] == Protocol::DIASPORA) {
$leading .= $media;
} else {
$trailing .= $media;
}
} }
} }
if ($shared) { if ($shared) {
$content = str_replace(BBCode::ANCHOR, '<div class="body-attach">' . $leading . '<div class="clear"></div></div>' . BBCode::ANCHOR, $content); $content = str_replace(BBCode::TOP_ANCHOR, '<div class="body-attach">' . $leading . '<div class="clear"></div></div>' . BBCode::TOP_ANCHOR, $content);
$content = str_replace(BBCode::ANCHOR, BBCode::ANCHOR . '<div class="body-attach">' . $trailing . '<div class="clear"></div></div>', $content); $content = str_replace(BBCode::BOTTOM_ANCHOR, '<div class="body-attach">' . $trailing . '<div class="clear"></div></div>' . BBCode::BOTTOM_ANCHOR, $content);
} else { } else {
if ($leading != '') { if ($leading != '') {
$content = '<div class="body-attach">' . $leading . '<div class="clear"></div></div>' . $content; $content = '<div class="body-attach">' . $leading . '<div class="clear"></div></div>' . $content;
@ -2819,12 +2851,13 @@ class Item
* Add link attachment to the content * Add link attachment to the content
* *
* @param array $attachments * @param array $attachments
* @param array $item * @param string $body
* @param string $content * @param string $content
* @param bool $shared * @param bool $shared
* @param array $ignore_links A list of URLs to ignore
* @return string modified content * @return string modified content
*/ */
private static function addLinkAttachment(array $attachments, array $item, string $content, bool $shared, string $ignore_link) private static function addLinkAttachment(array $attachments, string $body, string $content, bool $shared, array $ignore_links)
{ {
$stamp1 = microtime(true); $stamp1 = microtime(true);
// @ToDo Check only for audio and video // @ToDo Check only for audio and video
@ -2832,14 +2865,23 @@ class Item
if (!empty($attachments['link'])) { if (!empty($attachments['link'])) {
foreach ($attachments['link'] as $link) { foreach ($attachments['link'] as $link) {
if (!Strings::compareLink($link['url'], $ignore_link)) { $found = false;
foreach ($ignore_links as $ignore_link) {
if (Strings::compareLink($link['url'], $ignore_link)) {
$found = true;
}
}
// @todo Judge between the links to use the one with most information
if (!$found && (empty($attachment) || !empty($link['author-name']) ||
(empty($attachment['name']) && !empty($link['name'])) ||
(empty($attachment['description']) && !empty($link['description'])) ||
(empty($attachment['preview']) && !empty($link['preview'])))) {
$attachment = $link; $attachment = $link;
} }
} }
} }
if (!empty($attachment)) { if (!empty($attachment)) {
$footer = '';
$data = [ $data = [
'after' => '', 'after' => '',
'author_name' => $attachment['author-name'] ?? '', 'author_name' => $attachment['author-name'] ?? '',
@ -2861,17 +2903,58 @@ class Item
$data['preview'] = $attachment['preview'] ?? ''; $data['preview'] = $attachment['preview'] ?? '';
} }
} }
} elseif (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $item['body'], $match)) {
$footer = $match[1]; if (!empty($data['description']) && !empty($content)) {
$data = []; similar_text($data['description'], $content, $percent);
} else {
$percent = 0;
}
if (!empty($data['description']) && (($data['title'] == $data['description']) || ($percent > 95) || (strpos($content, $data['description']) !== false))) {
$data['description'] = '';
}
if (($data['author_name'] ?? '') == ($data['provider_name'] ?? '')) {
$data['author_name'] = '';
}
if (($data['author_url'] ?? '') == ($data['provider_url'] ?? '')) {
$data['author_url'] = '';
}
} elseif (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $body, $match)) {
$data = BBCode::getAttachmentData($match[1]);
} }
DI::profiler()->saveTimestamp($stamp1, 'rendering'); DI::profiler()->saveTimestamp($stamp1, 'rendering');
if (!empty($footer) || !empty($data)) { if (isset($data['url']) && !in_array($data['url'], $ignore_links)) {
// @todo Use a template if (!empty($data['description']) || !empty($data['image']) || !empty($data['preview'])) {
$rendered = BBCode::convertAttachment($footer, BBCode::INTERNAL, false, $data); $parts = parse_url($data['url']);
if (!empty($parts['scheme']) && !empty($parts['host'])) {
if (empty($data['provider_name'])) {
$data['provider_name'] = $parts['host'];
}
if (empty($data['provider_url']) || empty(parse_url($data['provider_url'], PHP_URL_SCHEME))) {
$data['provider_url'] = $parts['scheme'] . '://' . $parts['host'];
if (!empty($parts['port'])) {
$data['provider_url'] .= ':' . $parts['port'];
}
}
}
// @todo Use a template
$rendered = BBCode::convertAttachment('', BBCode::INTERNAL, false, $data);
} elseif (!self::containsLink($content, $data['url'])) {
$rendered = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/link.tpl'), [
'$url' => $data['url'],
'$title' => $data['title'],
]);
} else {
return $content;
}
if ($shared) { if ($shared) {
return str_replace(BBCode::ANCHOR, BBCode::ANCHOR . $rendered, $content); return str_replace(BBCode::BOTTOM_ANCHOR, BBCode::BOTTOM_ANCHOR . $rendered, $content);
} else { } else {
return $content . $rendered; return $content . $rendered;
} }
@ -2933,18 +3016,19 @@ class Item
'href' => "display/" . $item['guid'], 'href' => "display/" . $item['guid'],
'orig' => "display/" . $item['guid'], 'orig' => "display/" . $item['guid'],
'title' => DI::l10n()->t('View on separate page'), 'title' => DI::l10n()->t('View on separate page'),
'orig_title' => DI::l10n()->t('view on separate page'), 'orig_title' => DI::l10n()->t('View on separate page'),
]; ];
if (!empty($item['plink'])) { if (!empty($item['plink'])) {
$ret["href"] = DI::baseUrl()->remove($item['plink']); $ret['href'] = DI::baseUrl()->remove($item['plink']);
$ret["title"] = DI::l10n()->t('link to source'); $ret['title'] = DI::l10n()->t('Link to source');
} }
} elseif (!empty($item['plink']) && ($item['private'] != self::PRIVATE)) { } elseif (!empty($item['plink']) && ($item['private'] != self::PRIVATE)) {
$ret = [ $ret = [
'href' => $item['plink'], 'href' => $item['plink'],
'orig' => $item['plink'], 'orig' => $item['plink'],
'title' => DI::l10n()->t('link to source'), 'title' => DI::l10n()->t('Link to source'),
'orig_title' => DI::l10n()->t('Link to source'),
]; ];
} else { } else {
$ret = []; $ret = [];
@ -3182,4 +3266,41 @@ class Item
return true; return true;
} }
/**
* Improve the data in shared posts
*
* @param array $item
* @return string body
*/
public static function improveSharedDataInBody(array $item)
{
$shared = BBCode::fetchShareAttributes($item['body']);
if (empty($shared['link'])) {
return $item['body'];
}
$id = self::fetchByLink($shared['link']);
Logger::info('Fetched shared post', ['uri-id' => $item['uri-id'], 'id' => $id, 'author' => $shared['profile'], 'url' => $shared['link'], 'guid' => $shared['guid'], 'callstack' => System::callstack()]);
if (!$id) {
return $item['body'];
}
$shared_item = Post::selectFirst(['author-name', 'author-link', 'author-avatar', 'plink', 'created', 'guid', 'title', 'body'], ['id' => $id]);
if (!DBA::isResult($shared_item)) {
return $item['body'];
}
$shared_content = BBCode::getShareOpeningTag($shared_item['author-name'], $shared_item['author-link'], $shared_item['author-avatar'], $shared_item['plink'], $shared_item['created'], $shared_item['guid']);
if (!empty($shared_item['title'])) {
$shared_content .= '[h3]'.$shared_item['title'].'[/h3]'."\n";
}
$shared_content .= $shared_item['body'];
$item['body'] = preg_replace("/\[share.*?\](.*)\[\/share\]/ism", $shared_content . '[/share]', $item['body']);
Logger::info('New shared data', ['uri-id' => $item['uri-id'], 'id' => $id, 'shared_item' => $shared_item]);
return $item['body'];
}
} }

View file

@ -726,18 +726,35 @@ class Photo
* Then set the permissions to public. * Then set the permissions to public.
*/ */
$fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow, self::setPermissionForRessource($image_rid, $uid, $str_contact_allow, $str_group_allow, $str_contact_deny, $str_group_deny);
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny,
'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false)];
$condition = ['resource-id' => $image_rid, 'uid' => $uid];
Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]);
Photo::update($fields, $condition);
} }
return true; return true;
} }
/**
* Add permissions to photo ressource
* @todo mix with previous photo permissions
*
* @param string $image_rid
* @param integer $uid
* @param string $str_contact_allow
* @param string $str_group_allow
* @param string $str_contact_deny
* @param string $str_group_deny
* @return void
*/
public static function setPermissionForRessource(string $image_rid, int $uid, string $str_contact_allow, string $str_group_allow, string $str_contact_deny, string $str_group_deny)
{
$fields = ['allow_cid' => $str_contact_allow, 'allow_gid' => $str_group_allow,
'deny_cid' => $str_contact_deny, 'deny_gid' => $str_group_deny,
'accessible' => DI::pConfig()->get($uid, 'system', 'accessible-photos', false)];
$condition = ['resource-id' => $image_rid, 'uid' => $uid];
Logger::info('Set permissions', ['condition' => $condition, 'permissions' => $fields]);
Photo::update($fields, $condition);
}
/** /**
* Strips known picture extensions from picture links * Strips known picture extensions from picture links
* *

View file

@ -127,12 +127,13 @@ class Post
* Check if post data exists * Check if post data exists
* *
* @param array $condition array of fields for condition * @param array $condition array of fields for condition
* @param bool $user_mode true = post-user-view, false = post-view
* *
* @return boolean Are there rows for that condition? * @return boolean Are there rows for that condition?
* @throws \Exception * @throws \Exception
*/ */
public static function exists($condition) { public static function exists($condition, bool $user_mode = true) {
return DBA::exists('post-user-view', $condition); return DBA::exists($user_mode ? 'post-user-view' : 'post-view', $condition);
} }
/** /**
@ -140,6 +141,7 @@ class Post
* *
* @param array $condition array of fields for condition * @param array $condition array of fields for condition
* @param array $params Array of several parameters * @param array $params Array of several parameters
* @param bool $user_mode true = post-user-view, false = post-view
* *
* @return int * @return int
* *
@ -151,9 +153,9 @@ class Post
* $count = Post::count($condition); * $count = Post::count($condition);
* @throws \Exception * @throws \Exception
*/ */
public static function count(array $condition = [], array $params = []) public static function count(array $condition = [], array $params = [], bool $user_mode = true)
{ {
return DBA::count('post-user-view', $condition, $params); return DBA::count($user_mode ? 'post-user-view' : 'post-view', $condition, $params);
} }
/** /**
@ -265,13 +267,14 @@ class Post
* @param array $selected Array of selected fields, empty for all * @param array $selected Array of selected fields, empty for all
* @param array $condition Array of fields for condition * @param array $condition Array of fields for condition
* @param array $params Array of several parameters * @param array $params Array of several parameters
* @param bool $user_mode true = post-user-view, false = post-view
* *
* @return boolean|object * @return boolean|object
* @throws \Exception * @throws \Exception
*/ */
public static function select(array $selected = [], array $condition = [], $params = []) public static function select(array $selected = [], array $condition = [], $params = [], bool $user_mode = true)
{ {
return self::selectView('post-user-view', $selected, $condition, $params); return self::selectView($user_mode ? 'post-user-view' : 'post-view', $selected, $condition, $params);
} }
/** /**

View file

@ -112,6 +112,10 @@ class Delayed
*/ */
public static function publish(array $item, int $notify = 0, array $taglist = [], array $attachments = [], bool $unprepared = false, string $uri = '') public static function publish(array $item, int $notify = 0, array $taglist = [], array $attachments = [], bool $unprepared = false, string $uri = '')
{ {
if (!empty($attachments)) {
$item['attachments'] = $attachments;
}
if ($unprepared) { if ($unprepared) {
$_SESSION['authenticated'] = true; $_SESSION['authenticated'] = true;
$_SESSION['uid'] = $item['uid']; $_SESSION['uid'] = $item['uid'];
@ -157,11 +161,6 @@ class Delayed
foreach ($taglist as $tag) { foreach ($taglist as $tag) {
Tag::store($feeditem['uri-id'], Tag::HASHTAG, $tag); Tag::store($feeditem['uri-id'], Tag::HASHTAG, $tag);
} }
foreach ($attachments as $attachment) {
$attachment['uri-id'] = $feeditem['uri-id'];
Media::insert($attachment);
}
} }
return $id; return $id;

View file

@ -21,15 +21,17 @@
namespace Friendica\Model\Post; namespace Friendica\Model\Post;
use Friendica\Content\PageInfo;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Database\Database; use Friendica\Database\Database;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Util\Images; use Friendica\Util\Images;
use Friendica\Util\ParseUrl; use Friendica\Util\ParseUrl;
use Friendica\Util\Strings;
/** /**
* Class Media * Class Media
@ -64,6 +66,11 @@ class Media
return; return;
} }
if (DBA::exists('post-media', ['uri-id' => $media['uri-id'], 'preview' => $media['url']])) {
Logger::info('Media already exists as preview', ['uri-id' => $media['uri-id'], 'url' => $media['url'], 'callstack' => System::callstack()]);
return;
}
// "document" has got the lowest priority. So when the same file is both attached as document // "document" has got the lowest priority. So when the same file is both attached as document
// and embedded as picture then we only store the picture or replace the document // and embedded as picture then we only store the picture or replace the document
$found = DBA::selectFirst('post-media', ['type'], ['uri-id' => $media['uri-id'], 'url' => $media['url']]); $found = DBA::selectFirst('post-media', ['type'], ['uri-id' => $media['uri-id'], 'url' => $media['url']]);
@ -281,7 +288,13 @@ class Media
public static function insertFromBody(int $uriid, string $body) public static function insertFromBody(int $uriid, string $body)
{ {
// Simplify image codes // Simplify image codes
$body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body); $unshared_body = $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body);
// Only remove the shared data from "real" reshares
$shared = BBCode::fetchShareAttributes($body);
if (!empty($shared['guid'])) {
$unshared_body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
}
$attachments = []; $attachments = [];
if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) { if (preg_match_all("#\[url=([^\]]+?)\]\s*\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]\s*\[/url\]#ism", $body, $pictures, PREG_SET_ORDER)) {
@ -336,19 +349,51 @@ class Media
} }
} }
$url = PageInfo::getRelevantUrlFromBody($body);
if (!empty($url)) {
Logger::debug('Got page url', ['url' => $url]);
$attachments[$url] = ['uri-id' => $uriid, 'type' => self::UNKNOWN, 'url' => $url];
}
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
self::insert($attachment); // Only store attachments that are part of the unshared body
if (strpos($unshared_body, $attachment['url']) !== false) {
self::insert($attachment);
}
} }
return trim($body); return trim($body);
} }
/**
* Add media links from a relevant url in the body
*
* @param integer $uriid
* @param string $body
*/
public static function insertFromRelevantUrl(int $uriid, string $body)
{
// Only remove the shared data from "real" reshares
$shared = BBCode::fetchShareAttributes($body);
if (!empty($shared['guid'])) {
// Don't look at the shared content
$body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
}
// Remove all hashtags and mentions
$body = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '', $body);
// Search for pure links
if (preg_match_all("/\[url\](https?:.*?)\[\/url\]/ism", $body, $matches)) {
foreach ($matches[1] as $url) {
Logger::info('Got page url (link without description)', ['uri-id' => $uriid, 'url' => $url]);
self::insert(['uri-id' => $uriid, 'type' => self::UNKNOWN, 'url' => $url]);
}
}
// Search for links with descriptions
if (preg_match_all("/\[url\=(https?:.*?)\].*?\[\/url\]/ism", $body, $matches)) {
foreach ($matches[1] as $url) {
Logger::info('Got page url (link with description)', ['uri-id' => $uriid, 'url' => $url]);
self::insert(['uri-id' => $uriid, 'type' => self::UNKNOWN, 'url' => $url]);
}
}
}
/** /**
* Add media links from the attachment field * Add media links from the attachment field
* *
@ -357,6 +402,9 @@ class Media
*/ */
public static function insertFromAttachmentData(int $uriid, string $body) public static function insertFromAttachmentData(int $uriid, string $body)
{ {
// Don't look at the shared content
$body = preg_replace("/\s*\[share .*?\].*?\[\/share\]\s*/ism", '', $body);
$data = BBCode::getAttachmentData($body); $data = BBCode::getAttachmentData($body);
if (empty($data)) { if (empty($data)) {
return; return;
@ -447,11 +495,12 @@ class Media
/** /**
* Split the attachment media in the three segments "visual", "link" and "additional" * Split the attachment media in the three segments "visual", "link" and "additional"
* *
* @param int $uri_id * @param int $uri_id
* @param string $guid * @param string $guid
* @param array $links ist of links that shouldn't be added
* @return array attachments * @return array attachments
*/ */
public static function splitAttachments(int $uri_id, string $guid = '') public static function splitAttachments(int $uri_id, string $guid = '', array $links = [])
{ {
$attachments = ['visual' => [], 'link' => [], 'additional' => []]; $attachments = ['visual' => [], 'link' => [], 'additional' => []];
@ -462,8 +511,26 @@ class Media
$height = 0; $height = 0;
$selected = ''; $selected = '';
$previews = [];
foreach ($media as $medium) { foreach ($media as $medium) {
foreach ($links as $link) {
if (Strings::compareLink($link, $medium['url'])) {
continue 2;
}
}
// Avoid adding separate media entries for previews
foreach ($previews as $preview) {
if (Strings::compareLink($preview, $medium['url'])) {
continue 2;
}
}
if (!empty($medium['preview'])) {
$previews[] = $medium['preview'];
}
$type = explode('/', current(explode(';', $medium['mimetype']))); $type = explode('/', current(explode(';', $medium['mimetype'])));
if (count($type) < 2) { if (count($type) < 2) {
Logger::info('Unknown MimeType', ['type' => $type, 'media' => $medium]); Logger::info('Unknown MimeType', ['type' => $type, 'media' => $medium]);
@ -511,4 +578,57 @@ class Media
} }
return $attachments; return $attachments;
} }
/**
* Add media attachments to the body
*
* @param int $uriid
* @param string $body
* @return string body
*/
public static function addAttachmentsToBody(int $uriid, string $body = '')
{
if (empty($body)) {
$item = Post::selectFirst(['body'], ['uri-id' => $uriid]);
if (!DBA::isResult($item)) {
return '';
}
$body = $item['body'];
}
$original_body = $body;
$body = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $body);
foreach (self::getByURIId($uriid, [self::IMAGE, self::AUDIO, self::VIDEO]) as $media) {
if (Item::containsLink($body, $media['url'])) {
continue;
}
if ($media['type'] == self::IMAGE) {
if (!empty($media['preview'])) {
if (!empty($media['description'])) {
$body .= "\n[url=" . $media['url'] . "][img=" . $media['preview'] . ']' . $media['description'] .'[/img][/url]';
} else {
$body .= "\n[url=" . $media['url'] . "][img]" . $media['preview'] .'[/img][/url]';
}
} else {
if (!empty($media['description'])) {
$body .= "\n[img=" . $media['url'] . ']' . $media['description'] .'[/img]';
} else {
$body .= "\n[img]" . $media['url'] .'[/img]';
}
}
} elseif ($media['type'] == self::AUDIO) {
$body .= "\n[audio]" . $media['url'] . "[/audio]\n";
} elseif ($media['type'] == self::VIDEO) {
$body .= "\n[video]" . $media['url'] . "[/video]\n";
}
}
if (preg_match("/.*(\[attachment.*?\].*?\[\/attachment\]).*/ism", $original_body, $match)) {
$body .= "\n" . $match[1];
}
return $body;
}
} }

View file

@ -412,7 +412,12 @@ class Profile
} }
} }
$p = []; // Expected profile/vcard.tpl profile.* template variables
$p = [
'address' => null,
'edit' => null,
'upubkey' => null,
];
foreach ($profile as $k => $v) { foreach ($profile as $k => $v) {
$k = str_replace('-', '_', $k); $k = str_replace('-', '_', $k);
$p[$k] = $v; $p[$k] = $v;

View file

@ -56,7 +56,7 @@ class Queue extends BaseAdmin
} }
// @TODO Move to Model\WorkerQueue::getEntries() // @TODO Move to Model\WorkerQueue::getEntries()
$entries = DBA::select('workerqueue', ['id', 'parameter', 'created', 'priority'], $condition, ['limit' => 999, 'order' => ['created']]); $entries = DBA::select('workerqueue', ['id', 'parameter', 'created', 'priority', 'command'], $condition, ['limit' => 999, 'order' => ['created']]);
$r = []; $r = [];
while ($entry = DBA::fetch($entries)) { while ($entry = DBA::fetch($entries)) {
@ -73,6 +73,7 @@ class Queue extends BaseAdmin
'$page' => $sub_title, '$page' => $sub_title,
'$count' => count($r), '$count' => count($r),
'$id_header' => DI::l10n()->t('ID'), '$id_header' => DI::l10n()->t('ID'),
'$command_header' => DI::l10n()->t('Command'),
'$param_header' => DI::l10n()->t('Job Parameters'), '$param_header' => DI::l10n()->t('Job Parameters'),
'$created_header' => DI::l10n()->t('Created'), '$created_header' => DI::l10n()->t('Created'),
'$prio_header' => DI::l10n()->t('Priority'), '$prio_header' => DI::l10n()->t('Priority'),

View file

@ -38,7 +38,7 @@ class Accounts extends BaseApi
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
if (empty($parameters['id'])) { if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound(); DI::mstdnError()->UnprocessableEntity();
} }
$id = $parameters['id']; $id = $parameters['id'];
@ -46,7 +46,7 @@ class Accounts extends BaseApi
DI::mstdnError()->RecordNotFound(); DI::mstdnError()->RecordNotFound();
} }
$account = DI::mstdnAccount()->createFromContactId($id); $account = DI::mstdnAccount()->createFromContactId($id, self::getCurrentUserID());
System::jsonExit($account); System::jsonExit($account);
} }
} }

View file

@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
*/
class Followers extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) {
DI::mstdnError()->RecordNotFound();
}
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
$params = ['order' => ['cid' => true], 'limit' => $limit];
$condition = ['relation-cid' => $id, 'follows' => true];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $min_id]);
$params['order'] = ['cid'];
}
$followers = DBA::select('contact-relation', ['cid'], $condition, $parameters);
while ($follower = DBA::fetch($followers)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($follower['cid'], $uid);
}
DBA::close($followers);
if (!empty($min_id)) {
array_reverse($accounts);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
*/
class Following extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) {
DI::mstdnError()->RecordNotFound();
}
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
$params = ['order' => ['relation-cid' => true], 'limit' => $limit];
$condition = ['cid' => $id, 'follows' => true];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`relation-cid` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`relation-cid` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`relation-cid` > ?", $min_id]);
$params['order'] = ['cid'];
}
$followers = DBA::select('contact-relation', ['relation-cid'], $condition, $parameters);
while ($follower = DBA::fetch($followers)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($follower['relation-cid'], $uid);
}
DBA::close($followers);
if (!empty($min_id)) {
array_reverse($accounts);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\System;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
*/
class IdentityProofs extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
System::jsonExit([]);
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
*/
class Lists extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) {
DI::mstdnError()->RecordNotFound();
}
$lists = [];
$cdata = Contact::getPublicAndUserContacID($id, $uid);
if (!empty($cdata['user'])) {
$groups = DBA::select('group_member', ['gid'], ['contact-id' => $cdata['user']]);
while ($group = DBA::fetch($groups)) {
$lists[] = DI::mstdnList()->createFromGroupId($group['gid']);
}
DBA::close($groups);
}
System::jsonExit($lists);
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\Search as CoreSearch;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Module\BaseApi;
use Friendica\Object\Search\ContactResult;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
*/
class Search extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
// What to search for
$q = (int)!isset($_REQUEST['q']) ? 0 : $_REQUEST['q'];
// Maximum number of results. Defaults to 40.
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
// Attempt WebFinger lookup. Defaults to false. Use this when q is an exact address.
$resolve = (int)!isset($_REQUEST['resolve']) ? 0 : $_REQUEST['resolve'];
// Only who the user is following. Defaults to false.
$following = (int)!isset($_REQUEST['following']) ? 0 : $_REQUEST['following'];
$accounts = [];
if (!$following) {
if ((strrpos($q, '@') > 0) && $resolve) {
$results = CoreSearch::getContactsFromProbe($q);
}
if (empty($results)) {
if (DI::config()->get('system', 'poco_local_search')) {
$results = CoreSearch::getContactsFromLocalDirectory($q, CoreSearch::TYPE_ALL, 0, $limit);
} elseif (!empty(DI::config()->get('system', 'directory'))) {
$results = CoreSearch::getContactsFromGlobalDirectory($q, CoreSearch::TYPE_ALL, 1);
}
}
if (!empty($results)) {
$counter = 0;
foreach ($results->getResults() as $result) {
if (++$counter > $limit) {
continue;
}
if ($result instanceof ContactResult) {
$id = Contact::getIdForURL($result->getUrl(), 0, false);
$accounts[] = DI::mstdnAccount()->createFromContactId($id, $uid);
}
}
}
} else {
$contacts = Contact::searchByName($q, '', $uid);
$counter = 0;
foreach ($contacts as $contact) {
if (!in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) {
continue;
}
if (++$counter > $limit) {
continue;
}
$accounts[] = DI::mstdnAccount()->createFromContactId($contact['id'], $uid);
}
DBA::close($contacts);
}
System::jsonExit($accounts);
}
}

View file

@ -43,7 +43,7 @@ class Statuses extends BaseApi
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
if (empty($parameters['id'])) { if (empty($parameters['id'])) {
DI::mstdnError()->RecordNotFound(); DI::mstdnError()->UnprocessableEntity();
} }
$id = $parameters['id']; $id = $parameters['id'];
@ -52,7 +52,7 @@ class Statuses extends BaseApi
} }
// Show only statuses with media attached? Defaults to false. // Show only statuses with media attached? Defaults to false.
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported $only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
// Return results older than this id // Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id']; $max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id // Return results newer than this id
@ -64,12 +64,23 @@ class Statuses extends BaseApi
$params = ['order' => ['uri-id' => true], 'limit' => $limit]; $params = ['order' => ['uri-id' => true], 'limit' => $limit];
$condition = ['author-id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED], $uid = self::getCurrentUserID();
'uid' => 0, 'network' => Protocol::FEDERATED];
if (!$uid) {
$condition = ['author-id' => $id, 'private' => [Item::PUBLIC, Item::UNLISTED],
'uid' => 0, 'network' => Protocol::FEDERATED];
} else {
$condition = ["`author-id` = ? AND (`uid` = 0 OR (`uid` = ? AND NOT `global`))", $id, $uid];
}
$condition = DBA::mergeConditions($condition, ["(`gravity` IN (?, ?) OR (`gravity` = ? AND `vid` = ?))", $condition = DBA::mergeConditions($condition, ["(`gravity` IN (?, ?) OR (`gravity` = ? AND `vid` = ?))",
GRAVITY_PARENT, GRAVITY_COMMENT, GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE)]); GRAVITY_PARENT, GRAVITY_COMMENT, GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE)]);
if ($only_media) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-media` WHERE `type` IN (?, ?, ?))",
Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]);
}
if (!empty($max_id)) { if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]); $condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
} }
@ -83,11 +94,11 @@ class Statuses extends BaseApi
$params['order'] = ['uri-id']; $params['order'] = ['uri-id'];
} }
$items = Post::selectForUser(0, ['uri-id', 'uid'], $condition, $params); $items = Post::selectForUser($uid, ['uri-id'], $condition, $params);
$statuses = []; $statuses = [];
while ($item = Post::fetch($items)) { while ($item = Post::fetch($items)) {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $item['uid']); $statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
} }
DBA::close($items); DBA::close($items);

View file

@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Accounts;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/
*/
class VerifyCredentials extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
$self = User::getOwnerDataById($uid);
if (empty($self)) {
DI::mstdnError()->InternalError();
}
$cdata = Contact::getPublicAndUserContacID($self['id'], $uid);
if (empty($cdata)) {
DI::mstdnError()->InternalError();
}
// @todo Support the source property,
$account = DI::mstdnAccount()->createFromContactId($cdata['user'], $uid);
System::jsonExit($account);
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/announcements/
*/
class Announcements extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
// @todo Possibly use the message from the pageheader addon for this
System::jsonExit([]);
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
use Friendica\Util\Network;
/**
* Apps class to register new OAuth clients
*/
class Apps extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function post(array $parameters = [])
{
// Workaround for AndStatus, see issue https://github.com/andstatus/andstatus/issues/538
if (empty($_REQUEST['client_name']) || empty($_REQUEST['redirect_uris'])) {
$postdata = Network::postdata();
if (!empty($postdata)) {
$_REQUEST = json_decode($postdata, true);
if (empty($_REQUEST)) {
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Missing parameters'));
}
}
}
$name = $_REQUEST['client_name'] ?? '';
$redirect = $_REQUEST['redirect_uris'] ?? '';
$scopes = $_REQUEST['scopes'] ?? 'read';
$website = $_REQUEST['website'] ?? '';
if (empty($name) || empty($redirect)) {
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Missing parameters'));
}
$client_id = bin2hex(random_bytes(32));
$client_secret = bin2hex(random_bytes(32));
$fields = ['client_id' => $client_id, 'client_secret' => $client_secret, 'name' => $name, 'redirect_uri' => $redirect];
if (!empty($scopes)) {
$fields['scopes'] = $scopes;
}
$fields['read'] = (stripos($scopes, 'read') !== false);
$fields['write'] = (stripos($scopes, 'write') !== false);
$fields['follow'] = (stripos($scopes, 'follow') !== false);
if (!empty($website)) {
$fields['website'] = $website;
}
if (!DBA::insert('application', $fields)) {
DI::mstdnError()->InternalError();
}
System::jsonExit(DI::mstdnApplication()->createFromApplicationId(DBA::lastInsertId()));
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/blocks/
*/
class Blocks extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) {
DI::mstdnError()->RecordNotFound();
}
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Maximum number of results. Defaults to 40.
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
$params = ['order' => ['cid' => true], 'limit' => $limit];
$condition = ['cid' => $id, 'blocked' => true, 'uid' => $uid];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $min_id]);
$params['order'] = ['cid'];
}
$followers = DBA::select('user-contact', ['cid'], $condition, $parameters);
while ($follower = DBA::fetch($followers)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($follower['cid'], $uid);
}
DBA::close($followers);
if (!empty($min_id)) {
array_reverse($accounts);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
/**
* @see https://docs.joinmastodon.org/methods/accounts/bookmarks/
*/
class Bookmarks extends BaseApi
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
// Return results older than id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Return results immediately newer than id
$min_id = (int)!isset($_REQUEST['min_id']) ? 0 : $_REQUEST['min_id'];
$params = ['order' => ['uri-id' => true], 'limit' => $limit];
$condition = ['pinned' => true, 'uid' => $uid];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $min_id]);
$params['order'] = ['uri-id'];
}
$items = Post::selectThreadForUser($uid, ['uri-id'], $condition, $params);
$statuses = [];
while ($item = Post::fetch($items)) {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
}
DBA::close($items);
if (!empty($min_id)) {
array_reverse($statuses);
}
System::jsonExit($statuses);
}
}

View file

@ -44,7 +44,7 @@ class Directory extends BaseApi
{ {
$offset = (int)!isset($_REQUEST['offset']) ? 0 : $_REQUEST['offset']; $offset = (int)!isset($_REQUEST['offset']) ? 0 : $_REQUEST['offset'];
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit']; $limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
$order = !isset($_REQUEST['order']) ? 'active' : $_REQUEST['order']; $order = $_REQUEST['order'] ?? 'active';
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true'); $local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
Logger::info('directory', ['offset' => $offset, 'limit' => $limit, 'order' => $order, 'local' => $local]); Logger::info('directory', ['offset' => $offset, 'limit' => $limit, 'order' => $order, 'local' => $local]);

View file

@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/endorsements/
*/
class Endorsements extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
System::jsonExit([]);
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
/**
* @see https://docs.joinmastodon.org/methods/accounts/favourites/
*/
class Favourited extends BaseApi
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
// Return results immediately newer than id
$min_id = (int)!isset($_REQUEST['min_id']) ? 0 : $_REQUEST['min_id'];
// Return results older than id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
$params = ['order' => ['thr-parent-id' => true], 'limit' => $limit];
$condition = ['gravity' => GRAVITY_ACTIVITY, 'verb' => Activity::LIKE, 'uid' => $uid];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`thr-parent-id` < ?", $max_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`thr-parent-id` > ?", $min_id]);
$params['order'] = ['thr-parent-id'];
}
$items = Post::selectForUser($uid, ['thr-parent-id'], $condition, $params);
$statuses = [];
while ($item = Post::fetch($items)) {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['thr-parent-id'], $uid);
}
DBA::close($items);
if (!empty($min_id)) {
array_reverse($statuses);
}
System::jsonExit($statuses);
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\BaseApi;
use Friendica\Model\Group;
/**
* @see https://docs.joinmastodon.org/methods/timelines/lists/
*/
class Lists extends BaseApi
{
public static function delete(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
if (!Group::exists($parameters['id'], $uid)) {
DI::mstdnError()->RecordNotFound();
}
if (!Group::remove($parameters['id'])) {
DI::mstdnError()->InternalError();
}
System::jsonExit([]);
}
public static function post(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
$title = $_REQUEST['title'] ?? '';
if (empty($title)) {
DI::mstdnError()->UnprocessableEntity();
}
Group::create($uid, $title);
$id = Group::getIdByName($uid, $title);
if (!$id) {
DI::mstdnError()->InternalError();
}
System::jsonExit(DI::mstdnList()->createFromGroupId($id));
}
public static function put(array $parameters = [])
{
$data = self::getPutData();
if (empty($data['title']) || empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
Group::update($parameters['id'], $data['title']);
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
$lists = [];
$groups = Group::getByUserId($uid);
foreach ($groups as $group) {
$lists[] = DI::mstdnList()->createFromGroupId($group['id']);
}
} else {
$id = $parameters['id'];
if (!Group::exists($id, $uid)) {
DI::mstdnError()->RecordNotFound();
}
$lists = DI::mstdnList()->createFromGroupId($id);
}
System::jsonExit($lists);
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Lists;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/timelines/lists/
*
* Currently the output will be unordered since we use public contact ids in the api and not user contact ids.
*/
class Accounts extends BaseApi
{
public static function delete(array $parameters = [])
{
self::unsupported('delete');
}
public static function post(array $parameters = [])
{
self::unsupported('post');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!DBA::exists('group', ['id' => $id, 'uid' => $uid])) {
DI::mstdnError()->RecordNotFound();
}
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Maximum number of results. Defaults to 40. Max 40.
// Set to 0 in order to get all accounts without pagination.
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
$params = ['order' => ['contact-id' => true]];
if ($limit != 0) {
$params['limit'] = $limit;
}
$condition = ['gid' => $id];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`contact-id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`contact-id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`contact-id` > ?", $min_id]);
$params['order'] = ['contact-id'];
}
$accounts = [];
$members = DBA::select('group_member', ['contact-id'], $condition, $params);
while ($member = DBA::fetch($members)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($member['contact-id'], $uid);
}
DBA::close($members);
if (!empty($min_id)) {
array_reverse($accounts);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/timelines/markers/
*/
class Markers extends BaseApi
{
public static function post(array $parameters = [])
{
self::unsupported('post');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
System::jsonExit([]);
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Photo;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/statuses/media/
*/
class Media extends BaseApi
{
public static function put(array $parameters = [])
{
$data = self::getPutData();
self::unsupported('put');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!Photo::exists(['id' => $id, 'uid' => $uid])) {
DI::mstdnError()->RecordNotFound();
}
System::jsonExit(DI::mstdnAttachment()->createFromPhoto($id));
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/mutes/
*/
class Mutes extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!DBA::exists('contact', ['id' => $id, 'uid' => 0])) {
DI::mstdnError()->RecordNotFound();
}
// Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Maximum number of results. Defaults to 40.
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
$params = ['order' => ['cid' => true], 'limit' => $limit];
$condition = ['cid' => $id, 'ignored' => true, 'uid' => $uid];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`cid` > ?", $min_id]);
$params['order'] = ['cid'];
}
$followers = DBA::select('user-contact', ['cid'], $condition, $parameters);
while ($follower = DBA::fetch($followers)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($follower['cid'], $uid);
}
DBA::close($followers);
if (!empty($min_id)) {
array_reverse($accounts);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Notification;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/mutes/
*/
class Notifications extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (!empty($parameters['id'])) {
$id = $parameters['id'];
if (!DBA::exists('notify', ['id' => $id, 'uid' => $uid])) {
DI::mstdnError()->RecordNotFound();
}
System::jsonExit(DI::mstdnNotification()->createFromNotifyId($id));
}
// Return results older than this ID
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this ID
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Return results immediately newer than this ID
$min_id = (int)!isset($_REQUEST['min_id']) ? 0 : $_REQUEST['min_id'];
// Maximum number of results to return (default 20)
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
// Array of types to exclude (follow, favourite, reblog, mention, poll, follow_request)
$exclude_types = $_REQUEST['exclude_types'] ?? [];
// Return only notifications received from this account
$account_id = (int)!isset($_REQUEST['account_id']) ? 0 : $_REQUEST['account_id'];
$params = ['order' => ['id' => true], 'limit' => $limit];
$condition = ['uid' => $uid, 'seen' => false, 'type' => []];
if (!empty($account_id)) {
$contact = Contact::getById($account_id, ['url']);
if (!empty($contact['url'])) {
$condition['url'] = $contact['url'];
}
}
if (!in_array('follow_request', $exclude_types)) {
$condition['type'] = array_merge($condition['type'], [Notification\Type::INTRO]);
}
if (!in_array('mention', $exclude_types)) {
$condition['type'] = array_merge($condition['type'],
[Notification\Type::WALL, Notification\Type::COMMENT, Notification\Type::MAIL, Notification\Type::TAG_SELF, Notification\Type::POKE]);
}
if (!in_array('status', $exclude_types)) {
$condition['type'] = array_merge($condition['type'], [Notification\Type::SHARE]);
}
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`id` > ?", $min_id]);
$params['order'] = ['id'];
}
$notifications = [];
$notify = DBA::select('notify', ['id'], $condition, $params);
while ($notification = DBA::fetch($notify)) {
$notifications[] = DI::mstdnNotification()->createFromNotifyId($notification['id']);
}
if (!empty($min_id)) {
array_reverse($notifications);
}
System::jsonExit($notifications);
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\User;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/preferences/
*/
class Preferences extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
$user = User::getById($uid, ['language', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);
if (!empty($user['allow_cid']) || !empty($user['allow_gid']) || !empty($user['deny_cid']) || !empty($user['deny_gid'])) {
$visibility = 'private';
} elseif (DI::pConfig()->get($uid, 'system', 'unlisted')) {
$visibility = 'unlisted';
} else {
$visibility = 'public';
}
$sensitive = false;
$language = $user['language'];
$media = DI::pConfig()->get($uid, 'nsfw', 'disable') ? 'show_all' : 'default';
$spoilers = DI::pConfig()->get($uid, 'system', 'disable_cw');
$preferences = new \Friendica\Object\Api\Mastodon\Preferences($visibility, $sensitive, $language, $media, $spoilers);
System::jsonExit($preferences);
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/proofs/
*/
class Proofs extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
System::jsonError(404, ['error' => 'Record not found']);
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/
*/
class ScheduledStatuses extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
System::jsonExit([]);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/statuses/
*/
class Statuses extends BaseApi
{
public static function post(array $parameters = [])
{
$data = self::getJsonPostData();
self::unsupported('post');
}
public static function delete(array $parameters = [])
{
self::unsupported('delete');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
System::jsonExit(DI::mstdnStatus()->createFromUriId($parameters['id'], self::getCurrentUserID()));
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/statuses/
*/
class Context extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
$parent = Post::selectFirst(['parent-uri-id'], ['uri-id' => $id]);
if (!DBA::isResult($parent)) {
DI::mstdnError()->RecordNotFound();
}
$parents = [];
$children = [];
$posts = Post::select(['uri-id', 'thr-parent-id'],
['parent-uri-id' => $parent['parent-uri-id'], 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]], [], false);
while ($post = Post::fetch($posts)) {
if ($post['uri-id'] == $post['thr-parent-id']) {
continue;
}
$parents[$post['uri-id']] = $post['thr-parent-id'];
$children[$post['thr-parent-id']][] = $post['uri-id'];
}
DBA::close($posts);
$statuses = ['ancestors' => [], 'descendants' => []];
$ancestors = [];
foreach (self::getParents($id, $parents) as $ancestor) {
$ancestors[$ancestor] = DI::mstdnStatus()->createFromUriId($ancestor, $uid);
}
ksort($ancestors);
foreach ($ancestors as $ancestor) {
$statuses['ancestors'][] = $ancestor;
}
$descendants = [];
foreach (self::getChildren($id, $children) as $descendant) {
$descendants[] = DI::mstdnStatus()->createFromUriId($descendant, $uid);
}
ksort($descendants);
foreach ($descendants as $descendant) {
$statuses['descendants'][] = $descendant;
}
System::jsonExit($statuses);
}
private static function getParents(int $id, array $parents, array $list = [])
{
if (!empty($parents[$id])) {
$list[] = $parents[$id];
$list = self::getParents($parents[$id], $parents, $list);
}
return $list;
}
private static function getChildren(int $id, array $children, array $list = [])
{
if (!empty($children[$id])) {
foreach ($children[$id] as $child) {
$list[] = $child;
$list = self::getChildren($child, $children, $list);
}
}
return $list;
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Protocol\Activity;
/**
* @see https://docs.joinmastodon.org/methods/statuses/
*/
class FavouritedBy extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!Post::exists(['uri-id' => $id, 'uid' => [0, $uid]])) {
DI::mstdnError()->RecordNotFound();
}
$activities = Post::select(['author-id'], ['thr-parent-id' => $id, 'gravity' => GRAVITY_ACTIVITY, 'verb' => Activity::LIKE], [], false);
$accounts = [];
while ($activity = Post::fetch($activities)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($activity['author-id'], $uid);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Statuses;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Protocol\Activity;
/**
* @see https://docs.joinmastodon.org/methods/statuses/
*/
class RebloggedBy extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
$id = $parameters['id'];
if (!Post::exists(['uri-id' => $id, 'uid' => [0, $uid]])) {
DI::mstdnError()->RecordNotFound();
}
$activities = Post::select(['author-id'], ['thr-parent-id' => $id, 'gravity' => GRAVITY_ACTIVITY, 'verb' => Activity::ANNOUNCE], [], false);
$accounts = [];
while ($activity = Post::fetch($activities)) {
$accounts[] = DI::mstdnAccount()->createFromContactId($activity['author-id'], $uid);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/methods/accounts/suggestions/
*/
class Suggestions extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
// Maximum number of results to return. Defaults to 40.
$limit = (int)!isset($_REQUEST['limit']) ? 40 : $_REQUEST['limit'];
$suggestions = Contact\Relation::getSuggestions($uid, 0, $limit);
$accounts = [];
foreach ($suggestions as $suggestion) {
$accounts[] = DI::mstdnAccount()->createFromContactId($suggestion['id'], $uid);
}
System::jsonExit($accounts);
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Timelines;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
/**
* @see https://docs.joinmastodon.org/methods/timelines/
*/
class Home extends BaseApi
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
// Return results older than id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Return results immediately newer than id
$min_id = (int)!isset($_REQUEST['min_id']) ? 0 : $_REQUEST['min_id'];
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
// Return only local statuses? Defaults to false.
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
$params = ['order' => ['uri-id' => true], 'limit' => $limit];
$condition = ['gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'uid' => $uid];
if ($local) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]);
}
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $min_id]);
$params['order'] = ['uri-id'];
}
$items = Post::selectForUser($uid, ['uri-id'], $condition, $params);
$statuses = [];
while ($item = Post::fetch($items)) {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
}
DBA::close($items);
if (!empty($min_id)) {
array_reverse($statuses);
}
System::jsonExit($statuses);
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Timelines;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
/**
* @see https://docs.joinmastodon.org/methods/timelines/
*/
class ListTimeline extends BaseApi
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['id'])) {
DI::mstdnError()->UnprocessableEntity();
}
// Return results older than id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than id
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Return results immediately newer than id
$min_id = (int)!isset($_REQUEST['min_id']) ? 0 : $_REQUEST['min_id'];
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
$params = ['order' => ['uri-id' => true], 'limit' => $limit];
$condition = ["`uid` = ? AND `gravity` IN (?, ?) AND `contact-id` IN (SELECT `contact-id` FROM `group_member` WHERE `gid` = ?)",
$uid, GRAVITY_PARENT, GRAVITY_COMMENT, $parameters['id']];
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $min_id]);
$params['order'] = ['uri-id'];
}
$items = Post::selectForUser($uid, ['uri-id'], $condition, $params);
$statuses = [];
while ($item = Post::fetch($items)) {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
}
DBA::close($items);
if (!empty($min_id)) {
array_reverse($statuses);
}
System::jsonExit($statuses);
}
}

View file

@ -46,7 +46,7 @@ class PublicTimeline extends BaseApi
// Show only remote statuses? Defaults to false. // Show only remote statuses? Defaults to false.
$remote = (bool)!isset($_REQUEST['remote']) ? false : ($_REQUEST['remote'] == 'true'); $remote = (bool)!isset($_REQUEST['remote']) ? false : ($_REQUEST['remote'] == 'true');
// Show only statuses with media attached? Defaults to false. // Show only statuses with media attached? Defaults to false.
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true'); // Currently not supported $only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
// Return results older than this id // Return results older than this id
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id']; $max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this id // Return results newer than this id
@ -69,6 +69,11 @@ class PublicTimeline extends BaseApi
$condition = DBA::mergeConditions($condition, ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]); $condition = DBA::mergeConditions($condition, ["NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]);
} }
if ($only_media) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-media` WHERE `type` IN (?, ?, ?))",
Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]);
}
if (!empty($max_id)) { if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]); $condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
} }

View file

@ -0,0 +1,106 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Api\Mastodon\Timelines;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Post;
use Friendica\Module\BaseApi;
use Friendica\Network\HTTPException;
/**
* @see https://docs.joinmastodon.org/methods/timelines/
*/
class Tag extends BaseApi
{
/**
* @param array $parameters
* @throws HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
self::login();
$uid = self::getCurrentUserID();
if (empty($parameters['hashtag'])) {
DI::mstdnError()->UnprocessableEntity();
}
// If true, return only local statuses. Defaults to false.
$local = (bool)!isset($_REQUEST['local']) ? false : ($_REQUEST['local'] == 'true');
// If true, return only statuses with media attachments. Defaults to false.
$only_media = (bool)!isset($_REQUEST['only_media']) ? false : ($_REQUEST['only_media'] == 'true');
// Return results older than this ID.
$max_id = (int)!isset($_REQUEST['max_id']) ? 0 : $_REQUEST['max_id'];
// Return results newer than this ID.
$since_id = (int)!isset($_REQUEST['since_id']) ? 0 : $_REQUEST['since_id'];
// Return results immediately newer than this ID.
$min_id = (int)!isset($_REQUEST['min_id']) ? 0 : $_REQUEST['min_id'];
// Maximum number of results to return. Defaults to 20.
$limit = (int)!isset($_REQUEST['limit']) ? 20 : $_REQUEST['limit'];
$params = ['order' => ['uri-id' => true], 'limit' => $limit];
$condition = ["`name` = ? AND (`uid` = ? OR (`uid` = ? AND NOT `global`))
AND (`network` IN (?, ?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
$parameters['hashtag'], 0, $uid, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, $uid, 0];
if ($local) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]);
}
if ($only_media) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-media` WHERE `type` IN (?, ?, ?))",
Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]);
}
if (!empty($max_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` < ?", $max_id]);
}
if (!empty($since_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $since_id]);
}
if (!empty($min_id)) {
$condition = DBA::mergeConditions($condition, ["`uri-id` > ?", $min_id]);
$params['order'] = ['uri-id'];
}
$items = DBA::select('tag-search-view', ['uri-id'], $condition, $params);
$statuses = [];
while ($item = Post::fetch($items)) {
$statuses[] = DI::mstdnStatus()->createFromUriId($item['uri-id'], $uid);
}
DBA::close($items);
if (!empty($min_id)) {
array_reverse($statuses);
}
System::jsonExit($statuses);
}
}

View file

@ -21,9 +21,6 @@
namespace Friendica\Module\Api\Mastodon; namespace Friendica\Module\Api\Mastodon;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\BaseApi; use Friendica\Module\BaseApi;
/** /**
@ -31,17 +28,48 @@ use Friendica\Module\BaseApi;
*/ */
class Unimplemented extends BaseApi class Unimplemented extends BaseApi
{ {
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function delete(array $parameters = [])
{
self::unsupported('delete');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function patch(array $parameters = [])
{
self::unsupported('patch');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function post(array $parameters = [])
{
self::unsupported('post');
}
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function put(array $parameters = [])
{
self::unsupported('put');
}
/** /**
* @param array $parameters * @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public static function rawContent(array $parameters = []) public static function rawContent(array $parameters = [])
{ {
$path = DI::args()->getQueryString(); self::unsupported('get');
Logger::info('Unimplemented API call', ['path' => $path]);
$error = DI::l10n()->t('API endpoint "%s" is not implemented', $path);
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');;
$errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
System::jsonError(501, $errorobj->toArray());
} }
} }

View file

@ -22,8 +22,14 @@
namespace Friendica\Module; namespace Friendica\Module;
use Friendica\BaseModule; use Friendica\BaseModule;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\Database;
use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Network\HTTPException; use Friendica\Network\HTTPException;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
require_once __DIR__ . '/../../include/api.php'; require_once __DIR__ . '/../../include/api.php';
@ -53,6 +59,32 @@ class BaseApi extends BaseModule
} }
} }
public static function delete(array $parameters = [])
{
if (!api_user()) {
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.'));
}
$a = DI::app();
if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
}
}
public static function patch(array $parameters = [])
{
if (!api_user()) {
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.'));
}
$a = DI::app();
if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
}
}
public static function post(array $parameters = []) public static function post(array $parameters = [])
{ {
if (!api_user()) { if (!api_user()) {
@ -66,6 +98,74 @@ class BaseApi extends BaseModule
} }
} }
public static function put(array $parameters = [])
{
if (!api_user()) {
throw new HTTPException\UnauthorizedException(DI::l10n()->t('Permission denied.'));
}
$a = DI::app();
if (!empty($a->user['uid']) && $a->user['uid'] != api_user()) {
throw new HTTPException\ForbiddenException(DI::l10n()->t('Permission denied.'));
}
}
/**
* Quit execution with the message that the endpoint isn't implemented
*
* @param string $method
* @return void
*/
public static function unsupported(string $method = 'all')
{
$path = DI::args()->getQueryString();
Logger::info('Unimplemented API call', ['method' => $method, 'path' => $path, 'agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'request' => $_REQUEST ?? []]);
$error = DI::l10n()->t('API endpoint %s %s is not implemented', strtoupper($method), $path);
$error_description = DI::l10n()->t('The API endpoint is currently not implemented but might be in the future.');
$errorobj = new \Friendica\Object\Api\Mastodon\Error($error, $error_description);
System::jsonError(501, $errorobj->toArray());
}
/**
* Get post data that is transmitted as JSON
*
* @return array request data
*/
public static function getJsonPostData()
{
$postdata = Network::postdata();
if (empty($postdata)) {
return [];
}
return json_decode($postdata, true);
}
/**
* Get request data for put requests
*
* @return array request data
*/
public static function getPutData()
{
$rawdata = Network::postdata();
if (empty($rawdata)) {
return [];
}
$putdata = [];
foreach (explode('&', $rawdata) as $value) {
$data = explode('=', $value);
if (count($data) == 2) {
$putdata[$data[0]] = urldecode($data[1]);
}
}
return $putdata;
}
/** /**
* Log in user via OAuth1 or Simple HTTP Auth. * Log in user via OAuth1 or Simple HTTP Auth.
* *
@ -84,13 +184,138 @@ class BaseApi extends BaseModule
*/ */
protected static function login() protected static function login()
{ {
api_login(DI::app()); if (empty(self::$current_user_id)) {
self::$current_user_id = self::getUserByBearer();
}
if (empty(self::$current_user_id)) {
// The execution stops here if no one is logged in
api_login(DI::app());
}
self::$current_user_id = api_user(); self::$current_user_id = api_user();
return (bool)self::$current_user_id; return (bool)self::$current_user_id;
} }
/**
* Get current user id, returns 0 if not logged in
*
* @return int User ID
*/
protected static function getCurrentUserID()
{
if (empty(self::$current_user_id)) {
self::$current_user_id = self::getUserByBearer();
}
if (empty(self::$current_user_id)) {
// Fetch the user id if logged in - but don't fail if not
api_login(DI::app(), false);
self::$current_user_id = api_user();
}
return (int)self::$current_user_id;
}
/**
* Get the user id via the Bearer token
*
* @return int User-ID
*/
private static function getUserByBearer()
{
$authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (substr($authorization, 0, 7) != 'Bearer ') {
return 0;
}
$bearer = trim(substr($authorization, 7));
$condition = ['access_token' => $bearer];
$token = DBA::selectFirst('application-token', ['uid'], $condition);
if (!DBA::isResult($token)) {
Logger::warning('Token not found', $condition);
return 0;
}
Logger::info('Token found', $token);
return $token['uid'];
}
/**
* Get the application record via the proved request header fields
*
* @param string $client_id
* @param string $client_secret
* @param string $redirect_uri
* @return array application record
*/
public static function getApplication(string $client_id, string $client_secret, string $redirect_uri)
{
$condition = ['client_id' => $client_id];
if (!empty($client_secret)) {
$condition['client_secret'] = $client_secret;
}
if (!empty($redirect_uri)) {
$condition['redirect_uri'] = $redirect_uri;
}
$application = DBA::selectFirst('application', [], $condition);
if (!DBA::isResult($application)) {
Logger::warning('Application not found', $condition);
return [];
}
return $application;
}
/**
* Check if an token for the application and user exists
*
* @param array $application
* @param integer $uid
* @return boolean
*/
public static function existsTokenForUser(array $application, int $uid)
{
return DBA::exists('application-token', ['application-id' => $application['id'], 'uid' => $uid]);
}
/**
* Fetch the token for the given application and user
*
* @param array $application
* @param integer $uid
* @return array application record
*/
public static function getTokenForUser(array $application, int $uid)
{
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
}
/**
* Create and fetch an token for the application and user
*
* @param array $application
* @param integer $uid
* @param string $scope
* @return array application record
*/
public static function createTokenForUser(array $application, int $uid, string $scope)
{
$code = bin2hex(random_bytes(32));
$access_token = bin2hex(random_bytes(32));
$fields = ['application-id' => $application['id'], 'uid' => $uid, 'code' => $code, 'access_token' => $access_token, 'scopes' => $scope,
'read' => (stripos($scope, 'read') !== false), 'write' => (stripos($scope, 'write') !== false),
'follow' => (stripos($scope, 'follow') !== false), 'created_at' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL)];
if (!DBA::insert('application-token', $fields, Database::INSERT_UPDATE)) {
return [];
}
return DBA::selectFirst('application-token', [], ['application-id' => $application['id'], 'uid' => $uid]);
}
/** /**
* Get user info array. * Get user info array.
* *
@ -140,7 +365,7 @@ class BaseApi extends BaseModule
$return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return; $return = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $return;
break; break;
} }
return $return; return $return;
} }

View file

@ -413,6 +413,10 @@ class Contact extends BaseModule
} }
if ($cmd === 'block') { if ($cmd === 'block') {
if (public_contact() === $contact_id) {
throw new BadRequestException(DI::l10n()->t('You can\'t block yourself'));
}
self::blockContact($contact_id); self::blockContact($contact_id);
$blocked = Model\Contact\User::isBlocked($contact_id, local_user()); $blocked = Model\Contact\User::isBlocked($contact_id, local_user());
@ -423,6 +427,10 @@ class Contact extends BaseModule
} }
if ($cmd === 'ignore') { if ($cmd === 'ignore') {
if (public_contact() === $contact_id) {
throw new BadRequestException(DI::l10n()->t('You can\'t ignore yourself'));
}
self::ignoreContact($contact_id); self::ignoreContact($contact_id);
$ignored = Model\Contact\User::isIgnored($contact_id, local_user()); $ignored = Model\Contact\User::isIgnored($contact_id, local_user());

View file

@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\OAuth;
use Friendica\Core\Renderer;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* Acknowledgement of OAuth requests
*/
class Acknowledge extends BaseApi
{
public static function post(array $parameters = [])
{
DI::session()->set('oauth_acknowledge', true);
DI::app()->redirect(DI::session()->get('return_path'));
}
public static function content(array $parameters = [])
{
DI::session()->set('return_path', $_REQUEST['return_path'] ?? '');
$o = Renderer::replaceMacros(Renderer::getMarkupTemplate('oauth_authorize.tpl'), [
'$title' => DI::l10n()->t('Authorize application connection'),
'$app' => ['name' => $_REQUEST['application'] ?? ''],
'$authorize' => DI::l10n()->t('Do you want to authorize this application to access your posts and contacts, and/or create new posts for you?'),
'$yes' => DI::l10n()->t('Yes'),
'$no' => DI::l10n()->t('No'),
]);
return $o;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\OAuth;
use Friendica\Core\Logger;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/spec/oauth/
* @see https://aaronparecki.com/oauth-2-simplified/
*/
class Authorize extends BaseApi
{
/**
* @param array $parameters
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function rawContent(array $parameters = [])
{
$response_type = $_REQUEST['response_type'] ?? '';
$client_id = $_REQUEST['client_id'] ?? '';
$client_secret = $_REQUEST['client_secret'] ?? ''; // Isn't normally provided. We will use it if present.
$redirect_uri = $_REQUEST['redirect_uri'] ?? '';
$scope = $_REQUEST['scope'] ?? 'read';
$state = $_REQUEST['state'] ?? '';
if ($response_type != 'code') {
Logger::warning('Unsupported or missing response type', ['request' => $_REQUEST]);
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Unsupported or missing response type'));
}
if (empty($client_id) || empty($redirect_uri)) {
Logger::warning('Incomplete request data', ['request' => $_REQUEST]);
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Incomplete request data'));
}
$application = self::getApplication($client_id, $client_secret, $redirect_uri);
if (empty($application)) {
DI::mstdnError()->UnprocessableEntity();
}
// @todo Compare the application scope and requested scope
$request = $_REQUEST;
unset($request['pagename']);
$redirect = 'oauth/authorize?' . http_build_query($request);
$uid = local_user();
if (empty($uid)) {
Logger::info('Redirect to login');
DI::app()->redirect('login?return_path=' . urlencode($redirect));
} else {
Logger::info('Already logged in user', ['uid' => $uid]);
}
if (!self::existsTokenForUser($application, $uid) && !DI::session()->get('oauth_acknowledge')) {
Logger::info('Redirect to acknowledge');
DI::app()->redirect('oauth/acknowledge?' . http_build_query(['return_path' => $redirect, 'application' => $application['name']]));
}
DI::session()->remove('oauth_acknowledge');
$token = self::createTokenForUser($application, $uid, $scope);
if (!$token) {
DI::mstdnError()->UnprocessableEntity();
}
DI::app()->redirect($application['redirect_uri'] . (strpos($application['redirect_uri'], '?') ? '&' : '?') . http_build_query(['code' => $token['code'], 'state' => $state]));
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\OAuth;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/spec/oauth/
*/
class Revoke extends BaseApi
{
public static function post(array $parameters = [])
{
self::unsupported('post');
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\OAuth;
use Friendica\Core\Logger;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Module\BaseApi;
/**
* @see https://docs.joinmastodon.org/spec/oauth/
* @see https://aaronparecki.com/oauth-2-simplified/
*/
class Token extends BaseApi
{
public static function post(array $parameters = [])
{
$grant_type = $_REQUEST['grant_type'] ?? '';
$code = $_REQUEST['code'] ?? '';
$redirect_uri = $_REQUEST['redirect_uri'] ?? '';
$client_id = $_REQUEST['client_id'] ?? '';
$client_secret = $_REQUEST['client_secret'] ?? '';
// AndStatus transmits the client data in the AUTHORIZATION header field, see https://github.com/andstatus/andstatus/issues/530
if (empty($client_id) && !empty($_SERVER['HTTP_AUTHORIZATION']) && (substr($_SERVER['HTTP_AUTHORIZATION'], 0, 6) == 'Basic ')) {
$datapair = explode(':', base64_decode(trim(substr($_SERVER['HTTP_AUTHORIZATION'], 6))));
if (count($datapair) == 2) {
$client_id = $datapair[0];
$client_secret = $datapair[1];
}
}
if (empty($client_id) || empty($client_secret)) {
Logger::warning('Incomplete request data', ['request' => $_REQUEST]);
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Incomplete request data'));
}
$application = self::getApplication($client_id, $client_secret, $redirect_uri);
if (empty($application)) {
DI::mstdnError()->UnprocessableEntity();
}
if ($grant_type == 'client_credentials') {
// the "client_credentials" are used as a token for the application itself.
// see https://aaronparecki.com/oauth-2-simplified/#client-credentials
$token = self::createTokenForUser($application, 0, '');
} elseif ($grant_type == 'authorization_code') {
// For security reasons only allow freshly created tokens
$condition = ["`redirect_uri` = ? AND `id` = ? AND `code` = ? AND `created_at` > UTC_TIMESTAMP() - INTERVAL ? MINUTE",
$redirect_uri, $application['id'], $code, 5];
$token = DBA::selectFirst('application-view', ['access_token', 'created_at'], $condition);
if (!DBA::isResult($token)) {
Logger::warning('Token not found or outdated', $condition);
DI::mstdnError()->Unauthorized();
}
} else {
Logger::warning('Unsupported or missing grant type', ['request' => $_REQUEST]);
DI::mstdnError()->UnprocessableEntity(DI::l10n()->t('Unsupported or missing grant type'));
}
$object = new \Friendica\Object\Api\Mastodon\Token($token['access_token'], 'Bearer', $application['scopes'], $token['created_at']);
System::jsonExit($object->toArray());
}
}

View file

@ -34,7 +34,6 @@ use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\ItemContent;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Module\BaseSearch; use Friendica\Module\BaseSearch;

View file

@ -36,8 +36,12 @@ class Login extends BaseModule
{ {
public static function content(array $parameters = []) public static function content(array $parameters = [])
{ {
$return_path = $_REQUEST['return_path'] ?? '' ;
if (local_user()) { if (local_user()) {
DI::baseUrl()->redirect(); DI::baseUrl()->redirect($return_path);
} elseif (!empty($return_path)) {
Session::set('return_path', $return_path);
} }
return self::form(Session::get('return_path'), intval(DI::config()->get('config', 'register_policy')) !== \Friendica\Module\Register::CLOSED); return self::form(Session::get('return_path'), intval(DI::config()->get('config', 'register_policy')) !== \Friendica\Module\Register::CLOSED);

View file

@ -30,8 +30,18 @@ use Friendica\BaseDataTransferObject;
*/ */
class Application extends BaseDataTransferObject class Application extends BaseDataTransferObject
{ {
/** @var string */
protected $client_id;
/** @var string */
protected $client_secret;
/** @var int */
protected $id;
/** @var string */ /** @var string */
protected $name; protected $name;
/** @var string */
protected $redirect_uri;
/** @var string */
protected $website;
/** /**
* Creates an application entry * Creates an application entry
@ -39,8 +49,36 @@ class Application extends BaseDataTransferObject
* @param array $item * @param array $item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/ */
public function __construct(string $name) public function __construct(string $name, string $client_id = null, string $client_secret = null, int $id = null, string $redirect_uri = null, string $website = null)
{ {
$this->name = $name; $this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->id = $id;
$this->name = $name;
$this->redirect_uri = $redirect_uri;
$this->website = $website;
}
/**
* Returns the current entity as an array
*
* @return array
*/
public function toArray(): array
{
$application = parent::toArray();
if (empty($application['id'])) {
unset($application['client_id']);
unset($application['client_secret']);
unset($application['id']);
unset($application['redirect_uri']);
}
if (empty($application['website'])) {
unset($application['website']);
}
return $application;
} }
} }

View file

@ -39,9 +39,17 @@ class Card extends BaseDataTransferObject
/** @var string */ /** @var string */
protected $type; protected $type;
/** @var string */ /** @var string */
protected $author_name;
/** @var string */
protected $author_url;
/** @var string */
protected $provider_name; protected $provider_name;
/** @var string */ /** @var string */
protected $provider_url; protected $provider_url;
/** @var int */
protected $width;
/** @var int */
protected $height;
/** @var string */ /** @var string */
protected $image; protected $image;
@ -57,9 +65,13 @@ class Card extends BaseDataTransferObject
$this->title = $attachment['title'] ?? ''; $this->title = $attachment['title'] ?? '';
$this->description = $attachment['description'] ?? ''; $this->description = $attachment['description'] ?? '';
$this->type = $attachment['type'] ?? ''; $this->type = $attachment['type'] ?? '';
$this->image = $attachment['image'] ?? ''; $this->author_name = $attachment['author_name'] ?? '';
$this->author_url = $attachment['author_url'] ?? '';
$this->provider_name = $attachment['provider_name'] ?? ''; $this->provider_name = $attachment['provider_name'] ?? '';
$this->provider_url = $attachment['provider_url'] ?? ''; $this->provider_url = $attachment['provider_url'] ?? '';
$this->width = $attachment['width'] ?? 0;
$this->height = $attachment['height'] ?? 0;
$this->image = $attachment['image'] ?? '';
} }
/** /**

View file

@ -80,7 +80,7 @@ class Instance extends BaseDataTransferObject
$instance->description = DI::config()->get('config', 'info'); $instance->description = DI::config()->get('config', 'info');
$instance->email = DI::config()->get('config', 'admin_email'); $instance->email = DI::config()->get('config', 'admin_email');
$instance->version = FRIENDICA_VERSION; $instance->version = FRIENDICA_VERSION;
$instance->urls = []; // Not supported $instance->urls = null; // Not supported
$instance->stats = Stats::get(); $instance->stats = Stats::get();
$instance->thumbnail = $baseUrl->get() . (DI::config()->get('system', 'shortcut_icon') ?? 'images/friendica-32.png'); $instance->thumbnail = $baseUrl->get() . (DI::config()->get('system', 'shortcut_icon') ?? 'images/friendica-32.png');
$instance->languages = [DI::config()->get('system', 'language')]; $instance->languages = [DI::config()->get('system', 'language')];

View file

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseDataTransferObject;
/**
* Class ListEntity
*
* @see https://docs.joinmastodon.org/entities/list/
*/
class ListEntity extends BaseDataTransferObject
{
/** @var string */
protected $id;
/** @var string */
protected $title;
/**
* Creates an list record
*
* @param int $id
* @param string $title
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(int $id, string $title, string $policy)
{
$this->id = (string)$id;
$this->title = $title;
$this->replies_policy = $policy;
}
}

View file

@ -0,0 +1,78 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseDataTransferObject;
use Friendica\Util\DateTimeFormat;
/**
* Class Notification
*
* @see https://docs.joinmastodon.org/entities/notification/
*/
class Notification extends BaseDataTransferObject
{
/** @var string */
protected $id;
/** @var string (Enumerable oneOf) */
protected $type;
/** @var string (Datetime) */
protected $created_at;
/** @var Account */
protected $account;
/** @var Status|null */
protected $status = null;
/**
* Creates a notification record
*
* @param array $item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(int $id, string $type, string $created_at, Account $account = null, Status $status = null)
{
$this->id = (string)$id;
$this->type = $type;
$this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::ATOM);
$this->account = $account->toArray();
if (!empty($status)) {
$this->status = $status->toArray();
}
}
/**
* Returns the current entity as an array
*
* @return array
*/
public function toArray(): array
{
$notification = parent::toArray();
if (!$notification['status']) {
unset($notification['status']);
}
return $notification;
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon;
use Friendica\App\BaseURL;
use Friendica\BaseDataTransferObject;
/**
* Class Preferences
*
* @see https://docs.joinmastodon.org/entities/preferences/
*/
class Preferences extends BaseDataTransferObject
{
// /** @var string (Enumerable, oneOf) */
// protected $posting_default_visibility;
// /** @var bool */
// protected $posting_default_sensitive;
// /** @var string (ISO 639-1 language two-letter code), or null*/
// protected $posting_default_language;
// /** @var string (Enumerable, oneOf) */
// protected $reading_expand_media;
// /** @var bool */
// protected $reading_expand_spoilers;
/**
* Creates a preferences record.
*
* @param BaseURL $baseUrl
* @param array $publicContact Full contact table record with uid = 0
* @param array $apcontact Optional full apcontact table record
* @param array $userContact Optional full contact table record with uid != 0
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public function __construct(string $visibility, bool $sensitive, string $language, string $media, bool $spoilers)
{
$this->{'posting:default:visibility'} = $visibility;
$this->{'posting:default:sensitive'} = $sensitive;
$this->{'posting:default:language'} = $language;
$this->{'reading:expand:media'} = $media;
$this->{'reading:expand:spoilers'} = $spoilers;
}
}

View file

@ -126,7 +126,7 @@ class Status extends BaseDataTransferObject
$this->muted = $userAttributes->muted; $this->muted = $userAttributes->muted;
$this->bookmarked = $userAttributes->bookmarked; $this->bookmarked = $userAttributes->bookmarked;
$this->pinned = $userAttributes->pinned; $this->pinned = $userAttributes->pinned;
$this->content = BBCode::convert($item['raw-body'] ?? $item['body'], false); $this->content = BBCode::convert($item['raw-body'] ?? $item['body'], false, BBCode::API);
$this->reblog = $reblog; $this->reblog = $reblog;
$this->application = $application->toArray(); $this->application = $application->toArray();
$this->account = $account->toArray(); $this->account = $account->toArray();
@ -134,7 +134,7 @@ class Status extends BaseDataTransferObject
$this->mentions = $mentions; $this->mentions = $mentions;
$this->tags = $tags; $this->tags = $tags;
$this->emojis = []; $this->emojis = [];
$this->card = $card->toArray(); $this->card = $card->toArray() ?: null;
$this->poll = null; $this->poll = null;
} }

View file

@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (C) 2010-2021, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Object\Api\Mastodon;
use Friendica\BaseDataTransferObject;
use Friendica\Util\DateTimeFormat;
/**
* Class Error
*
* @see https://docs.joinmastodon.org/entities/error
*/
class Token extends BaseDataTransferObject
{
/** @var string */
protected $access_token;
/** @var string */
protected $token_type;
/** @var string */
protected $scope;
/** @var string (Datetime) */
protected $created_at;
/**
* Creates a token record
*
* @param string $access_token
* @param string $token_type
* @param string $scope
* @param string $created_at
*/
public function __construct(string $access_token, string $token_type, string $scope, string $created_at)
{
$this->access_token = $access_token;
$this->token_type = $token_type;
$this->scope = $scope;
$this->created_at = DateTimeFormat::utc($created_at, DateTimeFormat::ATOM);
}
}

View file

@ -211,7 +211,7 @@ class Post
$origin = $item['origin'] || $item['parent-origin']; $origin = $item['origin'] || $item['parent-origin'];
if ($item['pinned']) { if ($item['pinned']) {
$pinned = DI::l10n()->t('pinned item'); $pinned = DI::l10n()->t('Pinned item');
} }
// Showing the one or the other text, depending upon if we can only hide it or really delete it. // Showing the one or the other text, depending upon if we can only hide it or really delete it.
@ -223,9 +223,12 @@ class Post
$drop = [ $drop = [
'dropping' => $dropping, 'dropping' => $dropping,
'pagedrop' => $item['pagedrop'], 'pagedrop' => $item['pagedrop'],
'select' => DI::l10n()->t('Select'), 'select' => DI::l10n()->t('Select'),
'delete' => $delete, 'delete' => $delete,
]; ];
}
if (!$item['self']) {
$block = [ $block = [
'blocking' => true, 'blocking' => true,
'block' => DI::l10n()->t('Block %s', $item['author-name']), 'block' => DI::l10n()->t('Block %s', $item['author-name']),
@ -233,7 +236,7 @@ class Post
]; ];
} }
$filer = (($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) ? DI::l10n()->t("save to folder") : false); $filer = (($conv->getProfileOwner() == local_user() && ($item['uid'] != 0)) ? DI::l10n()->t('Save to folder') : false);
$profile_name = $item['author-name']; $profile_name = $item['author-name'];
if (!empty($item['author-link']) && empty($item['author-name'])) { if (!empty($item['author-link']) && empty($item['author-name'])) {
@ -297,12 +300,12 @@ class Post
$ignored = PostModel\ThreadUser::getIgnored($item['uri-id'], local_user()); $ignored = PostModel\ThreadUser::getIgnored($item['uri-id'], local_user());
if ($item['mention'] || $ignored) { if ($item['mention'] || $ignored) {
$ignore = [ $ignore = [
'do' => DI::l10n()->t("ignore thread"), 'do' => DI::l10n()->t('Ignore thread'),
'undo' => DI::l10n()->t("unignore thread"), 'undo' => DI::l10n()->t('Unignore thread'),
'toggle' => DI::l10n()->t("toggle ignore status"), 'toggle' => DI::l10n()->t('Toggle ignore status'),
'classdo' => $ignored ? "hidden" : "", 'classdo' => $ignored ? "hidden" : "",
'classundo' => $ignored ? "" : "hidden", 'classundo' => $ignored ? "" : "hidden",
'ignored' => DI::l10n()->t('ignored'), 'ignored' => DI::l10n()->t('Ignored'),
]; ];
} }
@ -311,28 +314,28 @@ class Post
$ispinned = ($item['pinned'] ? 'pinned' : 'unpinned'); $ispinned = ($item['pinned'] ? 'pinned' : 'unpinned');
$pin = [ $pin = [
'do' => DI::l10n()->t('pin'), 'do' => DI::l10n()->t('Pin'),
'undo' => DI::l10n()->t('unpin'), 'undo' => DI::l10n()->t('Unpin'),
'toggle' => DI::l10n()->t('toggle pin status'), 'toggle' => DI::l10n()->t('Toggle pin status'),
'classdo' => $item['pinned'] ? 'hidden' : '', 'classdo' => $item['pinned'] ? 'hidden' : '',
'classundo' => $item['pinned'] ? '' : 'hidden', 'classundo' => $item['pinned'] ? '' : 'hidden',
'pinned' => DI::l10n()->t('pinned'), 'pinned' => DI::l10n()->t('Pinned'),
]; ];
} }
$isstarred = (($item['starred']) ? "starred" : "unstarred"); $isstarred = (($item['starred']) ? "starred" : "unstarred");
$star = [ $star = [
'do' => DI::l10n()->t("add star"), 'do' => DI::l10n()->t('Add star'),
'undo' => DI::l10n()->t("remove star"), 'undo' => DI::l10n()->t('Remove star'),
'toggle' => DI::l10n()->t("toggle star status"), 'toggle' => DI::l10n()->t('Toggle star status'),
'classdo' => $item['starred'] ? "hidden" : "", 'classdo' => $item['starred'] ? "hidden" : "",
'classundo' => $item['starred'] ? "" : "hidden", 'classundo' => $item['starred'] ? "" : "hidden",
'starred' => DI::l10n()->t('starred'), 'starred' => DI::l10n()->t('Starred'),
]; ];
$tagger = [ $tagger = [
'add' => DI::l10n()->t("add tag"), 'add' => DI::l10n()->t('Add tag'),
'class' => "", 'class' => "",
]; ];
} }
@ -342,8 +345,8 @@ class Post
} }
if ($conv->isWritable()) { if ($conv->isWritable()) {
$buttons['like'] = [DI::l10n()->t("I like this \x28toggle\x29") , DI::l10n()->t("like")]; $buttons['like'] = [DI::l10n()->t("I like this \x28toggle\x29") , DI::l10n()->t('Like')];
$buttons['dislike'] = [DI::l10n()->t("I don't like this \x28toggle\x29"), DI::l10n()->t("dislike")]; $buttons['dislike'] = [DI::l10n()->t("I don't like this \x28toggle\x29"), DI::l10n()->t('Dislike')];
if ($shareable) { if ($shareable) {
$buttons['share'] = [DI::l10n()->t('Quote share this'), DI::l10n()->t('Quote Share')]; $buttons['share'] = [DI::l10n()->t('Quote share this'), DI::l10n()->t('Quote Share')];
} }
@ -399,7 +402,7 @@ class Post
// Fetching of Diaspora posts doesn't always work. There are issues with reshares and possibly comments // Fetching of Diaspora posts doesn't always work. There are issues with reshares and possibly comments
if (($item['network'] != Protocol::DIASPORA) && empty($comment) && !empty(Session::get('remote_comment'))) { if (($item['network'] != Protocol::DIASPORA) && empty($comment) && !empty(Session::get('remote_comment'))) {
$remote_comment = [DI::l10n()->t('Comment this item on your system'), DI::l10n()->t('remote comment'), $remote_comment = [DI::l10n()->t('Comment this item on your system'), DI::l10n()->t('Remote comment'),
str_replace('{uri}', urlencode($item['uri']), Session::get('remote_comment'))]; str_replace('{uri}', urlencode($item['uri']), Session::get('remote_comment'))];
} else { } else {
$remote_comment = ''; $remote_comment = '';
@ -424,6 +427,8 @@ class Post
$tmp_item = [ $tmp_item = [
'template' => $this->getTemplate(), 'template' => $this->getTemplate(),
'type' => implode("", array_slice(explode("/", $item['verb']), -1)), 'type' => implode("", array_slice(explode("/", $item['verb']), -1)),
'comment_firstcollapsed' => false,
'comment_lastcollapsed' => false,
'suppress_tags' => DI::config()->get('system', 'suppress_tags'), 'suppress_tags' => DI::config()->get('system', 'suppress_tags'),
'tags' => $tags['tags'], 'tags' => $tags['tags'],
'hashtags' => $tags['hashtags'], 'hashtags' => $tags['hashtags'],
@ -540,10 +545,7 @@ class Post
} }
} }
if ($this->isToplevel()) { $result['total_comments_num'] = $this->isToplevel() ? $total_children : 0;
$result['total_comments_num'] = "$total_children";
$result['total_comments_text'] = DI::l10n()->tt('comment', 'comments', $total_children);
}
$result['private'] = $item['private']; $result['private'] = $item['private'];
$result['toplevel'] = ($this->isToplevel() ? 'toplevel_item' : ''); $result['toplevel'] = ($this->isToplevel() ? 'toplevel_item' : '');
@ -884,8 +886,13 @@ class Post
$terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); $terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]);
foreach ($terms as $term) { foreach ($terms as $term) {
if (!$term['url']) {
DI::logger()->warning('Mention term with no URL', ['term' => $term]);
continue;
}
$profile = Contact::getByURL($term['url'], false, ['addr', 'contact-type']); $profile = Contact::getByURL($term['url'], false, ['addr', 'contact-type']);
if (!empty($profile['addr']) && ((($profile['contact-type'] ?? '') ?: Contact::TYPE_UNKNOWN) != Contact::TYPE_COMMUNITY) && if (!empty($profile['addr']) && (($profile['contact-type'] ?? Contact::TYPE_UNKNOWN) != Contact::TYPE_COMMUNITY) &&
($profile['addr'] != $owner['addr']) && !strstr($text, $profile['addr'])) { ($profile['addr'] != $owner['addr']) && !strstr($text, $profile['addr'])) {
$text .= '@' . $profile['addr'] . ' '; $text .= '@' . $profile['addr'] . ' ';
} }
@ -942,9 +949,9 @@ class Post
'$qcomment' => $qcomment, '$qcomment' => $qcomment,
'$default' => $default_text, '$default' => $default_text,
'$profile_uid' => $uid, '$profile_uid' => $uid,
'$mylink' => DI::baseUrl()->remove($a->contact['url']), '$mylink' => DI::baseUrl()->remove($a->contact['url'] ?? ''),
'$mytitle' => DI::l10n()->t('This is you'), '$mytitle' => DI::l10n()->t('This is you'),
'$myphoto' => DI::baseUrl()->remove($a->contact['thumb']), '$myphoto' => DI::baseUrl()->remove($a->contact['thumb'] ?? ''),
'$comment' => DI::l10n()->t('Comment'), '$comment' => DI::l10n()->t('Comment'),
'$submit' => DI::l10n()->t('Submit'), '$submit' => DI::l10n()->t('Submit'),
'$loading' => DI::l10n()->t('Loading...'), '$loading' => DI::l10n()->t('Loading...'),

View file

@ -21,7 +21,6 @@
namespace Friendica\Protocol\ActivityPub; namespace Friendica\Protocol\ActivityPub;
use Friendica\Content\PageInfo;
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML; use Friendica\Content\Text\HTML;
use Friendica\Content\Text\Markdown; use Friendica\Content\Text\Markdown;
@ -464,6 +463,7 @@ class Processor
if (!empty($activity['source'])) { if (!empty($activity['source'])) {
$item['body'] = $activity['source']; $item['body'] = $activity['source'];
$item['raw-body'] = $content; $item['raw-body'] = $content;
$item['body'] = Item::improveSharedDataInBody($item);
} else { } else {
if (empty($activity['directmessage']) && ($item['thr-parent'] != $item['uri']) && ($item['gravity'] == GRAVITY_COMMENT)) { if (empty($activity['directmessage']) && ($item['thr-parent'] != $item['uri']) && ($item['gravity'] == GRAVITY_COMMENT)) {
$item_private = !in_array(0, $activity['item_receiver']); $item_private = !in_array(0, $activity['item_receiver']);

View file

@ -111,9 +111,12 @@ class Receiver
} }
$http_signer = HTTPSignature::getSigner($body, $header); $http_signer = HTTPSignature::getSigner($body, $header);
if (empty($http_signer)) { if ($http_signer === false) {
Logger::warning('Invalid HTTP signature, message will be discarded.'); Logger::warning('Invalid HTTP signature, message will be discarded.');
return; return;
} elseif (empty($http_signer)) {
Logger::info('Signer is a tombstone. The message will be discarded, the signer account is deleted.');
return;
} else { } else {
Logger::info('Valid HTTP signature', ['signer' => $http_signer]); Logger::info('Valid HTTP signature', ['signer' => $http_signer]);
} }

View file

@ -1260,53 +1260,60 @@ class Transmitter
{ {
$attachments = []; $attachments = [];
// Currently deactivated, since it creates side effects on Mastodon and Pleroma. $uriids = [$item['uri-id']];
// It will be reactivated, once this cleared. $shared = BBCode::fetchShareAttributes($item['body']);
/* if (!empty($shared['guid'])) {
$attach_data = BBCode::getAttachmentData($item['body']); $shared_item = Post::selectFirst(['uri-id'], ['guid' => $shared['guid']]);
if (!empty($attach_data['url'])) { if (!empty($shared_item['uri-id'])) {
$attachment = ['type' => 'Page', $uriids[] = $shared_item['uri-id'];
'mediaType' => 'text/html',
'url' => $attach_data['url']];
if (!empty($attach_data['title'])) {
$attachment['name'] = $attach_data['title'];
} }
if (!empty($attach_data['description'])) {
$attachment['summary'] = $attach_data['description'];
}
if (!empty($attach_data['image'])) {
$imgdata = Images::getInfoFromURLCached($attach_data['image']);
if ($imgdata) {
$attachment['icon'] = ['type' => 'Image',
'mediaType' => $imgdata['mime'],
'width' => $imgdata[0],
'height' => $imgdata[1],
'url' => $attach_data['image']];
}
}
$attachments[] = $attachment;
} }
*/
foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]) as $attachment) { $urls = [];
$attachments[] = ['type' => 'Document', foreach ($uriids as $uriid) {
'mediaType' => $attachment['mimetype'], foreach (Post\Media::getByURIId($uriid, [Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) {
'url' => $attachment['url'], if (in_array($attachment['url'], $urls)) {
'name' => $attachment['description']]; continue;
}
$urls[] = $attachment['url'];
$attachments[] = ['type' => 'Document',
'mediaType' => $attachment['mimetype'],
'url' => $attachment['url'],
'name' => $attachment['description']];
}
} }
if ($type != 'Note') { if ($type != 'Note') {
return $attachments; return $attachments;
} }
foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]) as $attachment) { foreach ($uriids as $uriid) {
$attachments[] = ['type' => 'Document', foreach (Post\Media::getByURIId($uriid, [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]) as $attachment) {
'mediaType' => $attachment['mimetype'], if (in_array($attachment['url'], $urls)) {
'url' => $attachment['url'], continue;
'name' => $attachment['description']]; }
$urls[] = $attachment['url'];
$attachments[] = ['type' => 'Document',
'mediaType' => $attachment['mimetype'],
'url' => $attachment['url'],
'name' => $attachment['description']];
}
// Currently deactivated, since it creates side effects on Mastodon and Pleroma.
// It will be activated, once this cleared.
/*
foreach (Post\Media::getByURIId($uriid, [Post\Media::HTML]) as $attachment) {
if (in_array($attachment['url'], $urls)) {
continue;
}
$urls[] = $attachment['url'];
$attachments[] = ['type' => 'Page',
'mediaType' => $attachment['mimetype'],
'url' => $attachment['url'],
'name' => $attachment['description']];
}*/
} }
return $attachments; return $attachments;
@ -1543,7 +1550,7 @@ class Transmitter
$richbody = preg_replace_callback($regexp, ['self', 'mentionCallback'], $item['body']); $richbody = preg_replace_callback($regexp, ['self', 'mentionCallback'], $item['body']);
$richbody = BBCode::removeAttachment($richbody); $richbody = BBCode::removeAttachment($richbody);
$data['contentMap'][$language] = BBCode::convert($richbody, false); $data['contentMap'][$language] = BBCode::convert($richbody, false, BBCode::EXTERNAL);
} }
$data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"]; $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"];

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