Compare commits

...

74 commits

Author SHA1 Message Date
Hypolite Petovan b87588e371 Merge pull request 'Tumblr/Bluesky: Avoid problems on first fetch' (#1452) from heluecht/friendica-addons:first-fetch into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1452
2023-12-21 13:45:24 +01:00
Michael 96c70489f5 Tumblr/Bluesky: Avoid problems on first fetch 2023-12-21 05:23:38 +00:00
Hypolite Petovan 77ad52d1f4 Merge pull request 'Bluesky/Tumblr: Set "received" to "created" if fetched after previous poll' (#1451) from heluecht/friendica-addons:received into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1451
2023-12-21 05:21:43 +01:00
Michael 96a354bc65 Bluesky/Tumblr: Set "received" to "created" if fetched after previous poll 2023-12-20 20:46:24 +00:00
heluecht f32c90dc9f Merge pull request '[s3_storage] Bump version of akeeba/s3 to version 2.3.1' (#1450) from MrPetovan/friendica-addons:bug/deprecated into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1450
2023-12-20 14:17:01 +01:00
Hypolite Petovan 3e74af9775 [s3_storage] Bump version of akeeba/s3 to version 2.3.1
- Address https://github.com/friendica/friendica/issues/12011#issuecomment-1854681792
2023-12-18 21:28:16 -05:00
Hypolite Petovan 9daa11eb10 [woodpecker] Remove PHP 7.3 PHPUnit instance
- Friendica now supports at least PHP 7.4
2023-12-18 21:27:36 -05:00
Hypolite Petovan 011edb711c Merge pull request 'Invidious: The addon is now user definable' (#1449) from heluecht/friendica-addons:invidious-user into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1449
2023-12-18 19:33:55 +01:00
Michael cabdd924d0 Fix white spaces 2023-12-18 17:51:34 +00:00
Michael 8ba44cf5c6 Invidious: The addon is now user definable 2023-12-18 17:47:19 +00:00
heluecht dad3d477d3 Merge pull request 'translation updates' (#1448) from tobias/friendica-addons:20231216-lng into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1448
2023-12-16 15:57:57 +01:00
Tobias Diekershoff 3a063f999d translation updates 2023-12-16 08:59:07 +01:00
Hypolite Petovan fa01357445 Merge pull request 'Tumblr: Improved error handling whe fetching blog information' (#1447) from heluecht/friendica-addons:tumblr-warning into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1447
2023-12-11 14:21:12 +01:00
Michael a41a676bfb Tumblr: Improved error handling whe fetching blog information 2023-12-11 14:21:12 +01:00
Hypolite Petovan 3b518462ab Merge pull request 'This addon will replace "youtube.com" with the chosen Invidious instance' (#1441) from loma-one/friendica-addons:develop into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1441
2023-12-08 20:50:22 +01:00
loma-one d53ad98af2 invidious/invidious.php aktualisiert 2023-12-08 20:50:22 +01:00
loma-one 372e75a91c invidious/invidious.php aktualisiert 2023-12-08 20:50:22 +01:00
loma-one f45f6ba992 https://youtu.be Link
Many thanks for the hint. With a small change '/watch?v=' the link to https://youtu.be now also works
2023-12-08 20:50:22 +01:00
loma-one 26983977c4 This addon will replace "youtube.com" with the chosen Invidious instance
Suggestion from @heluecht for combined URLs adopted
2023-12-08 20:50:22 +01:00
loma-one 90d897f4fa This addon will replace "youtube.com" with the chosen Invidious instance
URL combine
2023-12-08 20:50:22 +01:00
loma-one 668ea972cc invidious/invidious.php aktualisiert 2023-12-08 20:50:22 +01:00
loma-one dc2d00b6c6 invidious/lang/C/messages.po aktualisiert 2023-12-08 20:50:22 +01:00
loma-one 0f65c23490 invidious/invidious.php aktualisiert
Redirects from youtu.be do not work reliably. Therefore this one has been removed.
2023-12-08 20:50:22 +01:00
loma-one c98caaf417 Dateien nach "invidious" hochladen 2023-12-08 20:50:22 +01:00
loma-one f46980c736 Dateien nach "invidious/lang/C" hochladen 2023-12-08 20:50:22 +01:00
loma-one ebf5ff1276 Dateien nach "invidious/templates" hochladen 2023-12-08 20:50:22 +01:00
loma-one 9d932e6fa0 Dateien nach "invidious" hochladen
Replaces links to youtube.com to an invidious instance in all displays of postings on a node.
2023-12-08 20:50:22 +01:00
loma-one 46fdcc1c0e invidious gelöscht 2023-12-08 20:50:22 +01:00
loma-one eadbcc069f invidious hinzugefügt 2023-12-08 20:50:22 +01:00
loma-one 2c2a813324 [pageheader] Improve visibility
Removed commented out code from your pull request
2023-12-08 20:50:22 +01:00
loma-one 9315b185e8 pageheader/pageheader.css aktualisiert 2023-12-08 20:50:22 +01:00
loma-one d685663ac0 Coloured box added 2023-12-08 20:50:22 +01:00
loma-one 727eca1ce7 Coloured box added
Among other things, I use the page header to inform about current maintenance work or other upcoming work. The information should therefore be provided within an appropriate framework. With a little CSS, the page header gets a frame in green. The font was adjusted to an appropriate size.
2023-12-08 20:50:22 +01:00
Hypolite Petovan 3b5e8901dc Merge pull request 'Bluesky: remove @ and spaces from the handle' (#1444) from heluecht/friendica-addons:bluesky-trim-handle into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1444
2023-12-07 13:07:42 +01:00
Michael 50d8d44489 Bluesky: remove @ and spaces from the handle 2023-12-07 12:03:53 +00:00
Hypolite Petovan b6d575c37f Merge pull request 'Bluesky: Improved status on the connector page' (#1443) from heluecht/friendica-addons:bluesky-status into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1443
2023-12-06 15:31:04 +01:00
Michael 397282cbb3 Bluesky: Improved on the connector page 2023-12-06 06:31:52 +00:00
Hypolite Petovan 2c6add7aa1 Merge pull request 'Bluesky: Fix adding a new account' (#1442) from heluecht/friendica-addons:bluesky-auth into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1442
2023-12-04 21:31:59 +01:00
Michael 22bf23b833 Bluesky: Fix adding a new account 2023-12-04 20:29:31 +00:00
Hypolite Petovan ed8c5945da Merge pull request 'Bluesky: Provide the correct user id while fetching content' (#1439) from heluecht/friendica-addons:bluesky-notices into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1439
2023-11-25 23:06:40 +01:00
Michael 14fd900628 Store hash tags 2023-11-25 22:00:45 +00:00
Michael 48cde643f6 Improved logging message 2023-11-25 19:02:10 +00:00
Michael e62f6a9586 Bluesky: Provide the correct user id while fetching content 2023-11-25 18:57:03 +00:00
Hypolite Petovan c2dfda5d72 Merge pull request 'Bluesky: Tags are now supported' (#1438) from heluecht/friendica-addons:bluesky-tag into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1438
2023-11-21 16:50:06 +01:00
Michael 9595760800 Bluesky: Tags are now supported 2023-11-20 21:07:09 +00:00
Hypolite Petovan 1c91ee200e Merge pull request 'Bluesky: Support personal data servers' (#1437) from heluecht/friendica-addons:bluesky-pds into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1437
2023-11-20 00:48:07 +01:00
Michael 00e30b5c2b Bluesky: Support personal data servers 2023-11-19 18:55:05 +00:00
heluecht 5f5c53ab49 Merge pull request '[advancedcontentfilter] Fix obsolete reference to Repository\PostMedia->splitAttachments' (#1436) from MrPetovan/friendica-addons:bug/1434-advancedcontentfilter-splitAttachments into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1436
2023-11-15 20:05:31 +01:00
Benjamin Lorteau 6a46d05bca [advancedcontentfilter] Fix obsolete reference to Repository\PostMedia->splitAttachments 2023-11-14 16:25:06 -05:00
Hypolite Petovan 8d3d0f267b Merge pull request 'Bluesky: Support for transmitted languages' (#1435) from heluecht/friendica-addons:languages into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1435
2023-11-13 01:15:45 +01:00
Michael 66fdd31915 Bluesky: Support for transmitted languages 2023-11-11 05:30:07 +00:00
Hypolite Petovan 607cc9238c Merge pull request 'CLD2: Use ISO-639-1 for the language detection' (#1433) from heluecht/friendica-addons:ISO-639-1 into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1433
2023-11-03 19:07:04 +01:00
Michael 2a782b512e CLD2: Use ISO-639-1 for the language detection 2023-11-02 22:54:19 +00:00
Hypolite Petovan a75c9ba373 Merge pull request 'Bluesky: Fix warnings' (#1432) from heluecht/friendica-addons:warnings into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1432
2023-10-29 16:11:19 +01:00
Michael 77765ff6ed Bluesky: Fix warnings 2023-10-29 16:11:19 +01:00
heluecht 9c53c0c8d1 Merge pull request '[smileybutton] Add explicit conversion from float to int' (#1431) from warnings into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1431
2023-10-29 12:40:31 +01:00
Hypolite Petovan 43c46ae6d9 [smileybutton] Add explicit conversion from float to int
Address part of https://github.com/friendica/friendica/issues/13157#issuecomment-1771572442
2023-10-29 12:40:31 +01:00
Hypolite Petovan c7e06bfa53 Merge pull request 'Langfilter: Use two letter code for the language / Bluesky: Remove callstack' (#1430) from heluecht/friendica-addons:callstack-language into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1430
2023-10-18 22:30:52 +02:00
Michael 6948a15f1c Langfilter: Use two letter code for the language / Bluesky: Remove callstack 2023-10-18 22:30:52 +02:00
Hypolite Petovan 74c56c32b0 Merge pull request 'Upgrade PHP version in CI' (#1429) from nupplaPhil/friendica-addons:feat/phpunit_upgrade into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1429
2023-10-13 15:21:46 +02:00
Philipp Holzer 9bdaa8092e
Upgrade phpunit version in PHP-CI 2023-10-12 21:33:03 +02:00
Hypolite Petovan b11538d195 Merge pull request 'CLD: Keep the original detected language array' (#1428) from heluecht/friendica-addons:cld into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1428
2023-10-12 13:36:58 +02:00
Michael 73c6a0ff0c CLD: Keep the original detected language array 2023-10-11 18:57:04 +00:00
Hypolite Petovan fbafa80815 Merge pull request 'CLD: New plugin for language detection via CLD2' (#1425) from heluecht/friendica-addons:cld2 into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1425
2023-10-07 07:07:00 +02:00
Michael 92251f4a6c Updated CLD installation description 2023-10-07 07:07:00 +02:00
Michael 18266ea6ef Changed hook parameter / more languages added 2023-10-07 07:07:00 +02:00
Michael 80ce855189 Renamed hook 2023-10-07 07:07:00 +02:00
Michael 0eda161e04 Cleaned up code 2023-10-07 07:07:00 +02:00
Michael 981e6821d0 CLD: New plugin for language detection via CLD2 2023-10-07 07:07:00 +02:00
Philipp Holzer a5ed02ed23 Merge pull request '[CI/CD] Kick CI again' (#1427) from nupplaPhil/friendica-addons:2023.09-rc into 2023.09-rc
Reviewed-on: friendica/friendica-addons#1427
2023-10-05 22:10:33 +02:00
Philipp Holzer 7a8f8fcbd2
[CI/CD] Kick CI again 2023-10-05 21:58:35 +02:00
Philipp Holzer 30b9f73f5e
[CI/CD] Kick CI again 2023-10-05 21:57:17 +02:00
Hypolite Petovan be8d8b9c10 Merge pull request 'Bluesky: Fix some issues when fetching posts' (#1424) from heluecht/friendica-addons:bluesky-fixes into develop
Reviewed-on: friendica/friendica-addons#1424
2023-10-03 03:54:41 +02:00
Michael 16d99dbdfc Bluesky: Fix some issues when fetching posts 2023-10-01 04:37:11 +00:00
147 changed files with 3207 additions and 2809 deletions

View file

@ -1,15 +1,13 @@
matrix: matrix:
include: include:
- PHP_MAJOR_VERSION: 7.3
PHP_VERSION: 7.3.33
- PHP_MAJOR_VERSION: 7.4 - PHP_MAJOR_VERSION: 7.4
PHP_VERSION: 7.4.33 PHP_VERSION: 7.4.33
- PHP_MAJOR_VERSION: 8.0 - PHP_MAJOR_VERSION: 8.0
PHP_VERSION: 8.0.29 PHP_VERSION: 8.0.30
- PHP_MAJOR_VERSION: 8.1 - PHP_MAJOR_VERSION: 8.1
PHP_VERSION: 8.1.21 PHP_VERSION: 8.1.23
- PHP_MAJOR_VERSION: 8.2 - PHP_MAJOR_VERSION: 8.2
PHP_VERSION: 8.2.8 PHP_VERSION: 8.2.11
# This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...) # This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...)
labels: labels:

View file

@ -455,7 +455,7 @@ function advancedcontentfilter_prepare_item_row(array $item_row): array
$item_row['tags'] = $tags['tags']; $item_row['tags'] = $tags['tags'];
$item_row['hashtags'] = $tags['hashtags']; $item_row['hashtags'] = $tags['hashtags'];
$item_row['mentions'] = $tags['mentions']; $item_row['mentions'] = $tags['mentions'];
$item_row['attachments'] = Post\Media::splitAttachments($item_row['uri-id']); $item_row['attachments'] = DI::postMediaRepository()->splitAttachments($item_row['uri-id']);
return $item_row; return $item_row;
} }

View file

@ -32,15 +32,16 @@ use Friendica\Core\Hook;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Core\Worker; use Friendica\Core\Worker;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\GServer;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\ItemURI; use Friendica\Model\ItemURI;
use Friendica\Model\Photo; use Friendica\Model\Photo;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Object\Image; use Friendica\Object\Image;
@ -50,9 +51,23 @@ use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings; use Friendica\Util\Strings;
const BLUESKY_DEFAULT_POLL_INTERVAL = 10; // given in minutes const BLUESKY_DEFAULT_POLL_INTERVAL = 10; // given in minutes
const BLUESKY_HOST = 'https://bsky.app'; // Hard wired until Bluesky will run on multiple systems
const BLUESKY_IMAGE_SIZE = [1000000, 500000, 100000, 50000]; const BLUESKY_IMAGE_SIZE = [1000000, 500000, 100000, 50000];
const BLUEKSY_STATUS_UNKNOWN = 0;
const BLUEKSY_STATUS_TOKEN_OK = 1;
const BLUEKSY_STATUS_SUCCESS = 2;
const BLUEKSY_STATUS_API_FAIL = 10;
const BLUEKSY_STATUS_DID_FAIL = 11;
const BLUEKSY_STATUS_PDS_FAIL = 12;
const BLUEKSY_STATUS_TOKEN_FAIL = 13;
/*
* (Currently) hard wired paths for Bluesky services
*/
const BLUESKY_DIRECTORY = 'https://plc.directory'; // Path to the directory server service to fetch the PDS of a given DID
const BLUESKY_PDS = 'https://bsky.social'; // Path to the personal data server service (PDS) to fetch the DID for a given handle
const BLUESKY_WEB = 'https://bsky.app'; // Path to the web interface with the user profile and posts
function bluesky_install() function bluesky_install()
{ {
Hook::register('load_config', __FILE__, 'bluesky_load_config'); Hook::register('load_config', __FILE__, 'bluesky_load_config');
@ -107,8 +122,8 @@ function bluesky_probe_detect(array &$hookData)
if (parse_url($hookData['uri'], PHP_URL_SCHEME) == 'did') { if (parse_url($hookData['uri'], PHP_URL_SCHEME) == 'did') {
$did = $hookData['uri']; $did = $hookData['uri'];
} elseif (preg_match('#^' . BLUESKY_HOST . '/profile/(.+)#', $hookData['uri'], $matches)) { } elseif (preg_match('#^' . BLUESKY_WEB . '/profile/(.+)#', $hookData['uri'], $matches)) {
$did = bluesky_get_did($pconfig['uid'], $matches[1]); $did = bluesky_get_did($matches[1]);
if (empty($did)) { if (empty($did)) {
return; return;
} }
@ -128,6 +143,8 @@ function bluesky_probe_detect(array &$hookData)
$hookData['result'] = bluesky_get_contact_fields($data, 0, false); $hookData['result'] = bluesky_get_contact_fields($data, 0, false);
$hookData['result']['baseurl'] = bluesky_get_pds($did);
// Preparing probe data. This differs slightly from the contact array // Preparing probe data. This differs slightly from the contact array
$hookData['result']['about'] = HTML::toBBCode($data->description ?? ''); $hookData['result']['about'] = HTML::toBBCode($data->description ?? '');
$hookData['result']['photo'] = $data->avatar ?? ''; $hookData['result']['photo'] = $data->avatar ?? '';
@ -153,11 +170,11 @@ function bluesky_item_by_link(array &$hookData)
return; return;
} }
if (!preg_match('#^' . BLUESKY_HOST . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) { if (!preg_match('#^' . BLUESKY_WEB . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) {
return; return;
} }
$did = bluesky_get_did($hookData['uid'], $matches[1]); $did = bluesky_get_did($matches[1]);
if (empty($did)) { if (empty($did)) {
return; return;
} }
@ -166,7 +183,7 @@ function bluesky_item_by_link(array &$hookData)
$uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches[2]; $uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches[2];
$uri = bluesky_fetch_missing_post($uri, $hookData['uid'], 0, 0); $uri = bluesky_fetch_missing_post($uri, $hookData['uid'], $hookData['uid'], 0, 0);
Logger::debug('Got post', ['profile' => $matches[1], 'cid' => $matches[2], 'result' => $uri]); Logger::debug('Got post', ['profile' => $matches[1], 'cid' => $matches[2], 'result' => $uri]);
if (!empty($uri)) { if (!empty($uri)) {
$item = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $hookData['uid']]); $item = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $hookData['uid']]);
@ -307,26 +324,24 @@ function bluesky_settings(array &$data)
$enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post') ?? false; $enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post') ?? false;
$def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default') ?? false; $def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default') ?? false;
$host = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'host') ?: 'https://bsky.social'; $pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
$handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'); $handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle');
$did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did'); $did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
$token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token'); $token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token');
$import = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import') ?? false; $import = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import') ?? false;
$import_feeds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds') ?? false; $import_feeds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds') ?? false;
$status = $token ? DI::l10n()->t("You are authenticated to Bluesky. For security reasons the password isn't stored.") : DI::l10n()->t('You are not authenticated. Please enter the app password.');
$t = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/bluesky/'); $t = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/bluesky/');
$html = Renderer::replaceMacros($t, [ $html = Renderer::replaceMacros($t, [
'$enable' => ['bluesky', DI::l10n()->t('Enable Bluesky Post Addon'), $enabled], '$enable' => ['bluesky', DI::l10n()->t('Enable Bluesky Post Addon'), $enabled],
'$bydefault' => ['bluesky_bydefault', DI::l10n()->t('Post to Bluesky by default'), $def_enabled], '$bydefault' => ['bluesky_bydefault', DI::l10n()->t('Post to Bluesky by default'), $def_enabled],
'$import' => ['bluesky_import', DI::l10n()->t('Import the remote timeline'), $import], '$import' => ['bluesky_import', DI::l10n()->t('Import the remote timeline'), $import],
'$import_feeds' => ['bluesky_import_feeds', DI::l10n()->t('Import the pinned feeds'), $import_feeds, DI::l10n()->t('When activated, Posts will be imported from all the feeds that you pinned in Bluesky.')], '$import_feeds' => ['bluesky_import_feeds', DI::l10n()->t('Import the pinned feeds'), $import_feeds, DI::l10n()->t('When activated, Posts will be imported from all the feeds that you pinned in Bluesky.')],
'$host' => ['bluesky_host', DI::l10n()->t('Bluesky host'), $host, '', '', 'readonly'], '$pds' => ['bluesky_pds', DI::l10n()->t('Personal Data Server'), $pds, DI::l10n()->t('The personal data server (PDS) is the system that hosts your profile.'), '', 'readonly'],
'$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle], '$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle],
'$did' => ['bluesky_did', DI::l10n()->t('Bluesky DID'), $did, DI::l10n()->t('This is the unique identifier. It will be fetched automatically, when the handle is entered.'), '', 'readonly'], '$did' => ['bluesky_did', DI::l10n()->t('Bluesky DID'), $did, DI::l10n()->t('This is the unique identifier. It will be fetched automatically, when the handle is entered.'), '', 'readonly'],
'$password' => ['bluesky_password', DI::l10n()->t('Bluesky app password'), '', DI::l10n()->t("Please don't add your real password here, but instead create a specific app password in the Bluesky settings.")], '$password' => ['bluesky_password', DI::l10n()->t('Bluesky app password'), '', DI::l10n()->t("Please don't add your real password here, but instead create a specific app password in the Bluesky settings.")],
'$status' => $status '$status' => bluesky_get_status($handle, $did, $pds, $token),
]); ]);
$data = [ $data = [
@ -338,35 +353,92 @@ function bluesky_settings(array &$data)
]; ];
} }
function bluesky_get_status(string $handle = null, string $did = null, string $pds = null, string $token = null): string
{
if (empty($handle)) {
return DI::l10n()->t('You are not authenticated. Please enter your handle and the app password.');
}
$status = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'status') ?? BLUEKSY_STATUS_UNKNOWN;
// Fallback mechanism for connection that had been established before the introduction of the status
if ($status == BLUEKSY_STATUS_UNKNOWN) {
if (empty($did)) {
$status = BLUEKSY_STATUS_DID_FAIL;
} elseif (empty($pds)) {
$status = BLUEKSY_STATUS_PDS_FAIL;
} elseif (!empty($token)) {
$status = BLUEKSY_STATUS_TOKEN_OK;
} else {
$status = BLUEKSY_STATUS_TOKEN_FAIL;
}
}
switch ($status) {
case BLUEKSY_STATUS_TOKEN_OK:
return DI::l10n()->t("You are authenticated to Bluesky. For security reasons the password isn't stored.");
case BLUEKSY_STATUS_SUCCESS:
return DI::l10n()->t('The communication with the personal data server service (PDS) is established.');
case BLUEKSY_STATUS_API_FAIL;
return DI::l10n()->t('Communication issues with the personal data server service (PDS).');
case BLUEKSY_STATUS_DID_FAIL:
return DI::l10n()->t('The DID for the provided handle could not be detected. Please check if you entered the correct handle.');
case BLUEKSY_STATUS_PDS_FAIL:
return DI::l10n()->t('The personal data server service (PDS) could not be detected.');
case BLUEKSY_STATUS_TOKEN_FAIL:
return DI::l10n()->t('The authentication with the provided handle and password failed. Please check if you entered the correct password.');
default:
return '';
}
}
function bluesky_settings_post(array &$b) function bluesky_settings_post(array &$b)
{ {
if (empty($_POST['bluesky-submit'])) { if (empty($_POST['bluesky-submit'])) {
return; return;
} }
$old_host = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'host'); $old_pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
$old_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'); $old_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle');
$old_did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did'); $old_did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
$host = $_POST['bluesky_host']; $handle = trim($_POST['bluesky_handle'], ' @');
$handle = $_POST['bluesky_handle'];
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post', intval($_POST['bluesky'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post', intval($_POST['bluesky']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default', intval($_POST['bluesky_bydefault'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default', intval($_POST['bluesky_bydefault']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'host', $host);
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'handle', $handle); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'handle', $handle);
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import', intval($_POST['bluesky_import'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import', intval($_POST['bluesky_import']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds', intval($_POST['bluesky_import_feeds'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds', intval($_POST['bluesky_import_feeds']));
if (!empty($host) && !empty($handle)) { if (!empty($handle)) {
if (empty($old_did) || $old_host != $host || $old_handle != $handle) { if (empty($old_did) || $old_handle != $handle) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'did', bluesky_get_did(DI::userSession()->getLocalUserId(), DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'))); $did = bluesky_get_did(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'));
if (empty($did)) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'status', BLUEKSY_STATUS_DID_FAIL);
}
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'did', $did);
} else {
$did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
}
if (!empty($did) && (empty($old_pds) || $old_handle != $handle)) {
$pds = bluesky_get_pds($did);
if (empty($pds)) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'status', BLUEKSY_STATUS_PDS_FAIL);
}
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'pds', $pds);
} else {
$pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
} }
} else { } else {
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'did'); DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token');
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'refresh_token');
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'token_created');
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'status');
} }
if (!empty($_POST['bluesky_password'])) { if (!empty($did) && !empty($pds) && !empty($_POST['bluesky_password'])) {
bluesky_create_token(DI::userSession()->getLocalUserId(), $_POST['bluesky_password']); bluesky_create_token(DI::userSession()->getLocalUserId(), $_POST['bluesky_password']);
} }
} }
@ -391,7 +463,7 @@ function bluesky_jot_nets(array &$jotnets_fields)
function bluesky_cron() function bluesky_cron()
{ {
$last = DI::keyValue()->get('bluesky_last_poll'); $last = (int)DI::keyValue()->get('bluesky_last_poll');
$poll_interval = intval(DI::config()->get('bluesky', 'poll_interval')); $poll_interval = intval(DI::config()->get('bluesky', 'poll_interval'));
if (!$poll_interval) { if (!$poll_interval) {
@ -426,13 +498,13 @@ function bluesky_cron()
// Refresh the token now, so that it doesn't need to be refreshed in parallel by the following workers // Refresh the token now, so that it doesn't need to be refreshed in parallel by the following workers
bluesky_get_token($pconfig['uid']); bluesky_get_token($pconfig['uid']);
Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_timeline.php', $pconfig['uid']); Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_timeline.php', $pconfig['uid'], $last);
Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_notifications.php', $pconfig['uid']); Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_notifications.php', $pconfig['uid'], $last);
if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import_feeds')) { if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import_feeds')) {
$feeds = bluesky_get_feeds($pconfig['uid']); $feeds = bluesky_get_feeds($pconfig['uid']);
foreach ($feeds as $feed) { foreach ($feeds as $feed) {
Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_feed.php', $pconfig['uid'], $feed); Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_feed.php', $pconfig['uid'], $feed, $last);
} }
} }
} }
@ -611,6 +683,13 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
return; return;
} }
// Try to fetch the language from the post itself
if (!empty($item['language'])) {
$language = array_key_first(json_decode($item['language'], true));
} else {
$language = '';
}
$did = DI::pConfig()->get($uid, 'bluesky', 'did'); $did = DI::pConfig()->get($uid, 'bluesky', 'did');
$urls = bluesky_get_urls(Post\Media::removeFromBody($item['body'])); $urls = bluesky_get_urls(Post\Media::removeFromBody($item['body']));
$item['body'] = $urls['body']; $item['body'] = $urls['body'];
@ -622,10 +701,14 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
$record = [ $record = [
'text' => $facets['body'], 'text' => $facets['body'],
'$type' => 'app.bsky.feed.post',
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), 'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => 'app.bsky.feed.post'
]; ];
if (!empty($language)) {
$record['langs'] = [$language];
}
if (!empty($facets['facets'])) { if (!empty($facets['facets'])) {
$record['facets'] = $facets['facets']; $record['facets'] = $facets['facets'];
} }
@ -672,11 +755,20 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
function bluesky_get_urls(string $body): array function bluesky_get_urls(string $body): array
{ {
// Remove all hashtag and mention links // Remove all hashtag and mention links
$body = preg_replace("/([#@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $body); $body = preg_replace("/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism", '$1$3', $body);
$body = BBCode::expandVideoLinks($body); $body = BBCode::expandVideoLinks($body);
$urls = []; $urls = [];
// Search for hash tags
if (preg_match_all("/#\[url\=(https?:.*?)\](.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$text = '#' . $match[2];
$urls[] = ['tag' => $match[2], 'text' => $text, 'hash' => $text];
$body = str_replace($match[0], $text, $body);
}
}
// Search for pure links // Search for pure links
if (preg_match_all("/\[url\](https?:.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER)) { if (preg_match_all("/\[url\](https?:.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) { foreach ($matches as $match) {
@ -742,9 +834,17 @@ function bluesky_get_facets(string $body, array $urls): array
$facet->index->byteStart = $pos; $facet->index->byteStart = $pos;
$feature = new stdClass; $feature = new stdClass;
$feature->uri = $url['url'];
$type = '$type'; $type = '$type';
if (!empty($url['tag'])) {
$feature->tag = $url['tag'];
$feature->$type = 'app.bsky.richtext.facet#tag';
} elseif (!empty($url['url'])) {
$feature->uri = $url['url'];
$feature->$type = 'app.bsky.richtext.facet#link'; $feature->$type = 'app.bsky.richtext.facet#link';
} else {
continue;
}
$facet->features = [$feature]; $facet->features = [$feature];
$facets[] = $facet; $facets[] = $facet;
@ -830,7 +930,7 @@ function bluesky_delete_post(string $uri, int $uid)
Logger::debug('Deleted', ['parts' => $parts]); Logger::debug('Deleted', ['parts' => $parts]);
} }
function bluesky_fetch_timeline(int $uid) function bluesky_fetch_timeline(int $uid, int $last_poll)
{ {
$data = bluesky_xrpc_get($uid, 'app.bsky.feed.getTimeline'); $data = bluesky_xrpc_get($uid, 'app.bsky.feed.getTimeline');
if (empty($data)) { if (empty($data)) {
@ -842,7 +942,7 @@ function bluesky_fetch_timeline(int $uid)
} }
foreach (array_reverse($data->feed) as $entry) { foreach (array_reverse($data->feed) as $entry) {
bluesky_process_post($entry->post, $uid, Item::PR_NONE, 0); bluesky_process_post($entry->post, $uid, Item::PR_NONE, 0, $last_poll);
if (!empty($entry->reason)) { if (!empty($entry->reason)) {
bluesky_process_reason($entry->reason, bluesky_get_uri($entry->post), $uid); bluesky_process_reason($entry->reason, bluesky_get_uri($entry->post), $uid);
} }
@ -893,7 +993,7 @@ function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
} }
} }
function bluesky_fetch_notifications(int $uid) function bluesky_fetch_notifications(int $uid, int $last_poll)
{ {
$data = bluesky_xrpc_get($uid, 'app.bsky.notification.listNotifications'); $data = bluesky_xrpc_get($uid, 'app.bsky.notification.listNotifications');
if (empty($data->notifications)) { if (empty($data->notifications)) {
@ -912,7 +1012,7 @@ function bluesky_fetch_notifications(int $uid)
$item['gravity'] = Item::GRAVITY_ACTIVITY; $item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::LIKE; $item['body'] = $item['verb'] = Activity::LIKE;
$item['thr-parent'] = bluesky_get_uri($notification->record->subject); $item['thr-parent'] = bluesky_get_uri($notification->record->subject);
$item['thr-parent'] = bluesky_fetch_missing_post($item['thr-parent'], $uid, $item['contact-id'], 0); $item['thr-parent'] = bluesky_fetch_missing_post($item['thr-parent'], $uid, $uid, $item['contact-id'], 0, $last_poll);
if (!empty($item['thr-parent'])) { if (!empty($item['thr-parent'])) {
$data = Item::insert($item); $data = Item::insert($item);
Logger::debug('Got like', ['uid' => $uid, 'result' => $data, 'uri' => $uri]); Logger::debug('Got like', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
@ -926,7 +1026,7 @@ function bluesky_fetch_notifications(int $uid)
$item['gravity'] = Item::GRAVITY_ACTIVITY; $item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::ANNOUNCE; $item['body'] = $item['verb'] = Activity::ANNOUNCE;
$item['thr-parent'] = bluesky_get_uri($notification->record->subject); $item['thr-parent'] = bluesky_get_uri($notification->record->subject);
$item['thr-parent'] = bluesky_fetch_missing_post($item['thr-parent'], $uid, $item['contact-id'], 0); $item['thr-parent'] = bluesky_fetch_missing_post($item['thr-parent'], $uid, $uid, $item['contact-id'], 0, $last_poll);
if (!empty($item['thr-parent'])) { if (!empty($item['thr-parent'])) {
$data = Item::insert($item); $data = Item::insert($item);
Logger::debug('Got repost', ['uid' => $uid, 'result' => $data, 'uri' => $uri]); Logger::debug('Got repost', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
@ -941,17 +1041,17 @@ function bluesky_fetch_notifications(int $uid)
break; break;
case 'mention': case 'mention':
$data = bluesky_process_post($notification, $uid, Item::PR_PUSHED, 0); $data = bluesky_process_post($notification, $uid, Item::PR_PUSHED, 0, $last_poll);
Logger::debug('Got mention', ['uid' => $uid, 'result' => $data, 'uri' => $uri]); Logger::debug('Got mention', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
break; break;
case 'reply': case 'reply':
$data = bluesky_process_post($notification, $uid, Item::PR_PUSHED, 0); $data = bluesky_process_post($notification, $uid, Item::PR_PUSHED, 0, $last_poll);
Logger::debug('Got reply', ['uid' => $uid, 'result' => $data, 'uri' => $uri]); Logger::debug('Got reply', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
break; break;
case 'quote': case 'quote':
$data = bluesky_process_post($notification, $uid, Item::PR_PUSHED, 0); $data = bluesky_process_post($notification, $uid, Item::PR_PUSHED, 0, $last_poll);
Logger::debug('Got quote', ['uid' => $uid, 'result' => $data, 'uri' => $uri]); Logger::debug('Got quote', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
break; break;
@ -962,7 +1062,7 @@ function bluesky_fetch_notifications(int $uid)
} }
} }
function bluesky_fetch_feed(int $uid, string $feed) function bluesky_fetch_feed(int $uid, string $feed, int $last_poll)
{ {
$data = bluesky_xrpc_get($uid, 'app.bsky.feed.getFeed', ['feed' => $feed]); $data = bluesky_xrpc_get($uid, 'app.bsky.feed.getFeed', ['feed' => $feed]);
if (empty($data)) { if (empty($data)) {
@ -983,15 +1083,22 @@ function bluesky_fetch_feed(int $uid, string $feed)
} }
foreach (array_reverse($data->feed) as $entry) { foreach (array_reverse($data->feed) as $entry) {
if (!Relay::isWantedLanguage($entry->post->record->text)) { $contact = bluesky_get_contact($entry->post->author, 0, $uid);
$languages = $entry->post->record->langs ?? [];
if (!Relay::isWantedLanguage($entry->post->record->text, 0, $contact['id'] ?? 0, $languages)) {
Logger::debug('Unwanted language detected', ['text' => $entry->post->record->text]); Logger::debug('Unwanted language detected', ['text' => $entry->post->record->text]);
continue; continue;
} }
$id = bluesky_process_post($entry->post, $uid, Item::PR_TAG, 0); $id = bluesky_process_post($entry->post, $uid, Item::PR_TAG, 0, $last_poll);
if (!empty($id)) { if (!empty($id)) {
$post = Post::selectFirst(['uri-id'], ['id' => $id]); $post = Post::selectFirst(['uri-id'], ['id' => $id]);
if (!empty($post['uri-id'])) {
$stored = Post\Category::storeFileByURIId($post['uri-id'], $uid, Post\Category::SUBCRIPTION, $feedname, $feedurl); $stored = Post\Category::storeFileByURIId($post['uri-id'], $uid, Post\Category::SUBCRIPTION, $feedname, $feedurl);
Logger::debug('Stored tag subscription for user', ['uri-id' => $post['uri-id'], 'uid' => $uid, 'name' => $feedname, 'url' => $feedurl, 'stored' => $stored]); Logger::debug('Stored tag subscription for user', ['uri-id' => $post['uri-id'], 'uid' => $uid, 'name' => $feedname, 'url' => $feedurl, 'stored' => $stored]);
} else {
Logger::notice('Post not found', ['id' => $id, 'entry' => $entry]);
}
} }
if (!empty($entry->reason)) { if (!empty($entry->reason)) {
bluesky_process_reason($entry->reason, bluesky_get_uri($entry->post), $uid); bluesky_process_reason($entry->reason, bluesky_get_uri($entry->post), $uid);
@ -999,31 +1106,35 @@ function bluesky_fetch_feed(int $uid, string $feed)
} }
} }
function bluesky_process_post(stdClass $post, int $uid, int $post_reason, $level): int function bluesky_process_post(stdClass $post, int $uid, int $post_reason, int $level, int $last_poll): int
{ {
$uri = bluesky_get_uri($post); $uri = bluesky_get_uri($post);
if ($id = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $uid]) || $id = Post::selectFirst(['id'], ['extid' => $uri, 'uid' => $uid])) { if ($id = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $uid])) {
return $id; return $id['id'];
}
if ($id = Post::selectFirst(['id'], ['extid' => $uri, 'uid' => $uid])) {
return $id['id'];
} }
Logger::debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid, 'root' => $post->record->reply->root ?? '']); Logger::debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid, 'root' => $post->record->reply->root ?? '']);
$item = bluesky_get_header($post, $uri, $uid, $uid); $item = bluesky_get_header($post, $uri, $uid, $uid);
$item = bluesky_get_content($item, $post->record, $uri, $uid, $level); $item = bluesky_get_content($item, $post->record, $uri, $uid, $uid, $level, $last_poll);
if (empty($item)) { if (empty($item)) {
return 0; return 0;
} }
if (!empty($post->embed)) { if (!empty($post->embed)) {
$item = bluesky_add_media($post->embed, $item, $uid, $level); $item = bluesky_add_media($post->embed, $item, $uid, $level, $last_poll);
} }
if (empty($item['post-reason'])) { if (empty($item['post-reason'])) {
$item['post-reason'] = $post_reason; $item['post-reason'] = $post_reason;
} }
return item::insert($item); return Item::insert($item);
} }
function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_uid): array function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_uid): array
@ -1061,7 +1172,7 @@ function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_ui
return $item; return $item;
} }
function bluesky_get_content(array $item, stdClass $record, string $uri, int $uid, int $level): array function bluesky_get_content(array $item, stdClass $record, string $uri, int $uid, int $fetch_uid, int $level, int $last_poll): array
{ {
if (empty($item)) { if (empty($item)) {
return []; return [];
@ -1070,7 +1181,7 @@ function bluesky_get_content(array $item, stdClass $record, string $uri, int $ui
if (!empty($record->reply)) { if (!empty($record->reply)) {
$item['parent-uri'] = bluesky_get_uri($record->reply->root); $item['parent-uri'] = bluesky_get_uri($record->reply->root);
if ($item['parent-uri'] != $uri) { if ($item['parent-uri'] != $uri) {
$item['parent-uri'] = bluesky_fetch_missing_post($item['parent-uri'], $uid, $item['contact-id'], $level); $item['parent-uri'] = bluesky_fetch_missing_post($item['parent-uri'], $uid, $fetch_uid, $item['contact-id'], $level, $last_poll);
if (empty($item['parent-uri'])) { if (empty($item['parent-uri'])) {
return []; return [];
} }
@ -1078,21 +1189,27 @@ function bluesky_get_content(array $item, stdClass $record, string $uri, int $ui
$item['thr-parent'] = bluesky_get_uri($record->reply->parent); $item['thr-parent'] = bluesky_get_uri($record->reply->parent);
if (!in_array($item['thr-parent'], [$uri, $item['parent-uri']])) { if (!in_array($item['thr-parent'], [$uri, $item['parent-uri']])) {
$item['thr-parent'] = bluesky_fetch_missing_post($item['thr-parent'], $uid, $item['contact-id'], $level, $item['parent-uri']); $item['thr-parent'] = bluesky_fetch_missing_post($item['thr-parent'], $uid, $fetch_uid, $item['contact-id'], $level, $last_poll, $item['parent-uri']);
if (empty($item['thr-parent'])) { if (empty($item['thr-parent'])) {
return []; return [];
} }
} }
} }
$item['body'] = bluesky_get_text($record); $item['body'] = bluesky_get_text($record, $item['uri-id']);
$item['created'] = DateTimeFormat::utc($record->createdAt, DateTimeFormat::MYSQL); $item['created'] = DateTimeFormat::utc($record->createdAt, DateTimeFormat::MYSQL);
$item['transmitted-languages'] = $record->langs ?? [];
if (($last_poll != 0) && strtotime($item['created']) > $last_poll) {
$item['received'] = $item['created'];
}
return $item; return $item;
} }
function bluesky_get_text(stdClass $record): string function bluesky_get_text(stdClass $record, int $uri_id): string
{ {
$text = $record->text; $text = $record->text ?? '';
if (empty($record->facets)) { if (empty($record->facets)) {
return $text; return $text;
@ -1129,8 +1246,14 @@ function bluesky_get_text(stdClass $record): string
} }
break; break;
case 'app.bsky.richtext.facet#tag';
Tag::store($uri_id, Tag::HASHTAG, $feature->tag);
$url = DI::baseUrl() . '/search?tag=' . urlencode($feature->tag);
$linktext = '#' . $feature->tag;
break;
default: default:
Logger::notice('Unhandled feature type', ['type' => $feature->$type, 'record' => $record]); Logger::notice('Unhandled feature type', ['type' => $feature->$type, 'feature' => $feature, 'record' => $record]);
break; break;
} }
} }
@ -1141,7 +1264,7 @@ function bluesky_get_text(stdClass $record): string
return $text; return $text;
} }
function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $level): array function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $level, int $last_poll): array
{ {
$type = '$type'; $type = '$type';
switch ($embed->$type) { switch ($embed->$type) {
@ -1178,18 +1301,17 @@ function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $le
break; break;
} }
$shared = bluesky_get_header($embed->record, $uri, 0, $fetch_uid); $shared = bluesky_get_header($embed->record, $uri, 0, $fetch_uid);
$shared = bluesky_get_content($shared, $embed->record->value, $uri, $item['uid'], $level); $shared = bluesky_get_content($shared, $embed->record->value, $uri, $item['uid'], $fetch_uid, $level, $last_poll);
if (!empty($shared)) { if (!empty($shared)) {
if (!empty($embed->record->embeds)) { if (!empty($embed->record->embeds)) {
foreach ($embed->record->embeds as $single) { foreach ($embed->record->embeds as $single) {
$shared = bluesky_add_media($single, $shared, $fetch_uid, $level); $shared = bluesky_add_media($single, $shared, $fetch_uid, $level, $last_poll);
} }
} }
$id = Item::insert($shared); Item::insert($shared);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
} }
} }
if (!empty($shared)) { if (!empty($shared['uri-id'])) {
$item['quote-uri-id'] = $shared['uri-id']; $item['quote-uri-id'] = $shared['uri-id'];
} }
break; break;
@ -1199,24 +1321,22 @@ function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $le
$shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => $item['uid']]); $shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => $item['uid']]);
if (empty($shared)) { if (empty($shared)) {
$shared = bluesky_get_header($embed->record->record, $uri, 0, $fetch_uid); $shared = bluesky_get_header($embed->record->record, $uri, 0, $fetch_uid);
$shared = bluesky_get_content($shared, $embed->record->record->value, $uri, $item['uid'], $level); $shared = bluesky_get_content($shared, $embed->record->record->value, $uri, $item['uid'], $fetch_uid, $level, $last_poll);
if (!empty($shared)) { if (!empty($shared)) {
if (!empty($embed->record->record->embeds)) { if (!empty($embed->record->record->embeds)) {
foreach ($embed->record->record->embeds as $single) { foreach ($embed->record->record->embeds as $single) {
$shared = bluesky_add_media($single, $shared, $fetch_uid, $level); $shared = bluesky_add_media($single, $shared, $fetch_uid, $level, $last_poll);
} }
} }
Item::insert($shared);
$id = Item::insert($shared);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
} }
} }
if (!empty($shared)) { if (!empty($shared['uri-id'])) {
$item['quote-uri-id'] = $shared['uri-id']; $item['quote-uri-id'] = $shared['uri-id'];
} }
if (!empty($embed->media)) { if (!empty($embed->media)) {
$item = bluesky_add_media($embed->media, $item, $fetch_uid, $level); $item = bluesky_add_media($embed->media, $item, $fetch_uid, $level, $last_poll);
} }
break; break;
@ -1230,7 +1350,7 @@ function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $le
function bluesky_get_uri(stdClass $post): string function bluesky_get_uri(stdClass $post): string
{ {
if (empty($post->cid)) { if (empty($post->cid)) {
Logger::info('Invalid URI', ['post' => $post, 'callstack' => System::callstack(10, 0, true)]); Logger::info('Invalid URI', ['post' => $post]);
return ''; return '';
} }
return $post->uri . ':' . $post->cid; return $post->uri . ':' . $post->cid;
@ -1279,7 +1399,7 @@ function bluesky_get_uri_parts(string $uri): ?stdClass
return $class; return $class;
} }
function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, int $level, string $fallback = ''): string function bluesky_fetch_missing_post(string $uri, int $uid, int $fetch_uid, int $causer, int $level, int $last_poll = 0, string $fallback = ''): string
{ {
$fetched_uri = bluesky_fetch_post($uri, $uid); $fetched_uri = bluesky_fetch_post($uri, $uid);
if (!empty($fetched_uri)) { if (!empty($fetched_uri)) {
@ -1297,13 +1417,13 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, int $lev
$fetch_uri = $class->uri; $fetch_uri = $class->uri;
Logger::debug('Fetch missing post', ['level' => $level, 'uid' => $uid, 'uri' => $uri]); Logger::debug('Fetch missing post', ['level' => $level, 'uid' => $uid, 'uri' => $uri]);
$data = bluesky_xrpc_get($uid, 'app.bsky.feed.getPostThread', ['uri' => $fetch_uri]); $data = bluesky_xrpc_get($fetch_uid, 'app.bsky.feed.getPostThread', ['uri' => $fetch_uri]);
if (empty($data)) { if (empty($data)) {
Logger::info('Thread was not fetched', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $fallback]); Logger::info('Thread was not fetched', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $fallback]);
return $fallback; return $fallback;
} }
Logger::debug('Reply count', ['replies' => $data->thread->post->replyCount, 'level' => $level, 'uid' => $uid, 'uri' => $uri]); Logger::debug('Reply count', ['level' => $level, 'uid' => $uid, 'uri' => $uri]);
if ($causer != 0) { if ($causer != 0) {
$cdata = Contact::getPublicAndUserContactID($causer, $uid); $cdata = Contact::getPublicAndUserContactID($causer, $uid);
@ -1311,7 +1431,7 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, int $lev
$cdata = []; $cdata = [];
} }
return bluesky_process_thread($data->thread, $uid, $cdata, $level); return bluesky_process_thread($data->thread, $uid, $fetch_uid, $cdata, $level, $last_poll);
} }
function bluesky_fetch_post(string $uri, int $uid): string function bluesky_fetch_post(string $uri, int $uid): string
@ -1329,14 +1449,19 @@ function bluesky_fetch_post(string $uri, int $uid): string
return ''; return '';
} }
function bluesky_process_thread(stdClass $thread, int $uid, array $cdata, int $level): string function bluesky_process_thread(stdClass $thread, int $uid, int $fetch_uid, array $cdata, int $level, int $last_poll): string
{ {
if (empty($thread->post)) {
Logger::info('Invalid post', ['post' => $thread]);
return '';
}
$uri = bluesky_get_uri($thread->post); $uri = bluesky_get_uri($thread->post);
$fetched_uri = bluesky_fetch_post($uri, $uid); $fetched_uri = bluesky_fetch_post($uri, $uid);
if (empty($fetched_uri)) { if (empty($fetched_uri)) {
Logger::debug('Process missing post', ['uri' => $uri]); Logger::debug('Process missing post', ['uri' => $uri]);
$item = bluesky_get_header($thread->post, $uri, $uid, $uid); $item = bluesky_get_header($thread->post, $uri, $uid, $uid);
$item = bluesky_get_content($item, $thread->post->record, $uri, $uid, $level); $item = bluesky_get_content($item, $thread->post->record, $uri, $uid, $fetch_uid, $level, $last_poll);
if (!empty($item)) { if (!empty($item)) {
$item['post-reason'] = Item::PR_FETCHED; $item['post-reason'] = Item::PR_FETCHED;
@ -1345,7 +1470,7 @@ function bluesky_process_thread(stdClass $thread, int $uid, array $cdata, int $l
} }
if (!empty($thread->post->embed)) { if (!empty($thread->post->embed)) {
$item = bluesky_add_media($thread->post->embed, $item, $uid, $level); $item = bluesky_add_media($thread->post->embed, $item, $uid, $level, $last_poll);
} }
$id = Item::insert($item); $id = Item::insert($item);
if (!$id) { if (!$id) {
@ -1363,7 +1488,7 @@ function bluesky_process_thread(stdClass $thread, int $uid, array $cdata, int $l
} }
foreach ($thread->replies ?? [] as $reply) { foreach ($thread->replies ?? [] as $reply) {
$reply_uri = bluesky_process_thread($reply, $uid, $cdata, $level); $reply_uri = bluesky_process_thread($reply, $uid, $fetch_uid, $cdata, $level, $last_poll);
Logger::debug('Reply has been processed', ['uri' => $uri, 'reply' => $reply_uri]); Logger::debug('Reply has been processed', ['uri' => $uri, 'reply' => $reply_uri]);
} }
@ -1428,10 +1553,9 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): a
'blocked' => false, 'blocked' => false,
'readonly' => false, 'readonly' => false,
'pending' => false, 'pending' => false,
'baseurl' => BLUESKY_HOST,
'url' => $author->did, 'url' => $author->did,
'nurl' => $author->did, 'nurl' => $author->did,
'alias' => BLUESKY_HOST . '/profile/' . $author->handle, 'alias' => BLUESKY_WEB . '/profile/' . $author->handle,
'name' => $author->displayName ?? $author->handle, 'name' => $author->displayName ?? $author->handle,
'nick' => $author->handle, 'nick' => $author->handle,
'addr' => $author->handle, 'addr' => $author->handle,
@ -1442,6 +1566,12 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): a
return $fields; return $fields;
} }
$fields['baseurl'] = bluesky_get_pds($author->did);
if (!empty($fields['baseurl'])) {
GServer::check($fields['baseurl'], Protocol::BLUESKY);
$fields['gsid'] = GServer::getID($fields['baseurl'], true);
}
$data = bluesky_xrpc_get($uid, 'app.bsky.actor.getProfile', ['actor' => $author->did]); $data = bluesky_xrpc_get($uid, 'app.bsky.actor.getProfile', ['actor' => $author->did]);
if (empty($data)) { if (empty($data)) {
Logger::debug('Error fetching contact fields', ['uid' => $uid, 'url' => $fields['url']]); Logger::debug('Error fetching contact fields', ['uid' => $uid, 'url' => $fields['url']]);
@ -1500,9 +1630,9 @@ function bluesky_get_preferences(int $uid): stdClass
return $data; return $data;
} }
function bluesky_get_did(int $uid, string $handle): string function bluesky_get_did(string $handle): string
{ {
$data = bluesky_get($uid, '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle)); $data = bluesky_get(BLUESKY_PDS . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle));
if (empty($data)) { if (empty($data)) {
return ''; return '';
} }
@ -1510,6 +1640,38 @@ function bluesky_get_did(int $uid, string $handle): string
return $data->did; return $data->did;
} }
function bluesky_get_user_pds(int $uid): string
{
$pds = DI::pConfig()->get($uid, 'bluesky', 'pds');
if (!empty($pds)) {
return $pds;
}
$did = DI::pConfig()->get($uid, 'bluesky', 'did');
if (empty($did)) {
Logger::notice('Empty did for user', ['uid' => $uid]);
return '';
}
$pds = bluesky_get_pds($did);
DI::pConfig()->set($uid, 'bluesky', 'pds', $pds);
return $pds;
}
function bluesky_get_pds(string $did): ?string
{
$data = bluesky_get(BLUESKY_DIRECTORY . '/' . $did);
if (empty($data) || empty($data->service)) {
return null;
}
foreach ($data->service as $service) {
if (($service->id == '#atproto_pds') && ($service->type == 'AtprotoPersonalDataServer') && !empty($service->serviceEndpoint)) {
return $service->serviceEndpoint;
}
}
return null;
}
function bluesky_get_token(int $uid): string function bluesky_get_token(int $uid): string
{ {
$token = DI::pConfig()->get($uid, 'bluesky', 'access_token'); $token = DI::pConfig()->get($uid, 'bluesky', 'access_token');
@ -1546,6 +1708,7 @@ function bluesky_create_token(int $uid, string $password): string
$data = bluesky_post($uid, '/xrpc/com.atproto.server.createSession', json_encode(['identifier' => $did, 'password' => $password]), ['Content-type' => 'application/json']); $data = bluesky_post($uid, '/xrpc/com.atproto.server.createSession', json_encode(['identifier' => $did, 'password' => $password]), ['Content-type' => 'application/json']);
if (empty($data)) { if (empty($data)) {
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_TOKEN_FAIL);
return ''; return '';
} }
@ -1553,6 +1716,7 @@ function bluesky_create_token(int $uid, string $password): string
DI::pConfig()->set($uid, 'bluesky', 'access_token', $data->accessJwt); DI::pConfig()->set($uid, 'bluesky', 'access_token', $data->accessJwt);
DI::pConfig()->set($uid, 'bluesky', 'refresh_token', $data->refreshJwt); DI::pConfig()->set($uid, 'bluesky', 'refresh_token', $data->refreshJwt);
DI::pConfig()->set($uid, 'bluesky', 'token_created', time()); DI::pConfig()->set($uid, 'bluesky', 'token_created', time());
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_TOKEN_OK);
return $data->accessJwt; return $data->accessJwt;
} }
@ -1564,17 +1728,20 @@ function bluesky_xrpc_post(int $uid, string $url, $parameters): ?stdClass
function bluesky_post(int $uid, string $url, string $params, array $headers): ?stdClass function bluesky_post(int $uid, string $url, string $params, array $headers): ?stdClass
{ {
try { try {
$curlResult = DI::httpClient()->post(DI::pConfig()->get($uid, 'bluesky', 'host') . $url, $params, $headers); $curlResult = DI::httpClient()->post(bluesky_get_user_pds($uid) . $url, $params, $headers);
} catch (\Exception $e) { } catch (\Exception $e) {
Logger::notice('Exception on post', ['exception' => $e]); Logger::notice('Exception on post', ['exception' => $e]);
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_API_FAIL);
return null; return null;
} }
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
Logger::notice('API Error', ['error' => json_decode($curlResult->getBody()) ?: $curlResult->getBody()]); Logger::notice('API Error', ['error' => json_decode($curlResult->getBody()) ?: $curlResult->getBody()]);
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_API_FAIL);
return null; return null;
} }
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_SUCCESS);
return json_decode($curlResult->getBody()); return json_decode($curlResult->getBody());
} }
@ -1584,13 +1751,16 @@ function bluesky_xrpc_get(int $uid, string $url, array $parameters = []): ?stdCl
$url .= '?' . http_build_query($parameters); $url .= '?' . http_build_query($parameters);
} }
return bluesky_get($uid, '/xrpc/' . $url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]); $data = bluesky_get(bluesky_get_user_pds($uid) . '/xrpc/' . $url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]);
DI::pConfig()->set($uid, 'bluesky', 'status', is_null($data) ? BLUEKSY_STATUS_API_FAIL : BLUEKSY_STATUS_SUCCESS);
return $data;
} }
function bluesky_get(int $uid, string $url, string $accept_content = HttpClientAccept::DEFAULT, array $opts = []): ?stdClass function bluesky_get(string $url, string $accept_content = HttpClientAccept::DEFAULT, array $opts = []): ?stdClass
{ {
try { try {
$curlResult = DI::httpClient()->get(DI::pConfig()->get($uid, 'bluesky', 'host') . $url, $accept_content, $opts); $curlResult = DI::httpClient()->get($url, $accept_content, $opts);
} catch (\Exception $e) { } catch (\Exception $e) {
Logger::notice('Exception on get', ['exception' => $e]); Logger::notice('Exception on get', ['exception' => $e]);
return null; return null;

View file

@ -6,11 +6,11 @@ function bluesky_feed_run($argv, $argc)
{ {
require_once 'addon/bluesky/bluesky.php'; require_once 'addon/bluesky/bluesky.php';
if ($argc != 3) { if ($argc != 4) {
return; return;
} }
Logger::debug('Importing feed - start', ['user' => $argv[1], 'feed' => $argv[2]]); Logger::debug('Importing feed - start', ['user' => $argv[1], 'feed' => $argv[2], 'last_poll' => $argv[3]]);
bluesky_fetch_feed($argv[1], $argv[2]); bluesky_fetch_feed($argv[1], $argv[2], $argv[3]);
Logger::debug('Importing feed - done', ['user' => $argv[1], 'feed' => $argv[2]]); Logger::debug('Importing feed - done', ['user' => $argv[1], 'feed' => $argv[2], 'last_poll' => $argv[3]]);
} }

View file

@ -6,11 +6,11 @@ function bluesky_notifications_run($argv, $argc)
{ {
require_once 'addon/bluesky/bluesky.php'; require_once 'addon/bluesky/bluesky.php';
if ($argc != 2) { if ($argc != 3) {
return; return;
} }
Logger::notice('importing notifications - start', ['user' => $argv[1]]); Logger::notice('importing notifications - start', ['user' => $argv[1], 'last_poll' => $argv[2]]);
bluesky_fetch_notifications($argv[1]); bluesky_fetch_notifications($argv[1], $argv[2]);
Logger::notice('importing notifications - done', ['user' => $argv[1]]); Logger::notice('importing notifications - done', ['user' => $argv[1], 'last_poll' => $argv[2]]);
} }

View file

@ -6,11 +6,11 @@ function bluesky_timeline_run($argv, $argc)
{ {
require_once 'addon/bluesky/bluesky.php'; require_once 'addon/bluesky/bluesky.php';
if ($argc != 2) { if ($argc != 3) {
return; return;
} }
Logger::notice('importing timeline - start', ['user' => $argv[1]]); Logger::notice('importing timeline - start', ['user' => $argv[1], 'last_poll' => $argv[2]]);
bluesky_fetch_timeline($argv[1]); bluesky_fetch_timeline($argv[1], $argv[2]);
Logger::notice('importing timeline - done', ['user' => $argv[1]]); Logger::notice('importing timeline - done', ['user' => $argv[1], 'last_poll' => $argv[2]]);
} }

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-06-05 04:34+0000\n" "POT-Creation-Date: 2023-12-06 06:30+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,70 +17,100 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: bluesky.php:314 #: bluesky.php:336
msgid ""
"You are authenticated to Bluesky. For security reasons the password isn't "
"stored."
msgstr ""
#: bluesky.php:314
msgid "You are not authenticated. Please enter the app password."
msgstr ""
#: bluesky.php:318
msgid "Enable Bluesky Post Addon" msgid "Enable Bluesky Post Addon"
msgstr "" msgstr ""
#: bluesky.php:319 #: bluesky.php:337
msgid "Post to Bluesky by default" msgid "Post to Bluesky by default"
msgstr "" msgstr ""
#: bluesky.php:320 #: bluesky.php:338
msgid "Import the remote timeline" msgid "Import the remote timeline"
msgstr "" msgstr ""
#: bluesky.php:321 #: bluesky.php:339
msgid "Import the pinned feeds" msgid "Import the pinned feeds"
msgstr "" msgstr ""
#: bluesky.php:321 #: bluesky.php:339
msgid "" msgid ""
"When activated, Posts will be imported from all the feeds that you pinned in " "When activated, Posts will be imported from all the feeds that you pinned in "
"Bluesky." "Bluesky."
msgstr "" msgstr ""
#: bluesky.php:322 #: bluesky.php:340
msgid "Bluesky host" msgid "Personal Data Server"
msgstr "" msgstr ""
#: bluesky.php:323 #: bluesky.php:340
msgid "The personal data server (PDS) is the system that hosts your profile."
msgstr ""
#: bluesky.php:341
msgid "Bluesky handle" msgid "Bluesky handle"
msgstr "" msgstr ""
#: bluesky.php:324 #: bluesky.php:342
msgid "Bluesky DID" msgid "Bluesky DID"
msgstr "" msgstr ""
#: bluesky.php:324 #: bluesky.php:342
msgid "" msgid ""
"This is the unique identifier. It will be fetched automatically, when the " "This is the unique identifier. It will be fetched automatically, when the "
"handle is entered." "handle is entered."
msgstr "" msgstr ""
#: bluesky.php:325 #: bluesky.php:343
msgid "Bluesky app password" msgid "Bluesky app password"
msgstr "" msgstr ""
#: bluesky.php:325 #: bluesky.php:343
msgid "" msgid ""
"Please don't add your real password here, but instead create a specific app " "Please don't add your real password here, but instead create a specific app "
"password in the Bluesky settings." "password in the Bluesky settings."
msgstr "" msgstr ""
#: bluesky.php:331 #: bluesky.php:349
msgid "Bluesky Import/Export" msgid "Bluesky Import/Export"
msgstr "" msgstr ""
#: bluesky.php:382 #: bluesky.php:359
msgid ""
"You are not authenticated. Please enter your handle and the app password."
msgstr ""
#: bluesky.php:379
msgid ""
"You are authenticated to Bluesky. For security reasons the password isn't "
"stored."
msgstr ""
#: bluesky.php:381
msgid ""
"The communication with the personal data server service (PDS) is established."
msgstr ""
#: bluesky.php:383
msgid "Communication issues with the personal data server service (PDS)."
msgstr ""
#: bluesky.php:385
msgid ""
"The DID for the provided handle could not be detected. Please check if you "
"entered the correct handle."
msgstr ""
#: bluesky.php:387
msgid "The personal data server service (PDS) could not be detected."
msgstr ""
#: bluesky.php:389
msgid ""
"The authentication with the provided handle and password failed. Please "
"check if you entered the correct password."
msgstr ""
#: bluesky.php:457
msgid "Post to Bluesky" msgid "Post to Bluesky"
msgstr "" msgstr ""

View file

@ -3,7 +3,7 @@
{{include file="field_checkbox.tpl" field=$bydefault}} {{include file="field_checkbox.tpl" field=$bydefault}}
{{include file="field_checkbox.tpl" field=$import}} {{include file="field_checkbox.tpl" field=$import}}
{{include file="field_checkbox.tpl" field=$import_feeds}} {{include file="field_checkbox.tpl" field=$import_feeds}}
{{include file="field_input.tpl" field=$host}} {{include file="field_input.tpl" field=$pds}}
{{include file="field_input.tpl" field=$handle}} {{include file="field_input.tpl" field=$handle}}
{{include file="field_input.tpl" field=$did}} {{include file="field_input.tpl" field=$did}}
{{include file="field_input.tpl" field=$password}} {{include file="field_input.tpl" field=$password}}

View file

@ -5,8 +5,7 @@
# #
# Translators: # Translators:
# Vladimir Núñez <lapoubelle111@gmail.com>, 2019 # Vladimir Núñez <lapoubelle111@gmail.com>, 2019
# Walter Bulbazor, 2021 # Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
@ -15,8 +14,8 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-21 19:14-0500\n" "POT-Creation-Date: 2021-11-21 19:14-0500\n"
"PO-Revision-Date: 2018-04-07 05:23+0000\n" "PO-Revision-Date: 2018-04-07 05:23+0000\n"
"Last-Translator: Hypolite Petovan <hypolite@mrpetovan.com>, 2022\n" "Last-Translator: Florent C., 2023\n"
"Language-Team: French (https://www.transifex.com/Friendica/teams/12172/fr/)\n" "Language-Team: French (https://app.transifex.com/Friendica/teams/12172/fr/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -25,7 +24,7 @@ msgstr ""
#: catavatar.php:48 #: catavatar.php:48
msgid "Set default profile avatar or randomize the cat." msgid "Set default profile avatar or randomize the cat."
msgstr "Mettre l'avatar par défaut ou tirer au sort le Chat." msgstr "Mettre l'avatar par défaut ou tirer au sort le chat."
#: catavatar.php:53 #: catavatar.php:53
msgid "Cat Avatar Settings" msgid "Cat Avatar Settings"
@ -33,15 +32,15 @@ msgstr "Paramètres de Chat avatar"
#: catavatar.php:56 #: catavatar.php:56
msgid "Use Cat as Avatar" msgid "Use Cat as Avatar"
msgstr "Utiliser Chat comme avatar" msgstr "Utiliser ce Chat"
#: catavatar.php:57 #: catavatar.php:57
msgid "Another random Cat!" msgid "Another random Cat!"
msgstr "Un autre chat aléatoire !" msgstr "Un autre Chat aléatoire !"
#: catavatar.php:58 #: catavatar.php:58
msgid "Reset to email Cat" msgid "Reset to email Cat"
msgstr "Réinitialiser à Chat courriel" msgstr "Revenir au Chat par défaut"
#: catavatar.php:77 #: catavatar.php:77
msgid "The cat hadn't found itself." msgid "The cat hadn't found itself."

View file

@ -5,11 +5,11 @@ function string_plural_select_fr($n){
$n = intval($n); $n = intval($n);
if (($n == 0 || $n == 1)) { return 0; } else if ($n != 0 && $n % 1000000 == 0) { return 1; } else { return 2; } if (($n == 0 || $n == 1)) { return 0; } else if ($n != 0 && $n % 1000000 == 0) { return 1; } else { return 2; }
}} }}
$a->strings['Set default profile avatar or randomize the cat.'] = 'Mettre l\'avatar par défaut ou tirer au sort le Chat.'; $a->strings['Set default profile avatar or randomize the cat.'] = 'Mettre l\'avatar par défaut ou tirer au sort le chat.';
$a->strings['Cat Avatar Settings'] = 'Paramètres de Chat avatar'; $a->strings['Cat Avatar Settings'] = 'Paramètres de Chat avatar';
$a->strings['Use Cat as Avatar'] = 'Utiliser Chat comme avatar'; $a->strings['Use Cat as Avatar'] = 'Utiliser ce Chat';
$a->strings['Another random Cat!'] = 'Un autre chat aléatoire !'; $a->strings['Another random Cat!'] = 'Un autre Chat aléatoire !';
$a->strings['Reset to email Cat'] = 'Réinitialiser à Chat courriel'; $a->strings['Reset to email Cat'] = 'Revenir au Chat par défaut';
$a->strings['The cat hadn\'t found itself.'] = 'Le Chat ne s\'y est pas retrouvé'; $a->strings['The cat hadn\'t found itself.'] = 'Le Chat ne s\'y est pas retrouvé';
$a->strings['There was an error, the cat ran away.'] = 'Il y a eu une erreur et le chat s\'est enfui'; $a->strings['There was an error, the cat ran away.'] = 'Il y a eu une erreur et le chat s\'est enfui';
$a->strings['Profile Photos'] = 'Photos de profil'; $a->strings['Profile Photos'] = 'Photos de profil';

85
cld/README.md Normal file
View file

@ -0,0 +1,85 @@
Compact Language Detector
===
CLD2 is an advanced language dectection library with a high reliability.
This addon depends on the CLD PHP module which is not included in any Linux distribution.
It needs to be built and installed by hand, which is not totally straightforward.
Prerequisite
---
To be able to build the extension, you need the CLD module and the files for the PHP module development.
On Debian you install the packages php-dev, libcld2-dev and libcld2-0.
Make sure to have installed the correct PHP version.
Means: When you have got both PHP 8.0 and 8.2 on your system, you have to install php8.0-dev as well.
Installation
---
The original PHP extension is https://github.com/fntlnz/cld2-php-ext.
However, it doesn't support PHP8.
So https://github.com/hiteule/cld2-php-ext/tree/support-php8 has to be used.
Download the source code:
```
wget https://github.com/hiteule/cld2-php-ext/archive/refs/heads/support-php8.zip
```
Unzip it:
```
unzip support-php8.zip
```
Change into the folder:
```
cd cld2-php-ext-support-php8/
```
Configure for the PHP Api version:
```
phpize
```
(if you have got several PHP versions on your system, execute the command with the version that you run Friendica with, e.g. `phpize8.0`)
Create the Makefile:
```
./configure --with-cld2=/usr/include/cld2
```
Have a look at the line `checking for PHP includes`.
When the output (for example `/usr/include/php/20220829` doesn't match the API version that you got from `phpize`, then you have to change all the version codes in your `Makefile` afterwards)
Create the module:
```
make -j
```
Install it:
```
sudo make install
```
Change to the folder with the available modules. When you use PHP 8.2 on Debian it is:
```
cd /etc/php/8.2/mods-available
```
Create the file `cld2.ini` with this content:
```
; configuration for php cld2 module
; priority=20
extension=cld2.so
```
Enable the module for all versions and all sapi:
```
phpenmod -v ALL -s ALL cld2
```
Then restart the apache or fpm (or whatever you use) to load the changed configuration.
Call `/admin/phpinfo` on your webserver.
You then see the PHP Info.
Search for "cld2".
The module is installed, when you find it here.
**Only proceed when the module is installed**
Now you can enable the addon.

71
cld/cld.php Normal file
View file

@ -0,0 +1,71 @@
<?php
/**
* Name: Compact Language Detector
* Description: Improved language detection
* Version: 0.1
* Author: Michael Vogel <heluecht@pirati.ca>
*/
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\DI;
function cld_install()
{
Hook::register('detect_languages', __FILE__, 'cld_detect_languages');
}
function cld_detect_languages(array &$data)
{
if (!in_array('cld2', get_loaded_extensions())) {
Logger::warning('CLD2 is not installed.');
return;
}
$cld2 = new \CLD2Detector();
$cld2->setEncodingHint(CLD2Encoding::UTF8); // optional, hints about text encoding
$cld2->setPlainText(true);
$result = $cld2->detect($data['text']);
if ($data['detected']) {
$original = array_key_first($data['detected']);
} else {
$original = '';
}
$detected = DI::l10n()->toISO6391($result['language_code']);
// languages that aren't supported via the base language detection or tend to false detections
if ((strlen($detected) == 3) || in_array($detected, ['ht', 'kk', 'ku', 'ky', 'lg', 'mg', 'mk', 'mt', 'ny', 'rw', 'st', 'su', 'tg', 'ts', 'xx'])) {
return;
}
if (!$result['is_reliable']) {
Logger::debug('Unreliable detection', ['uri-id' => $data['uri-id'], 'original' => $original, 'detected' => $detected, 'name' => $result['language_name'], 'probability' => $result['language_probability'], 'text' => $data['text']]);
if (($original == $detected) && ($data['detected'][$original] < $result['language_probability'] / 100)) {
$data['detected'][$original] = $result['language_probability'] / 100;
}
return;
}
$available = array_keys(DI::l10n()->getLanguageCodes());
if (!in_array($detected, $available)) {
Logger::debug('Unsupported language', ['uri-id' => $data['uri-id'], 'original' => $original, 'detected' => $detected, 'name' => $result['language_name'], 'probability' => $result['language_probability'], 'text' => $data['text']]);
return;
}
if ($original != $detected) {
Logger::debug('Detected different language', ['uri-id' => $data['uri-id'], 'original' => $original, 'detected' => $detected, 'name' => $result['language_name'], 'probability' => $result['language_probability'], 'text' => $data['text']]);
}
$length = count($data['detected']);
if ($length > 0) {
unset($data['detected'][$detected]);
$data['detected'] = array_merge([$detected => $result['language_probability'] / 100], array_slice($data['detected'], 0, $length - 1));
} else {
$data['detected'] = [$detected => $result['language_probability'] / 100];
}
}

4
invidious/README.md Normal file
View file

@ -0,0 +1,4 @@
invidious Addon for Friendica
==========================
This addon will replace "youtube.com" with the chosen Invidious instance

103
invidious/invidious.php Normal file
View file

@ -0,0 +1,103 @@
<?php
/*
* Name: invidious
* Description: Replaces links to youtube.com to an invidious instance in all displays of postings on a node.
* Version: 0.3
* Author: Matthias Ebers <https://loma.ml/profile/feb>
* Author: Michael Vogel <https://pirati.ca/profile/heluecht>
*
*/
use Friendica\Core\Hook;
use Friendica\Core\Renderer;
use Friendica\DI;
CONST INVIDIOUS_DEFAULT = 'https://invidio.us';
function invidious_install()
{
Hook::register('prepare_body_final', __FILE__, 'invidious_render');
Hook::register('addon_settings', __FILE__, 'invidious_settings');
Hook::register('addon_settings_post', __FILE__, 'invidious_settings_post');
}
/* Handle the send data from the admin settings
*/
function invidious_addon_admin_post()
{
DI::config()->set('invidious', 'server', trim($_POST['invidiousserver'], " \n\r\t\v\x00/"));
}
/* Hook into the admin settings to let the admin choose an
* invidious server to use for the replacement.
*/
function invidious_addon_admin(string &$o)
{
$invidiousserver = DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT);
$t = Renderer::getMarkupTemplate('admin.tpl', 'addon/invidious/');
$o = Renderer::replaceMacros($t, [
'$settingdescription' => DI::l10n()->t('Which Invidious server shall be used for the replacements in the post bodies? Use the URL with servername and protocol. See %s for a list of available public Invidious servers.', 'https://redirect.invidious.io'),
'$invidiousserver' => ['invidiousserver', DI::l10n()->t('Invidious server'), $invidiousserver, DI::l10n()->t('See %s for a list of available Invidious servers.', '<a href="https://api.invidious.io/">https://api.invidious.io/</a>')],
'$submit' => DI::l10n()->t('Save Settings'),
]);
}
function invidious_settings(array &$data)
{
if (!DI::userSession()->getLocalUserId()) {
return;
}
$enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'enabled');
$server = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'server', DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT));
$t = Renderer::getMarkupTemplate('settings.tpl', 'addon/invidious/');
$html = Renderer::replaceMacros($t, [
'$enabled' => ['enabled', DI::l10n()->t('Replace Youtube links with links to an Invidious server'), $enabled, DI::l10n()->t('If enabled, Youtube links are replaced with the links to the specified Invidious server.')],
'$server' => ['server', DI::l10n()->t('Invidious server'), $server, DI::l10n()->t('See %s for a list of available Invidious servers.', '<a href="https://api.invidious.io/">https://api.invidious.io/</a>')],
]);
$data = [
'addon' => 'invidious',
'title' => DI::l10n()->t('Invidious Settings'),
'html' => $html,
];
}
function invidious_settings_post(array &$b)
{
if (!DI::userSession()->getLocalUserId() || empty($_POST['invidious-submit'])) {
return;
}
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'invidious', 'enabled', (bool)$_POST['enabled']);
$server = trim($_POST['server'], " \n\r\t\v\x00/");
if ($server != DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT) && !empty($server)) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'invidious', 'server', $server);
} else {
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'invidious', 'server');
}
}
/*
* replace "youtube.com" with the chosen Invidious instance
*/
function invidious_render(array &$b)
{
if (!DI::userSession()->getLocalUserId() || !DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'enabled')) {
return;
}
$original = $b['html'];
$server = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'invidious', 'server', DI::config()->get('invidious', 'server', INVIDIOUS_DEFAULT));
$b['html'] = preg_replace("/https?:\/\/www.youtube.com\/watch\?v\=(.*?)/ism", $server . '/watch?v=$1', $b['html']);
$b['html'] = preg_replace("/https?:\/\/www.youtube.com\/embed\/(.*?)/ism", $server . '/embed/$1', $b['html']);
$b['html'] = preg_replace("/https?:\/\/www.youtube.com\/shorts\/(.*?)/ism", $server . '/shorts/$1', $b['html']);
$b['html'] = preg_replace("/https?:\/\/youtu.be\/(.*?)/ism", $server . '/watch?v=$1', $b['html']);
if ($original != $b['html']) {
$b['html'] .= '<hr><p><small>' . DI::l10n()->t('(Invidious addon enabled: YouTube links via %s)', $server) . '</small></p>';
}
}

View file

@ -0,0 +1,58 @@
# ADDON invidious
# Copyright (C)
# This file is distributed under the same license as the Friendica invidious addon package.
#
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-18 17:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: invidious.php:39
#, php-format
msgid ""
"Which Invidious server shall be used for the replacements in the post "
"bodies? Use the URL with servername and protocol. See %s for a list of "
"available public Invidious servers."
msgstr ""
#: invidious.php:40 invidious.php:57
msgid "Invidious server"
msgstr ""
#: invidious.php:40 invidious.php:57
#, php-format
msgid "See %s for a list of available Invidious servers."
msgstr ""
#: invidious.php:41
msgid "Save Settings"
msgstr ""
#: invidious.php:56
msgid "Replace Youtube links with links to an Invidious server"
msgstr ""
#: invidious.php:56
msgid ""
"If enabled, Youtube links are replaced with the links to the specified "
"Invidious server."
msgstr ""
#: invidious.php:62
msgid "Invidious Settings"
msgstr ""
#: invidious.php:101
#, php-format
msgid "(Invidious addon enabled: YouTube links via %s)"
msgstr ""

View file

@ -0,0 +1,5 @@
<p>{{$settingdescription}}</p>
{{include file="field_input.tpl" field=$invidiousserver}}
<div class="submit"><input type="submit" name="page_site" value="{{$submit}}" /></div>

View file

@ -0,0 +1,2 @@
{{include file="field_checkbox.tpl" field=$enabled}}
{{include file="field_input.tpl" field=$server}}

View file

@ -4,63 +4,59 @@
# #
# #
# Translators: # Translators:
# Alexander An <ravnina@gmail.com>, 2020 # Alexander An <ravnina@gmail.com>, 2020,2023
# Stanislav N. <pztrn@pztrn.name>, 2018 # Stanislav N. <pztrn@pztrn.name>, 2018
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-10-03 22:02-0400\n" "POT-Creation-Date: 2021-11-21 19:15-0500\n"
"PO-Revision-Date: 2020-10-09 17:48+0000\n" "PO-Revision-Date: 2015-07-25 08:05+0000\n"
"Last-Translator: Alexander An <ravnina@gmail.com>\n" "Last-Translator: Alexander An <ravnina@gmail.com>, 2020,2023\n"
"Language-Team: Russian (http://www.transifex.com/Friendica/friendica/language/ru/)\n" "Language-Team: Russian (http://app.transifex.com/Friendica/friendica/language/ru/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: ru\n" "Language: ru\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#: langfilter.php:52 #: langfilter.php:49
msgid "Language Filter"
msgstr "Языковой фильтр"
#: langfilter.php:53
msgid "" msgid ""
"This addon tries to identify the language posts are written in. If it does " "This addon tries to identify the language posts are written in. If it does "
"not match any language specified below, posts will be hidden by collapsing " "not match any language specified below, posts will be hidden by collapsing "
"them." "them."
msgstr "Это дополнение пытается идентифицировать язык, на котором написаны посты. Если язык не соответствует ни одному, указанному ниже, то такие посты будут скрыты." msgstr "Это дополнение пытается идентифицировать язык, на котором сделаны записи. Если язык не соответствует ни одному, указанному ниже, то такие посты будут свёрнуты."
#: langfilter.php:54 #: langfilter.php:50
msgid "Use the language filter" msgid "Use the language filter"
msgstr "Использовать языковой фильтр" msgstr "Использовать языковой фильтр"
#: langfilter.php:55 #: langfilter.php:51
msgid "Able to read" msgid "Able to read"
msgstr "Возможность читать" msgstr "Возможность читать"
#: langfilter.php:55 #: langfilter.php:51
msgid "" msgid ""
"List of abbreviations (ISO 639-1 codes) for languages you speak, comma " "List of abbreviations (ISO 639-1 codes) for languages you speak, comma "
"separated. For example \"de,it\"." "separated. For example \"de,it\"."
msgstr "Список аббревиатур (кодов по ISO 639-1 ) для языков, на которых вы говорите. Например, \"ru,en\"." msgstr "Список аббревиатур (кодов по ISO 639-1 ) для языков, на которых вы говорите. Например, \"ru,en\"."
#: langfilter.php:56 #: langfilter.php:52
msgid "Minimum confidence in language detection" msgid "Minimum confidence in language detection"
msgstr "Минимальная уверенность в определении языка" msgstr "Минимальная уверенность в определении языка"
#: langfilter.php:56 #: langfilter.php:52
msgid "" msgid ""
"Minimum confidence in language detection being correct, from 0 to 100. Posts" "Minimum confidence in language detection being correct, from 0 to 100. Posts"
" will not be filtered when the confidence of language detection is below " " will not be filtered when the confidence of language detection is below "
"this percent value." "this percent value."
msgstr "Минимальная уверенность в правильном определении языка, от 0 до 100. Посты не будут скрыты, если уверенность в правильном определении языка в процентах ниже этого значения." msgstr "Минимальная уверенность в правильном определении языка, от 0 до 100. Посты не будут скрыты, если уверенность в правильном определении языка в процентах ниже этого значения."
#: langfilter.php:57 #: langfilter.php:53
msgid "Minimum length of message body" msgid "Minimum length of message body"
msgstr "Минимальная длина тела сообщения" msgstr "Минимальная длина тела сообщения"
#: langfilter.php:57 #: langfilter.php:53
msgid "" msgid ""
"Minimum number of characters in message body for filter to be used. Posts " "Minimum number of characters in message body for filter to be used. Posts "
"shorter than this will not be filtered. Note: Language detection is " "shorter than this will not be filtered. Note: Language detection is "
@ -68,10 +64,14 @@ msgid ""
msgstr "Минимальное количество знаков в теле сообщения для применения фильтрации. Посты, длина которых меньше указанного значения, не будут отфильтрованы. Обратите внимание, что определение языка работает ненадежно для небольших постов (<200 символов)." msgstr "Минимальное количество знаков в теле сообщения для применения фильтрации. Посты, длина которых меньше указанного значения, не будут отфильтрованы. Обратите внимание, что определение языка работает ненадежно для небольших постов (<200 символов)."
#: langfilter.php:58 #: langfilter.php:58
msgid "Language Filter"
msgstr "Языковой фильтр"
#: langfilter.php:60
msgid "Save Settings" msgid "Save Settings"
msgstr "Сохранить настройки" msgstr "Сохранить настройки"
#: langfilter.php:189 #: langfilter.php:193
#, php-format #, php-format
msgid "Filtered language: %s" msgid "Filtered language: %s"
msgstr "Отфильтрованный язык: %s" msgstr "Отфильтрованный язык: %s"

View file

@ -5,8 +5,7 @@ function string_plural_select_ru($n){
$n = intval($n); $n = intval($n);
if ($n%10==1 && $n%100!=11) { return 0; } else if ($n%10>=2 && $n%10<=4 && ($n%100<12 || $n%100>14)) { return 1; } else if ($n%10==0 || ($n%10>=5 && $n%10<=9) || ($n%100>=11 && $n%100<=14)) { return 2; } else { return 3; } if ($n%10==1 && $n%100!=11) { return 0; } else if ($n%10>=2 && $n%10<=4 && ($n%100<12 || $n%100>14)) { return 1; } else if ($n%10==0 || ($n%10>=5 && $n%10<=9) || ($n%100>=11 && $n%100<=14)) { return 2; } else { return 3; }
}} }}
$a->strings['Language Filter'] = 'Языковой фильтр'; $a->strings['This addon tries to identify the language posts are written in. If it does not match any language specified below, posts will be hidden by collapsing them.'] = 'Это дополнение пытается идентифицировать язык, на котором сделаны записи. Если язык не соответствует ни одному, указанному ниже, то такие посты будут свёрнуты.';
$a->strings['This addon tries to identify the language posts are written in. If it does not match any language specified below, posts will be hidden by collapsing them.'] = 'Это дополнение пытается идентифицировать язык, на котором написаны посты. Если язык не соответствует ни одному, указанному ниже, то такие посты будут скрыты.';
$a->strings['Use the language filter'] = 'Использовать языковой фильтр'; $a->strings['Use the language filter'] = 'Использовать языковой фильтр';
$a->strings['Able to read'] = 'Возможность читать'; $a->strings['Able to read'] = 'Возможность читать';
$a->strings['List of abbreviations (ISO 639-1 codes) for languages you speak, comma separated. For example "de,it".'] = 'Список аббревиатур (кодов по ISO 639-1 ) для языков, на которых вы говорите. Например, "ru,en".'; $a->strings['List of abbreviations (ISO 639-1 codes) for languages you speak, comma separated. For example "de,it".'] = 'Список аббревиатур (кодов по ISO 639-1 ) для языков, на которых вы говорите. Например, "ru,en".';
@ -14,5 +13,6 @@ $a->strings['Minimum confidence in language detection'] = 'Минимальна
$a->strings['Minimum confidence in language detection being correct, from 0 to 100. Posts will not be filtered when the confidence of language detection is below this percent value.'] = 'Минимальная уверенность в правильном определении языка, от 0 до 100. Посты не будут скрыты, если уверенность в правильном определении языка в процентах ниже этого значения.'; $a->strings['Minimum confidence in language detection being correct, from 0 to 100. Posts will not be filtered when the confidence of language detection is below this percent value.'] = 'Минимальная уверенность в правильном определении языка, от 0 до 100. Посты не будут скрыты, если уверенность в правильном определении языка в процентах ниже этого значения.';
$a->strings['Minimum length of message body'] = 'Минимальная длина тела сообщения'; $a->strings['Minimum length of message body'] = 'Минимальная длина тела сообщения';
$a->strings['Minimum number of characters in message body for filter to be used. Posts shorter than this will not be filtered. Note: Language detection is unreliable for short content (<200 characters).'] = 'Минимальное количество знаков в теле сообщения для применения фильтрации. Посты, длина которых меньше указанного значения, не будут отфильтрованы. Обратите внимание, что определение языка работает ненадежно для небольших постов (<200 символов).'; $a->strings['Minimum number of characters in message body for filter to be used. Posts shorter than this will not be filtered. Note: Language detection is unreliable for short content (<200 characters).'] = 'Минимальное количество знаков в теле сообщения для применения фильтрации. Посты, длина которых меньше указанного значения, не будут отфильтрованы. Обратите внимание, что определение языка работает ненадежно для небольших постов (<200 символов).';
$a->strings['Language Filter'] = 'Языковой фильтр';
$a->strings['Save Settings'] = 'Сохранить настройки'; $a->strings['Save Settings'] = 'Сохранить настройки';
$a->strings['Filtered language: %s'] = 'Отфильтрованный язык: %s'; $a->strings['Filtered language: %s'] = 'Отфильтрованный язык: %s';

View file

@ -163,7 +163,7 @@ function langfilter_prepare_body_content_filter(&$hook_data)
return; return;
} }
$lang = $iso639->languageByCode1($iso2); $lang = $iso639->languageByCode1(substr($iso2, 0, 2));
} else { } else {
$opts = $hook_data['item']['postopts']; $opts = $hook_data['item']['postopts'];
if (!$opts) { if (!$opts) {

View file

@ -4,6 +4,7 @@
# #
# #
# Translators: # Translators:
# Florent C., 2023
# Nicolas Derive, 2022 # Nicolas Derive, 2022
# ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015 # ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015
# StefOfficiel <pichard.stephane@free.fr>, 2015 # StefOfficiel <pichard.stephane@free.fr>, 2015
@ -13,8 +14,8 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-21 19:15-0500\n" "POT-Creation-Date: 2021-11-21 19:15-0500\n"
"PO-Revision-Date: 2014-06-23 09:54+0000\n" "PO-Revision-Date: 2014-06-23 09:54+0000\n"
"Last-Translator: Nicolas Derive, 2022\n" "Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n" "Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -33,68 +34,72 @@ msgstr "Adresse de courriel de laquelle les éléments du flux sembleront proven
msgid "Save Settings" msgid "Save Settings"
msgstr "Sauvegarder les paramètres" msgstr "Sauvegarder les paramètres"
#: mailstream.php:301 #: mailstream.php:311
msgid "Re:" msgid "Re:"
msgstr "Re :" msgstr "Re :"
#: mailstream.php:314 mailstream.php:317 #: mailstream.php:324 mailstream.php:327
msgid "Friendica post" msgid "Friendica post"
msgstr "Message Friendica" msgstr "Message Friendica"
#: mailstream.php:320 #: mailstream.php:330
msgid "Diaspora post" msgid "Diaspora post"
msgstr "Message Diaspora" msgstr "Message Diaspora"
#: mailstream.php:330 #: mailstream.php:340
msgid "Feed item" msgid "Feed item"
msgstr "Élément du flux" msgstr "Élément du flux"
#: mailstream.php:333 #: mailstream.php:343
msgid "Email" msgid "Email"
msgstr "Courriel" msgstr "Courriel"
#: mailstream.php:335 #: mailstream.php:345
msgid "Friendica Item" msgid "Friendica Item"
msgstr "Élément de Friendica" msgstr "Élément de Friendica"
#: mailstream.php:404 #: mailstream.php:419
msgid "Upstream" msgid "Upstream"
msgstr "En amont" msgstr "En amont"
#: mailstream.php:405 #: mailstream.php:420
msgid "URI"
msgstr "URI"
#: mailstream.php:421
msgid "Local" msgid "Local"
msgstr "Local" msgstr "Local"
#: mailstream.php:481 #: mailstream.php:499
msgid "Enabled" msgid "Enabled"
msgstr "Activer" msgstr "Activer"
#: mailstream.php:486 #: mailstream.php:504
msgid "Email Address" msgid "Email Address"
msgstr "Adresse de courriel" msgstr "Adresse de courriel"
#: mailstream.php:488 #: mailstream.php:506
msgid "Leave blank to use your account email address" msgid "Leave blank to use your account email address"
msgstr "Laissez vide pour utiliser l'adresse de courriel de votre compte" msgstr "Laissez vide pour utiliser l'adresse de courriel de votre compte"
#: mailstream.php:492 #: mailstream.php:510
msgid "Exclude Likes" msgid "Exclude Likes"
msgstr "Exclure les \"j'aime\"" msgstr "Exclure les \"j'aime\""
#: mailstream.php:494 #: mailstream.php:512
msgid "Check this to omit mailing \"Like\" notifications" msgid "Check this to omit mailing \"Like\" notifications"
msgstr "Cochez ceci pour éviter d'envoyer les notifications des \"J'aime\"" msgstr "Cochez ceci pour éviter d'envoyer les notifications des \"J'aime\""
#: mailstream.php:498 #: mailstream.php:516
msgid "Attach Images" msgid "Attach Images"
msgstr "Attacher les images" msgstr "Attacher les images"
#: mailstream.php:500 #: mailstream.php:518
msgid "" msgid ""
"Download images in posts and attach them to the email. Useful for reading " "Download images in posts and attach them to the email. Useful for reading "
"email while offline." "email while offline."
msgstr "Télécharger les images des messages et les attacher au courriel. Utile pour les les courriels hors-ligne." msgstr "Télécharger les images des messages et les attacher au courriel. Utile pour les les courriels hors-ligne."
#: mailstream.php:507 #: mailstream.php:525
msgid "Mail Stream Settings" msgid "Mail Stream Settings"
msgstr "Paramètres de Mail Stream" msgstr "Paramètres de Mail Stream"

View file

@ -15,6 +15,7 @@ $a->strings['Feed item'] = 'Élément du flux';
$a->strings['Email'] = 'Courriel'; $a->strings['Email'] = 'Courriel';
$a->strings['Friendica Item'] = 'Élément de Friendica'; $a->strings['Friendica Item'] = 'Élément de Friendica';
$a->strings['Upstream'] = 'En amont'; $a->strings['Upstream'] = 'En amont';
$a->strings['URI'] = 'URI';
$a->strings['Local'] = 'Local'; $a->strings['Local'] = 'Local';
$a->strings['Enabled'] = 'Activer'; $a->strings['Enabled'] = 'Activer';
$a->strings['Email Address'] = 'Adresse de courriel'; $a->strings['Email Address'] = 'Adresse de courriel';

View file

@ -10,7 +10,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Aditoo, 2018\n" "Last-Translator: Aditoo, 2018\n"
"Language-Team: Czech (http://app.transifex.com/Friendica/friendica/language/cs/)\n" "Language-Team: Czech (http://app.transifex.com/Friendica/friendica/language/cs/)\n"
@ -29,48 +29,48 @@ msgid "Tips for New Members"
msgstr "Tipy pro nové členy" msgstr "Tipy pro nové členy"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Globální fórum podpory" msgstr ""
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Místní fórum podpory" msgstr ""
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Uložit nastavení" msgstr "Uložit nastavení"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Zpráva" msgstr "Zpráva"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Vaše zpráva pro nové členy. Zde můžete použít BBCode." msgstr "Vaše zpráva pro nové členy. Zde můžete použít BBCode."
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Přidejte odkaz na globální fórum podpory" msgstr ""
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Má být zobrazen odkaz na globální fórum podpory?" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Přidejte odkaz na místní fórum podpory" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "" msgstr ""
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Název místního fóra podpory" msgstr "Název místního fóra podpory"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,13 +7,8 @@ function string_plural_select_cs($n){
}} }}
$a->strings['New Member'] = 'Nový člen'; $a->strings['New Member'] = 'Nový člen';
$a->strings['Tips for New Members'] = 'Tipy pro nové členy'; $a->strings['Tips for New Members'] = 'Tipy pro nové členy';
$a->strings['Global Support Forum'] = 'Globální fórum podpory';
$a->strings['Local Support Forum'] = 'Místní fórum podpory';
$a->strings['Save Settings'] = 'Uložit nastavení'; $a->strings['Save Settings'] = 'Uložit nastavení';
$a->strings['Message'] = 'Zpráva'; $a->strings['Message'] = 'Zpráva';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Vaše zpráva pro nové členy. Zde můžete použít BBCode.'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Vaše zpráva pro nové členy. Zde můžete použít BBCode.';
$a->strings['Add a link to global support forum'] = 'Přidejte odkaz na globální fórum podpory';
$a->strings['Should a link to the global support forum be displayed?'] = 'Má být zobrazen odkaz na globální fórum podpory?';
$a->strings['Add a link to the local support forum'] = 'Přidejte odkaz na místní fórum podpory';
$a->strings['Name of the local support group'] = 'Název místního fóra podpory'; $a->strings['Name of the local support group'] = 'Název místního fóra podpory';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Pokud jste zaškrtl/a výše uvedenou možnost, specifikujte zde <em>přezdívku</em> místní skupiny podpory (např. pomocnici)'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Pokud jste zaškrtl/a výše uvedenou možnost, specifikujte zde <em>přezdívku</em> místní skupiny podpory (např. pomocnici)';

View file

@ -4,15 +4,16 @@
# #
# #
# Translators: # Translators:
# Raroun, 2023
# Tobias Diekershoff <tobias.diekershoff@gmx.net>, 2021 # Tobias Diekershoff <tobias.diekershoff@gmx.net>, 2021
# Ulf Rompe <transifex.com@rompe.org>, 2019 # Ulf Rompe <transifex.com@rompe.org>, 2019
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Tobias Diekershoff <tobias.diekershoff@gmx.net>, 2021\n" "Last-Translator: Raroun, 2023\n"
"Language-Team: German (http://app.transifex.com/Friendica/friendica/language/de/)\n" "Language-Team: German (http://app.transifex.com/Friendica/friendica/language/de/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -29,48 +30,48 @@ msgid "Tips for New Members"
msgstr "Tipps für neue Nutzer" msgstr "Tipps für neue Nutzer"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Globales Forum für Hilfsanfragen" msgstr "Globale Support-Gruppe"
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Lokales Forum für Hilfsanfragen" msgstr "Lokale Support-Gruppe"
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Einstellungen speichern" msgstr "Einstellungen speichern"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Nachricht" msgstr "Nachricht"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Deine Nachricht für neue Nutzer. BBCode kann verwendet werden." msgstr "Deine Nachricht für neue Nutzer. BBCode kann verwendet werden."
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Link zum globalen Support-Forum anzeigen" msgstr "Fügen Sie einen Link der globalen Support-Gruppe hinzu"
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Soll ein Link zum globalen Support-Forum angezeigt werden?" msgstr "Soll ein Link zur globalen Support-Gruppe angezeigt werden?"
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Link zum lokalen Support-Forum anzeigen" msgstr "Fügen Sie einen Link der lokalen Support-Gruppe hinzu"
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Wenn du ein lokales Support-Forum eingerichtet hast und ein Link darauf angezeigt werden soll, schalte dies ein." msgstr "Wenn Sie eine lokale Support-Gruppe haben und einen Link im Widget anzeigen lassen möchten, markieren Sie dieses Feld."
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Name des lokalen Support-Forums" msgstr "Name des lokalen Support-Forums"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,14 +7,14 @@ function string_plural_select_de($n){
}} }}
$a->strings['New Member'] = 'Neue Nutzer'; $a->strings['New Member'] = 'Neue Nutzer';
$a->strings['Tips for New Members'] = 'Tipps für neue Nutzer'; $a->strings['Tips for New Members'] = 'Tipps für neue Nutzer';
$a->strings['Global Support Forum'] = 'Globales Forum für Hilfsanfragen'; $a->strings['Global Support Group'] = 'Globale Support-Gruppe';
$a->strings['Local Support Forum'] = 'Lokales Forum für Hilfsanfragen'; $a->strings['Local Support Group'] = 'Lokale Support-Gruppe';
$a->strings['Save Settings'] = 'Einstellungen speichern'; $a->strings['Save Settings'] = 'Einstellungen speichern';
$a->strings['Message'] = 'Nachricht'; $a->strings['Message'] = 'Nachricht';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Deine Nachricht für neue Nutzer. BBCode kann verwendet werden.'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Deine Nachricht für neue Nutzer. BBCode kann verwendet werden.';
$a->strings['Add a link to global support forum'] = 'Link zum globalen Support-Forum anzeigen'; $a->strings['Add a link to global support group'] = 'Fügen Sie einen Link der globalen Support-Gruppe hinzu';
$a->strings['Should a link to the global support forum be displayed?'] = 'Soll ein Link zum globalen Support-Forum angezeigt werden?'; $a->strings['Should a link to the global support group be displayed?'] = 'Soll ein Link zur globalen Support-Gruppe angezeigt werden?';
$a->strings['Add a link to the local support forum'] = 'Link zum lokalen Support-Forum anzeigen'; $a->strings['Add a link to the local support group'] = 'Fügen Sie einen Link der lokalen Support-Gruppe hinzu';
$a->strings['If you have a local support forum and want to have a link displayed in the widget, check this box.'] = 'Wenn du ein lokales Support-Forum eingerichtet hast und ein Link darauf angezeigt werden soll, schalte dies ein.'; $a->strings['If you have a local support group and want to have a link displayed in the widget, check this box.'] = 'Wenn Sie eine lokale Support-Gruppe haben und einen Link im Widget anzeigen lassen möchten, markieren Sie dieses Feld.';
$a->strings['Name of the local support group'] = 'Name des lokalen Support-Forums'; $a->strings['Name of the local support group'] = 'Name des lokalen Support-Forums';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Wenn der Link angezeigt werden soll, dann trage hier den <em>Spitznamen</em> des Forums ein (z.B. helpers)'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Wenn der Link angezeigt werden soll, dann trage hier den <em>Spitznamen</em> des Forums ein (z.B. helpers)';

View file

@ -10,15 +10,15 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2021-04-06 01:46+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Senex Petrovic <javierruizo@hotmail.com>\n" "Last-Translator: Senex Petrovic <javierruizo@hotmail.com>, 2021\n"
"Language-Team: Spanish (http://www.transifex.com/Friendica/friendica/language/es/)\n" "Language-Team: Spanish (http://app.transifex.com/Friendica/friendica/language/es/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: es\n" "Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: newmemberwidget.php:29 #: newmemberwidget.php:29
msgid "New Member" msgid "New Member"
@ -29,48 +29,48 @@ msgid "Tips for New Members"
msgstr "Consejos para Nuevos Miembros" msgstr "Consejos para Nuevos Miembros"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Foro de Soporte Global" msgstr ""
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Foro de Soporte Local" msgstr ""
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Guardar Ajustes" msgstr "Guardar Ajustes"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Mensaje" msgstr "Mensaje"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Su mensaje para los nuevos miembros. Puede usar bbcode aquí" msgstr "Su mensaje para los nuevos miembros. Puede usar bbcode aquí"
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Añadir un enlace al foro de soporte global" msgstr ""
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "¿Debería mostrarse un enlace al foro de soporte global?" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Añadir un enlace al foro de soporte local" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Si tiene foro de soporte local y desea que se muestre un enlace en el widget, marque esta casilla." msgstr ""
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Nombre del grupo de soporte local" msgstr "Nombre del grupo de soporte local"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -3,18 +3,12 @@
if(! function_exists("string_plural_select_es")) { if(! function_exists("string_plural_select_es")) {
function string_plural_select_es($n){ function string_plural_select_es($n){
$n = intval($n); $n = intval($n);
return intval($n != 1); if ($n == 1) { return 0; } else if ($n != 0 && $n % 1000000 == 0) { return 1; } else { return 2; }
}} }}
$a->strings['New Member'] = 'Nuevo Miembro'; $a->strings['New Member'] = 'Nuevo Miembro';
$a->strings['Tips for New Members'] = 'Consejos para Nuevos Miembros'; $a->strings['Tips for New Members'] = 'Consejos para Nuevos Miembros';
$a->strings['Global Support Forum'] = 'Foro de Soporte Global';
$a->strings['Local Support Forum'] = 'Foro de Soporte Local';
$a->strings['Save Settings'] = 'Guardar Ajustes'; $a->strings['Save Settings'] = 'Guardar Ajustes';
$a->strings['Message'] = 'Mensaje'; $a->strings['Message'] = 'Mensaje';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Su mensaje para los nuevos miembros. Puede usar bbcode aquí'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Su mensaje para los nuevos miembros. Puede usar bbcode aquí';
$a->strings['Add a link to global support forum'] = 'Añadir un enlace al foro de soporte global';
$a->strings['Should a link to the global support forum be displayed?'] = '¿Debería mostrarse un enlace al foro de soporte global?';
$a->strings['Add a link to the local support forum'] = 'Añadir un enlace al foro de soporte local';
$a->strings['If you have a local support forum and want to have a link displayed in the widget, check this box.'] = 'Si tiene foro de soporte local y desea que se muestre un enlace en el widget, marque esta casilla.';
$a->strings['Name of the local support group'] = 'Nombre del grupo de soporte local'; $a->strings['Name of the local support group'] = 'Nombre del grupo de soporte local';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Si chequeó arriba, especifique el <em>apodo</em> del grupo de soporte local aquí (asistentes)'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Si chequeó arriba, especifique el <em>apodo</em> del grupo de soporte local aquí (asistentes)';

View file

@ -9,67 +9,67 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-06-01 14:12+0200\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2019-04-16 05:07+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Rain Hawk\n" "Last-Translator: Rain Hawk, 2019\n"
"Language-Team: Estonian (http://www.transifex.com/Friendica/friendica/language/et/)\n" "Language-Team: Estonian (http://app.transifex.com/Friendica/friendica/language/et/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: et\n" "Language: et\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: newmemberwidget.php:21 #: newmemberwidget.php:29
msgid "New Member" msgid "New Member"
msgstr "Uus liige" msgstr "Uus liige"
#: newmemberwidget.php:22 #: newmemberwidget.php:30
msgid "Tips for New Members" msgid "Tips for New Members"
msgstr "Nippe uutele liikmetele" msgstr "Nippe uutele liikmetele"
#: newmemberwidget.php:24 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Globaalne tugifoorum" msgstr ""
#: newmemberwidget.php:26 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Lokaalne tugifoorum" msgstr ""
#: newmemberwidget.php:49 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Salvesta sätted" msgstr "Salvesta sätted"
#: newmemberwidget.php:50 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Sõnum" msgstr "Sõnum"
#: newmemberwidget.php:50 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "" msgstr ""
#: newmemberwidget.php:51 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "" msgstr ""
#: newmemberwidget.php:51 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "" msgstr ""
#: newmemberwidget.php:52 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "" msgstr ""
#: newmemberwidget.php:52 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and wand to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "" msgstr ""
#: newmemberwidget.php:53 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "" msgstr ""
#: newmemberwidget.php:53 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,7 +7,5 @@ function string_plural_select_et($n){
}} }}
$a->strings['New Member'] = 'Uus liige'; $a->strings['New Member'] = 'Uus liige';
$a->strings['Tips for New Members'] = 'Nippe uutele liikmetele'; $a->strings['Tips for New Members'] = 'Nippe uutele liikmetele';
$a->strings['Global Support Forum'] = 'Globaalne tugifoorum';
$a->strings['Local Support Forum'] = 'Lokaalne tugifoorum';
$a->strings['Save Settings'] = 'Salvesta sätted'; $a->strings['Save Settings'] = 'Salvesta sätted';
$a->strings['Message'] = 'Sõnum'; $a->strings['Message'] = 'Sõnum';

View file

@ -4,6 +4,7 @@
# #
# #
# Translators: # Translators:
# Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022 # Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# Nicolas Derive, 2022 # Nicolas Derive, 2022
# StefOfficiel <pichard.stephane@free.fr>, 2015 # StefOfficiel <pichard.stephane@free.fr>, 2015
@ -11,10 +12,10 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Nicolas Derive, 2022\n" "Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n" "Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -30,48 +31,48 @@ msgid "Tips for New Members"
msgstr "Conseils aux nouveaux venus" msgstr "Conseils aux nouveaux venus"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Forum de support global" msgstr "Groupe de support global"
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Forum de support local" msgstr "Groupe de support local"
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Enregistrer les paramètres" msgstr "Enregistrer les paramètres"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Message" msgstr "Message"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Votre messages aux nouveaux venus. Vous pouvez utiliser des BBCodes." msgstr "Votre messages aux nouveaux venus. Vous pouvez utiliser des BBCodes."
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Ajouter un lien vers le forum de support global" msgstr "Ajouter un lien vers le groupe de support global"
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Montrer un lien vers le forum de support global?" msgstr "Montrer un lien vers le groupe de support global ?"
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Ajouter un lien vers le forum de support local" msgstr "Ajouter un lien vers le groupe de support local"
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Si vous avez un forum d'assistance local et désirez avoir un lien affiché dans l'appliquette/widget, cochez cette case." msgstr "Si vous avez un groupe de support local et désirez avoir un lien affiché dans l'appliquette/widget, cochez cette case."
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Nom du groupe de support local" msgstr "Nom du groupe de support local"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,14 +7,14 @@ function string_plural_select_fr($n){
}} }}
$a->strings['New Member'] = 'Nouveau Membre'; $a->strings['New Member'] = 'Nouveau Membre';
$a->strings['Tips for New Members'] = 'Conseils aux nouveaux venus'; $a->strings['Tips for New Members'] = 'Conseils aux nouveaux venus';
$a->strings['Global Support Forum'] = 'Forum de support global'; $a->strings['Global Support Group'] = 'Groupe de support global';
$a->strings['Local Support Forum'] = 'Forum de support local'; $a->strings['Local Support Group'] = 'Groupe de support local';
$a->strings['Save Settings'] = 'Enregistrer les paramètres'; $a->strings['Save Settings'] = 'Enregistrer les paramètres';
$a->strings['Message'] = 'Message'; $a->strings['Message'] = 'Message';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Votre messages aux nouveaux venus. Vous pouvez utiliser des BBCodes.'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Votre messages aux nouveaux venus. Vous pouvez utiliser des BBCodes.';
$a->strings['Add a link to global support forum'] = 'Ajouter un lien vers le forum de support global'; $a->strings['Add a link to global support group'] = 'Ajouter un lien vers le groupe de support global';
$a->strings['Should a link to the global support forum be displayed?'] = 'Montrer un lien vers le forum de support global?'; $a->strings['Should a link to the global support group be displayed?'] = 'Montrer un lien vers le groupe de support global ?';
$a->strings['Add a link to the local support forum'] = 'Ajouter un lien vers le forum de support local'; $a->strings['Add a link to the local support group'] = 'Ajouter un lien vers le groupe de support local';
$a->strings['If you have a local support forum and want to have a link displayed in the widget, check this box.'] = 'Si vous avez un forum d\'assistance local et désirez avoir un lien affiché dans l\'appliquette/widget, cochez cette case.'; $a->strings['If you have a local support group and want to have a link displayed in the widget, check this box.'] = 'Si vous avez un groupe de support local et désirez avoir un lien affiché dans l\'appliquette/widget, cochez cette case.';
$a->strings['Name of the local support group'] = 'Nom du groupe de support local'; $a->strings['Name of the local support group'] = 'Nom du groupe de support local';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Si vous avez coché la case ci-dessus, spécifiez le <em>nom d\'utilisateur</em> du groupe de support local (par ex. "helpers")'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Si vous avez coché la case ci-dessus, spécifiez le <em>nom d\'utilisateur</em> du groupe de support local (par ex. "helpers")';

View file

@ -4,15 +4,15 @@
# #
# #
# Translators: # Translators:
# Balázs Úr, 2020-2021 # Balázs Úr, 2020-2021,2023
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Balázs Úr, 2020-2021\n" "Last-Translator: Balázs Úr, 2020-2021,2023\n"
"Language-Team: Hungarian (http://www.transifex.com/Friendica/friendica/language/hu/)\n" "Language-Team: Hungarian (http://app.transifex.com/Friendica/friendica/language/hu/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -28,48 +28,48 @@ msgid "Tips for New Members"
msgstr "Tippek új tagoknak" msgstr "Tippek új tagoknak"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Globális támogató fórum" msgstr "Globális támogatási csoport"
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Helyi támogató fórum" msgstr "Helyi támogatási csoport"
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Beállítások mentése" msgstr "Beállítások mentése"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Üzenet" msgstr "Üzenet"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Az Ön üzenete az új tagoknak. Itt használhat BBCode-ot." msgstr "Az Ön üzenete az új tagoknak. Itt használhat BBCode-ot."
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "A globális támogató fórumra mutató hivatkozás hozzáadása" msgstr "A globális támogatási csoportra mutató hivatkozás hozzáadása"
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Meg kell jeleníteni a globális támogató fórumra mutató hivatkozást?" msgstr "Meg kell jeleníteni a globális támogatási csoportra mutató hivatkozást?"
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "A helyi támogató fórumra mutató hivatkozás hozzáadása" msgstr "A helyi támogatási csoportra mutató hivatkozás hozzáadása"
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Ha van helyi támogató fóruma és szeretne egy hivatkozást megjeleníteni a felületi elemben, akkor jelölje be azt a négyzetet." msgstr "Ha van helyi támogatási csoportja és meg szeretne jeleníteni egy hivatkozást a felületi elemben, akkor jelölje be ezt a négyzetet."
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "A helyi támogató csoport neve" msgstr "A helyi támogató csoport neve"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,14 +7,14 @@ function string_plural_select_hu($n){
}} }}
$a->strings['New Member'] = 'Új tag'; $a->strings['New Member'] = 'Új tag';
$a->strings['Tips for New Members'] = 'Tippek új tagoknak'; $a->strings['Tips for New Members'] = 'Tippek új tagoknak';
$a->strings['Global Support Forum'] = 'Globális támogató fórum'; $a->strings['Global Support Group'] = 'Globális támogatási csoport';
$a->strings['Local Support Forum'] = 'Helyi támogató fórum'; $a->strings['Local Support Group'] = 'Helyi támogatási csoport';
$a->strings['Save Settings'] = 'Beállítások mentése'; $a->strings['Save Settings'] = 'Beállítások mentése';
$a->strings['Message'] = 'Üzenet'; $a->strings['Message'] = 'Üzenet';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Az Ön üzenete az új tagoknak. Itt használhat BBCode-ot.'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Az Ön üzenete az új tagoknak. Itt használhat BBCode-ot.';
$a->strings['Add a link to global support forum'] = 'A globális támogató fórumra mutató hivatkozás hozzáadása'; $a->strings['Add a link to global support group'] = 'A globális támogatási csoportra mutató hivatkozás hozzáadása';
$a->strings['Should a link to the global support forum be displayed?'] = 'Meg kell jeleníteni a globális támogató fórumra mutató hivatkozást?'; $a->strings['Should a link to the global support group be displayed?'] = 'Meg kell jeleníteni a globális támogatási csoportra mutató hivatkozást?';
$a->strings['Add a link to the local support forum'] = 'A helyi támogató fórumra mutató hivatkozás hozzáadása'; $a->strings['Add a link to the local support group'] = 'A helyi támogatási csoportra mutató hivatkozás hozzáadása';
$a->strings['If you have a local support forum and want to have a link displayed in the widget, check this box.'] = 'Ha van helyi támogató fóruma és szeretne egy hivatkozást megjeleníteni a felületi elemben, akkor jelölje be azt a négyzetet.'; $a->strings['If you have a local support group and want to have a link displayed in the widget, check this box.'] = 'Ha van helyi támogatási csoportja és meg szeretne jeleníteni egy hivatkozást a felületi elemben, akkor jelölje be ezt a négyzetet.';
$a->strings['Name of the local support group'] = 'A helyi támogató csoport neve'; $a->strings['Name of the local support group'] = 'A helyi támogató csoport neve';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Ha bejelölte a fentit, akkor itt adja meg a helyi támogató csoport <em>becenevét</em> (például segítők)'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Ha bejelölte a fentit, akkor itt adja meg a helyi támogató csoport <em>becenevét</em> (például segítők)';

View file

@ -10,7 +10,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Sylke Vicious <silkevicious@gmail.com>, 2020-2021\n" "Last-Translator: Sylke Vicious <silkevicious@gmail.com>, 2020-2021\n"
"Language-Team: Italian (http://app.transifex.com/Friendica/friendica/language/it/)\n" "Language-Team: Italian (http://app.transifex.com/Friendica/friendica/language/it/)\n"
@ -29,48 +29,48 @@ msgid "Tips for New Members"
msgstr "Consigli per i Nuovi Utenti" msgstr "Consigli per i Nuovi Utenti"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Forum Globale di Supporto" msgstr ""
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Forum Locale di Supporto" msgstr ""
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Salva Impostazioni" msgstr "Salva Impostazioni"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Messaggio" msgstr "Messaggio"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Il tuo messaggio per i nuovi utenti. Puoi usare BBCode" msgstr "Il tuo messaggio per i nuovi utenti. Puoi usare BBCode"
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Aggiunge un collegamento al forum di supporto globale" msgstr ""
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Mostrare il collegamento al forum di supporto globale?" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Aggiunge un collegamento al forum di supporto locale" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Se hai un forum di supporto locale e vuoi che sia mostrato il collegamento nel widget, seleziona questo box." msgstr ""
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Nome del gruppo locale di supporto" msgstr "Nome del gruppo locale di supporto"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,14 +7,8 @@ function string_plural_select_it($n){
}} }}
$a->strings['New Member'] = 'Nuovi Utenti'; $a->strings['New Member'] = 'Nuovi Utenti';
$a->strings['Tips for New Members'] = 'Consigli per i Nuovi Utenti'; $a->strings['Tips for New Members'] = 'Consigli per i Nuovi Utenti';
$a->strings['Global Support Forum'] = 'Forum Globale di Supporto';
$a->strings['Local Support Forum'] = 'Forum Locale di Supporto';
$a->strings['Save Settings'] = 'Salva Impostazioni'; $a->strings['Save Settings'] = 'Salva Impostazioni';
$a->strings['Message'] = 'Messaggio'; $a->strings['Message'] = 'Messaggio';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Il tuo messaggio per i nuovi utenti. Puoi usare BBCode'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Il tuo messaggio per i nuovi utenti. Puoi usare BBCode';
$a->strings['Add a link to global support forum'] = 'Aggiunge un collegamento al forum di supporto globale';
$a->strings['Should a link to the global support forum be displayed?'] = 'Mostrare il collegamento al forum di supporto globale?';
$a->strings['Add a link to the local support forum'] = 'Aggiunge un collegamento al forum di supporto locale';
$a->strings['If you have a local support forum and want to have a link displayed in the widget, check this box.'] = 'Se hai un forum di supporto locale e vuoi che sia mostrato il collegamento nel widget, seleziona questo box.';
$a->strings['Name of the local support group'] = 'Nome del gruppo locale di supporto'; $a->strings['Name of the local support group'] = 'Nome del gruppo locale di supporto';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Se hai selezionato il box sopra, specifica qui il <em>nome utente</em> del gruppo locale di supporto (e.s. \'supporto\')'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Se hai selezionato il box sopra, specifica qui il <em>nome utente</em> del gruppo locale di supporto (e.s. \'supporto\')';

View file

@ -10,10 +10,10 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Piotr Strębski <strebski@gmail.com>, 2022\n" "Last-Translator: Piotr Strębski <strebski@gmail.com>, 2022\n"
"Language-Team: Polish (http://www.transifex.com/Friendica/friendica/language/pl/)\n" "Language-Team: Polish (http://app.transifex.com/Friendica/friendica/language/pl/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -29,48 +29,48 @@ msgid "Tips for New Members"
msgstr "Wskazówki dla nowych użytkowników" msgstr "Wskazówki dla nowych użytkowników"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Globalne forum pomocy technicznej" msgstr ""
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Lokalne Forum Wsparcia" msgstr ""
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Zapisz ustawienia" msgstr "Zapisz ustawienia"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Wiadomość" msgstr "Wiadomość"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Twoja wiadomość dla nowych członków. Możesz tutaj użyć bbcode." msgstr "Twoja wiadomość dla nowych członków. Możesz tutaj użyć bbcode."
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Dodaj odnośnik do globalnego forum pomocy technicznej" msgstr ""
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Czy powinien być wyświetlany odnośnik do globalnego forum pomocy technicznej?" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Dodaj odnośnik do lokalnego forum pomocy technicznej" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Jeżeli masz lokalne wsparcie forum i chcesz mieć łącze wyświetlane w widżecie, zaznacz to pole wyboru." msgstr ""
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Nazwa grupy lokalnej pomocy technicznej" msgstr "Nazwa grupy lokalnej pomocy technicznej"
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,14 +7,8 @@ function string_plural_select_pl($n){
}} }}
$a->strings['New Member'] = 'Nowy użytkownik'; $a->strings['New Member'] = 'Nowy użytkownik';
$a->strings['Tips for New Members'] = 'Wskazówki dla nowych użytkowników'; $a->strings['Tips for New Members'] = 'Wskazówki dla nowych użytkowników';
$a->strings['Global Support Forum'] = 'Globalne forum pomocy technicznej';
$a->strings['Local Support Forum'] = 'Lokalne Forum Wsparcia';
$a->strings['Save Settings'] = 'Zapisz ustawienia'; $a->strings['Save Settings'] = 'Zapisz ustawienia';
$a->strings['Message'] = 'Wiadomość'; $a->strings['Message'] = 'Wiadomość';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Twoja wiadomość dla nowych członków. Możesz tutaj użyć bbcode.'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Twoja wiadomość dla nowych członków. Możesz tutaj użyć bbcode.';
$a->strings['Add a link to global support forum'] = 'Dodaj odnośnik do globalnego forum pomocy technicznej';
$a->strings['Should a link to the global support forum be displayed?'] = 'Czy powinien być wyświetlany odnośnik do globalnego forum pomocy technicznej?';
$a->strings['Add a link to the local support forum'] = 'Dodaj odnośnik do lokalnego forum pomocy technicznej';
$a->strings['If you have a local support forum and want to have a link displayed in the widget, check this box.'] = 'Jeżeli masz lokalne wsparcie forum i chcesz mieć łącze wyświetlane w widżecie, zaznacz to pole wyboru.';
$a->strings['Name of the local support group'] = 'Nazwa grupy lokalnej pomocy technicznej'; $a->strings['Name of the local support group'] = 'Nazwa grupy lokalnej pomocy technicznej';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Jeśli zaznaczyłeś powyższe, określ tutaj pseudonim lokalnej grupy wsparcia (np. Pomocnicy)'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Jeśli zaznaczyłeś powyższe, określ tutaj pseudonim lokalnej grupy wsparcia (np. Pomocnicy)';

View file

@ -9,67 +9,67 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-06-01 14:12+0200\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2020-04-23 14:23+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Alexander An <ravnina@gmail.com>\n" "Last-Translator: Alexander An <ravnina@gmail.com>, 2020\n"
"Language-Team: Russian (http://www.transifex.com/Friendica/friendica/language/ru/)\n" "Language-Team: Russian (http://app.transifex.com/Friendica/friendica/language/ru/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: ru\n" "Language: ru\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#: newmemberwidget.php:21 #: newmemberwidget.php:29
msgid "New Member" msgid "New Member"
msgstr "Новичок" msgstr "Новичок"
#: newmemberwidget.php:22 #: newmemberwidget.php:30
msgid "Tips for New Members" msgid "Tips for New Members"
msgstr "Советы новичкам" msgstr "Советы новичкам"
#: newmemberwidget.php:24 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "Общий форум поддержки" msgstr ""
#: newmemberwidget.php:26 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Местный форум поддержки" msgstr ""
#: newmemberwidget.php:49 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Сохранить настройки" msgstr "Сохранить настройки"
#: newmemberwidget.php:50 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Сообщение" msgstr "Сообщение"
#: newmemberwidget.php:50 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "Ваше сообщение новичкам. Вы можете использовать BBCode." msgstr "Ваше сообщение новичкам. Вы можете использовать BBCode."
#: newmemberwidget.php:51 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "Добавить ссылку на общий форум поддержки" msgstr ""
#: newmemberwidget.php:51 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "Показывать ссылку на общий форум поддержки?" msgstr ""
#: newmemberwidget.php:52 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "Добавить ссылку на местный форум поддержки" msgstr ""
#: newmemberwidget.php:52 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and wand to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "Если у вас есть местный форум поддержки и вы хотите добавить ссылку на него, включите это." msgstr ""
#: newmemberwidget.php:53 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "Название местной группы поддержки" msgstr "Название местной группы поддержки"
#: newmemberwidget.php:53 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,14 +7,8 @@ function string_plural_select_ru($n){
}} }}
$a->strings['New Member'] = 'Новичок'; $a->strings['New Member'] = 'Новичок';
$a->strings['Tips for New Members'] = 'Советы новичкам'; $a->strings['Tips for New Members'] = 'Советы новичкам';
$a->strings['Global Support Forum'] = 'Общий форум поддержки';
$a->strings['Local Support Forum'] = 'Местный форум поддержки';
$a->strings['Save Settings'] = 'Сохранить настройки'; $a->strings['Save Settings'] = 'Сохранить настройки';
$a->strings['Message'] = 'Сообщение'; $a->strings['Message'] = 'Сообщение';
$a->strings['Your message for new members. You can use bbcode here.'] = 'Ваше сообщение новичкам. Вы можете использовать BBCode.'; $a->strings['Your message for new members. You can use bbcode here.'] = 'Ваше сообщение новичкам. Вы можете использовать BBCode.';
$a->strings['Add a link to global support forum'] = 'Добавить ссылку на общий форум поддержки';
$a->strings['Should a link to the global support forum be displayed?'] = 'Показывать ссылку на общий форум поддержки?';
$a->strings['Add a link to the local support forum'] = 'Добавить ссылку на местный форум поддержки';
$a->strings['If you have a local support forum and wand to have a link displayed in the widget, check this box.'] = 'Если у вас есть местный форум поддержки и вы хотите добавить ссылку на него, включите это.';
$a->strings['Name of the local support group'] = 'Название местной группы поддержки'; $a->strings['Name of the local support group'] = 'Название местной группы поддержки';
$a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Если вы включили настройку выше, укажите <em>ник</em>местной группы поддержки пользователей.'; $a->strings['If you checked the above, specify the <em>nickname</em> of the local support group here (i.e. helpers)'] = 'Если вы включили настройку выше, укажите <em>ник</em>местной группы поддержки пользователей.';

View file

@ -9,10 +9,10 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2022-01-16 00:48+0000\n" "PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Kristoffer Grundström <lovaren@gmail.com>\n" "Last-Translator: Kristoffer Grundström <lovaren@gmail.com>, 2022\n"
"Language-Team: Swedish (http://www.transifex.com/Friendica/friendica/language/sv/)\n" "Language-Team: Swedish (http://app.transifex.com/Friendica/friendica/language/sv/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -28,48 +28,48 @@ msgid "Tips for New Members"
msgstr "Tips för nya medlemmar" msgstr "Tips för nya medlemmar"
#: newmemberwidget.php:33 #: newmemberwidget.php:33
msgid "Global Support Forum" msgid "Global Support Group"
msgstr "" msgstr ""
#: newmemberwidget.php:37 #: newmemberwidget.php:37
msgid "Local Support Forum" msgid "Local Support Group"
msgstr "Lokalt hjälpforum" msgstr ""
#: newmemberwidget.php:65 #: newmemberwidget.php:62
msgid "Save Settings" msgid "Save Settings"
msgstr "Spara inställningar" msgstr "Spara inställningar"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Message" msgid "Message"
msgstr "Meddelande" msgstr "Meddelande"
#: newmemberwidget.php:66 #: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here." msgid "Your message for new members. You can use bbcode here."
msgstr "" msgstr ""
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Add a link to global support forum" msgid "Add a link to global support group"
msgstr "" msgstr ""
#: newmemberwidget.php:67 #: newmemberwidget.php:64
msgid "Should a link to the global support forum be displayed?" msgid "Should a link to the global support group be displayed?"
msgstr "" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "Add a link to the local support forum" msgid "Add a link to the local support group"
msgstr "" msgstr ""
#: newmemberwidget.php:68 #: newmemberwidget.php:65
msgid "" msgid ""
"If you have a local support forum and want to have a link displayed in the " "If you have a local support group and want to have a link displayed in the "
"widget, check this box." "widget, check this box."
msgstr "" msgstr ""
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "Name of the local support group" msgid "Name of the local support group"
msgstr "" msgstr ""
#: newmemberwidget.php:69 #: newmemberwidget.php:66
msgid "" msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support" "If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)" " group here (i.e. helpers)"

View file

@ -7,6 +7,5 @@ function string_plural_select_sv($n){
}} }}
$a->strings['New Member'] = 'Ny medlem'; $a->strings['New Member'] = 'Ny medlem';
$a->strings['Tips for New Members'] = 'Tips för nya medlemmar'; $a->strings['Tips for New Members'] = 'Tips för nya medlemmar';
$a->strings['Local Support Forum'] = 'Lokalt hjälpforum';
$a->strings['Save Settings'] = 'Spara inställningar'; $a->strings['Save Settings'] = 'Spara inställningar';
$a->strings['Message'] = 'Meddelande'; $a->strings['Message'] = 'Meddelande';

View file

@ -4,6 +4,7 @@
# #
# #
# Translators: # Translators:
# Florent C., 2023
# Nicolas Derive, 2022-2023 # Nicolas Derive, 2022-2023
# StefOfficiel <pichard.stephane@free.fr>, 2015 # StefOfficiel <pichard.stephane@free.fr>, 2015
# Vincent Vindarel <vindarel@mailz.org>, 2018 # Vincent Vindarel <vindarel@mailz.org>, 2018
@ -13,7 +14,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-10 14:42-0500\n" "POT-Creation-Date: 2022-12-10 14:42-0500\n"
"PO-Revision-Date: 2014-06-23 10:34+0000\n" "PO-Revision-Date: 2014-06-23 10:34+0000\n"
"Last-Translator: Nicolas Derive, 2022-2023\n" "Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n" "Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@ -41,7 +42,7 @@ msgstr "Liste de mots-clés - séparés par des virgules - à cacher"
msgid "" msgid ""
"Use /expression/ to provide regular expressions, #tag to specfically match " "Use /expression/ to provide regular expressions, #tag to specfically match "
"hashtags (case-insensitive), or regular words (case-sensitive)" "hashtags (case-insensitive), or regular words (case-sensitive)"
msgstr "Utiliser /expression/ pour fournir des expressions régulières, #tag pour correspondre à un mot-dièse (hashtag, insensible à la casse), ou des mots classiques (sensible à la casse)" msgstr "Utiliser /expression/ pour fournir des expressions régulières, #tag pour correspondre à un tag (insensible à la casse), ou des mots classiques (sensible à la casse)"
#: nsfw.php:72 #: nsfw.php:72
msgid "Content Filter (NSFW and more)" msgid "Content Filter (NSFW and more)"
@ -55,7 +56,7 @@ msgstr "La compilation de l'expression régulière \"%s\" a échoué"
#: nsfw.php:154 #: nsfw.php:154
#, php-format #, php-format
msgid "Filtered tag: %s" msgid "Filtered tag: %s"
msgstr "Tag filtré: %s" msgstr "Tag filtré : %s"
#: nsfw.php:156 #: nsfw.php:156
#, php-format #, php-format

View file

@ -8,8 +8,8 @@ function string_plural_select_fr($n){
$a->strings['This addon searches for specified words/text in posts and collapses them. It can be used to filter content tagged with for instance #NSFW that may be deemed inappropriate at certain times or places, such as being at work. It is also useful for hiding irrelevant or annoying content from direct view.'] = 'Cette extension recherche des mots/textes spécifiés dans les publications et les masque. Elle peut être utilisée pour filtrer le contenu étiqueté par exemple avec #NSFW qui peut être considéré comme inapproprié à certains moments ou endroits, comme par exemple au travail. Elle est aussi utile pour cacher du contenu non pertinent ou ennuyeux d\'une vue directe.'; $a->strings['This addon searches for specified words/text in posts and collapses them. It can be used to filter content tagged with for instance #NSFW that may be deemed inappropriate at certain times or places, such as being at work. It is also useful for hiding irrelevant or annoying content from direct view.'] = 'Cette extension recherche des mots/textes spécifiés dans les publications et les masque. Elle peut être utilisée pour filtrer le contenu étiqueté par exemple avec #NSFW qui peut être considéré comme inapproprié à certains moments ou endroits, comme par exemple au travail. Elle est aussi utile pour cacher du contenu non pertinent ou ennuyeux d\'une vue directe.';
$a->strings['Enable Content filter'] = 'Activer le filtrage de contenu'; $a->strings['Enable Content filter'] = 'Activer le filtrage de contenu';
$a->strings['Comma separated list of keywords to hide'] = 'Liste de mots-clés - séparés par des virgules - à cacher'; $a->strings['Comma separated list of keywords to hide'] = 'Liste de mots-clés - séparés par des virgules - à cacher';
$a->strings['Use /expression/ to provide regular expressions, #tag to specfically match hashtags (case-insensitive), or regular words (case-sensitive)'] = 'Utiliser /expression/ pour fournir des expressions régulières, #tag pour correspondre à un mot-dièse (hashtag, insensible à la casse), ou des mots classiques (sensible à la casse)'; $a->strings['Use /expression/ to provide regular expressions, #tag to specfically match hashtags (case-insensitive), or regular words (case-sensitive)'] = 'Utiliser /expression/ pour fournir des expressions régulières, #tag pour correspondre à un tag (insensible à la casse), ou des mots classiques (sensible à la casse)';
$a->strings['Content Filter (NSFW and more)'] = 'Filtre de contenu (NSFW et autres)'; $a->strings['Content Filter (NSFW and more)'] = 'Filtre de contenu (NSFW et autres)';
$a->strings['Regular expression "%s" fails to compile'] = 'La compilation de l\'expression régulière "%s" a échoué'; $a->strings['Regular expression "%s" fails to compile'] = 'La compilation de l\'expression régulière "%s" a échoué';
$a->strings['Filtered tag: %s'] = 'Tag filtré: %s'; $a->strings['Filtered tag: %s'] = 'Tag filtré : %s';
$a->strings['Filtered word: %s'] = 'Mot filtré: %s'; $a->strings['Filtered word: %s'] = 'Mot filtré: %s';

View file

@ -20,4 +20,9 @@
width: 100%; width: 100%;
margin-top: 25px; margin-top: 25px;
font-size: 20px; font-size: 20px;
/* The pageheader box */
padding: 20px;
border: 1px solid transparent;
border-radius: 2px;
margin-bottom: 15px;
} }

View file

@ -13,5 +13,5 @@ $a->strings['Absolute path to your Matomo (Piwik) installation. (without protoco
$a->strings['Site ID'] = 'Site ID'; $a->strings['Site ID'] = 'Site ID';
$a->strings['Show opt-out cookie link?'] = 'Show opt-out cookie link?'; $a->strings['Show opt-out cookie link?'] = 'Show opt-out cookie link?';
$a->strings['Asynchronous tracking'] = 'Asynchronous tracking'; $a->strings['Asynchronous tracking'] = 'Asynchronous tracking';
$a->strings['Shortcut path to the script (\'/js/\' instead of \'/piwik.js\')'] = 'Shortcut path to the script (\'/js/\' instead of \'/piwik.js\')';
$a->strings['Settings updated.'] = 'Settings updated.'; $a->strings['Settings updated.'] = 'Settings updated.';
$a->strings["Shortcut path to the script ('/js/' instead of '/piwik.js')"] = "Shortcut path to the script ('/js/' instead of '/piwik.js')";

View file

@ -4,6 +4,7 @@
# #
# #
# Translators: # Translators:
# Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022 # Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# Nicolas Derive, 2022 # Nicolas Derive, 2022
# ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015 # ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015
@ -12,23 +13,23 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n" "POT-Creation-Date: 2023-05-01 07:39+0200\n"
"PO-Revision-Date: 2014-06-23 11:18+0000\n" "PO-Revision-Date: 2014-06-23 11:18+0000\n"
"Last-Translator: Nicolas Derive, 2022\n" "Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n" "Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: fr\n" "Language: fr\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: piwik.php:87 #: piwik.php:96
msgid "" msgid ""
"This website is tracked using the <a href='http://www.matomo.org'>Matomo</a>" "This website is tracked using the <a href='http://www.matomo.org'>Matomo</a>"
" analytics tool." " analytics tool."
msgstr "Ce site Internet utilise <a href='http://www.matomo.org'>Matomo</a> pour mesurer son audience." msgstr "Ce site Internet utilise <a href='http://www.matomo.org'>Matomo</a> pour mesurer son audience."
#: piwik.php:90 #: piwik.php:99
#, php-format #, php-format
msgid "" msgid ""
"If you do not want that your visits are logged in this way you <a " "If you do not want that your visits are logged in this way you <a "
@ -36,28 +37,32 @@ msgid ""
"visits of the site</a> (opt-out)." "visits of the site</a> (opt-out)."
msgstr "Si vous ne désirez pas que vos visites soient journalisées de cette manière, vous <a href='%s'>pouvez définir un cookie pour empêcher Matomo/Piwik de surveiller de prochaines visites sur le site</a> (opt-out)" msgstr "Si vous ne désirez pas que vos visites soient journalisées de cette manière, vous <a href='%s'>pouvez définir un cookie pour empêcher Matomo/Piwik de surveiller de prochaines visites sur le site</a> (opt-out)"
#: piwik.php:97 #: piwik.php:108
msgid "Save Settings" msgid "Save Settings"
msgstr "Sauvegarder les paramètres" msgstr "Sauvegarder les paramètres"
#: piwik.php:98 #: piwik.php:109
msgid "Matomo (Piwik) Base URL" msgid "Matomo (Piwik) Base URL"
msgstr "URL de base de Matomo (Piwik)" msgstr "URL de base de Matomo (Piwik)"
#: piwik.php:98 #: piwik.php:109
msgid "" msgid ""
"Absolute path to your Matomo (Piwik) installation. (without protocol " "Absolute path to your Matomo (Piwik) installation. (without protocol "
"(http/s), with trailing slash)" "(http/s), with trailing slash)"
msgstr "Chemin absolu vers votre installation Matomo (Piwik) (sans protocole (http/s), avec un slash à la fin)." msgstr "Chemin absolu vers votre installation Matomo (Piwik) (sans protocole (http/s), avec un slash à la fin)."
#: piwik.php:99 #: piwik.php:110
msgid "Site ID" msgid "Site ID"
msgstr "ID du site" msgstr "ID du site"
#: piwik.php:100 #: piwik.php:111
msgid "Show opt-out cookie link?" msgid "Show opt-out cookie link?"
msgstr "Montrer le lien d'opt-out pour les cookies ?" msgstr "Montrer le lien d'opt-out pour les cookies ?"
#: piwik.php:101 #: piwik.php:112
msgid "Asynchronous tracking" msgid "Asynchronous tracking"
msgstr "Suivi asynchrone" msgstr "Suivi asynchrone"
#: piwik.php:113
msgid "Shortcut path to the script ('/js/' instead of '/piwik.js')"
msgstr "Chemin réduit vers le script ('/js/' au lieu de '/piwik.js') "

View file

@ -13,3 +13,4 @@ $a->strings['Absolute path to your Matomo (Piwik) installation. (without protoco
$a->strings['Site ID'] = 'ID du site'; $a->strings['Site ID'] = 'ID du site';
$a->strings['Show opt-out cookie link?'] = 'Montrer le lien d\'opt-out pour les cookies ?'; $a->strings['Show opt-out cookie link?'] = 'Montrer le lien d\'opt-out pour les cookies ?';
$a->strings['Asynchronous tracking'] = 'Suivi asynchrone'; $a->strings['Asynchronous tracking'] = 'Suivi asynchrone';
$a->strings['Shortcut path to the script (\'/js/\' instead of \'/piwik.js\')'] = 'Chemin réduit vers le script (\'/js/\' au lieu de \'/piwik.js\') ';

View file

@ -4,6 +4,7 @@
# #
# #
# Translators: # Translators:
# Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022 # Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015 # ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015
# StefOfficiel <pichard.stephane@free.fr>, 2015 # StefOfficiel <pichard.stephane@free.fr>, 2015
@ -11,86 +12,86 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: friendica\n" "Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-21 19:17-0500\n" "POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 11:30+0000\n" "PO-Revision-Date: 2014-06-23 11:30+0000\n"
"Last-Translator: Hypolite Petovan <hypolite@mrpetovan.com>, 2022\n" "Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n" "Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: fr\n" "Language: fr\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: pumpio.php:57 #: pumpio.php:62
msgid "Permission denied." msgid "Permission denied."
msgstr "Permission refusée." msgstr "Permission refusée."
#: pumpio.php:152 #: pumpio.php:156
#, php-format #, php-format
msgid "Unable to register the client at the pump.io server '%s'." msgid "Unable to register the client at the pump.io server '%s'."
msgstr "Impossible d'enregistrer le client sur le serveur pump.io \"%s\"." msgstr "Impossible d'enregistrer le client sur le serveur pump.io \"%s\"."
#: pumpio.php:192 #: pumpio.php:196
msgid "You are now authenticated to pumpio." msgid "You are now authenticated to pumpio."
msgstr "Vous êtes maintenant authentifié sur pump.io." msgstr "Vous êtes maintenant authentifié sur pump.io."
#: pumpio.php:193 #: pumpio.php:197
msgid "return to the connector page" msgid "return to the connector page"
msgstr "Retourner à la page du connecteur" msgstr "Retourner à la page du connecteur"
#: pumpio.php:213 #: pumpio.php:217
msgid "Post to pumpio" msgid "Post to pumpio"
msgstr "Publier sur pump.io" msgstr "Publier sur pump.io"
#: pumpio.php:237 #: pumpio.php:241
msgid "Save Settings" msgid "Save Settings"
msgstr "Sauvegarder les paramètres" msgstr "Sauvegarder les paramètres"
#: pumpio.php:239 #: pumpio.php:243
msgid "Delete this preset" msgid "Delete this preset"
msgstr "Supprimer ce préréglage" msgstr "Supprimer ce préréglage"
#: pumpio.php:245 #: pumpio.php:249
msgid "Authenticate your pump.io connection" msgid "Authenticate your pump.io connection"
msgstr "Identifiez votre connexion à pump.io" msgstr "Identifiez votre connexion à pump.io"
#: pumpio.php:252 #: pumpio.php:256
msgid "Pump.io servername (without \"http://\" or \"https://\" )" msgid "Pump.io servername (without \"http://\" or \"https://\" )"
msgstr "Domaine du serveur Pump.io (sans \"http://\" ou \"https://\")" msgstr "Domaine du serveur Pump.io (sans \"http://\" ou \"https://\")"
#: pumpio.php:253 #: pumpio.php:257
msgid "Pump.io username (without the servername)" msgid "Pump.io username (without the servername)"
msgstr "Nom d'utilisateur Pump.io (sans le domaine de serveur)" msgstr "Nom d'utilisateur Pump.io (sans le domaine de serveur)"
#: pumpio.php:254 #: pumpio.php:258
msgid "Import the remote timeline" msgid "Import the remote timeline"
msgstr "Importer la timeline distante" msgstr "Importer le flux distant"
#: pumpio.php:255 #: pumpio.php:259
msgid "Enable Pump.io Post Addon" msgid "Enable Pump.io Post Addon"
msgstr "Activer l'extension Pump.io" msgstr "Activer l'extension Pump.io"
#: pumpio.php:256 #: pumpio.php:260
msgid "Post to Pump.io by default" msgid "Post to Pump.io by default"
msgstr "Publier sur Pump.io par défaut" msgstr "Publier sur Pump.io par défaut"
#: pumpio.php:257 #: pumpio.php:261
msgid "Should posts be public?" msgid "Should posts be public?"
msgstr "Les messages devraient être publiques ?" msgstr "Les messages devraient être publiques ?"
#: pumpio.php:258 #: pumpio.php:262
msgid "Mirror all public posts" msgid "Mirror all public posts"
msgstr "Refléter toutes les publications publiques" msgstr "Refléter toutes les publications publiques"
#: pumpio.php:263 #: pumpio.php:267
msgid "Pump.io Import/Export/Mirror" msgid "Pump.io Import/Export/Mirror"
msgstr "Import/Export/Miroir Pump.io" msgstr "Import/Export/Miroir Pump.io"
#: pumpio.php:920 #: pumpio.php:924
msgid "status" msgid "status"
msgstr "statut" msgstr "statut"
#: pumpio.php:924 #: pumpio.php:928
#, php-format #, php-format
msgid "%1$s likes %2$s's %3$s" msgid "%1$s likes %2$s's %3$s"
msgstr "%1$s aime lea %3$s de %2$s" msgstr "%1$s aime lea %3$s de %2$s"

View file

@ -15,7 +15,7 @@ $a->strings['Delete this preset'] = 'Supprimer ce préréglage';
$a->strings['Authenticate your pump.io connection'] = 'Identifiez votre connexion à pump.io'; $a->strings['Authenticate your pump.io connection'] = 'Identifiez votre connexion à pump.io';
$a->strings['Pump.io servername (without "http://" or "https://" )'] = 'Domaine du serveur Pump.io (sans "http://" ou "https://")'; $a->strings['Pump.io servername (without "http://" or "https://" )'] = 'Domaine du serveur Pump.io (sans "http://" ou "https://")';
$a->strings['Pump.io username (without the servername)'] = 'Nom d\'utilisateur Pump.io (sans le domaine de serveur)'; $a->strings['Pump.io username (without the servername)'] = 'Nom d\'utilisateur Pump.io (sans le domaine de serveur)';
$a->strings['Import the remote timeline'] = 'Importer la timeline distante'; $a->strings['Import the remote timeline'] = 'Importer le flux distant';
$a->strings['Enable Pump.io Post Addon'] = 'Activer l\'extension Pump.io'; $a->strings['Enable Pump.io Post Addon'] = 'Activer l\'extension Pump.io';
$a->strings['Post to Pump.io by default'] = 'Publier sur Pump.io par défaut'; $a->strings['Post to Pump.io by default'] = 'Publier sur Pump.io par défaut';
$a->strings['Should posts be public?'] = 'Les messages devraient être publiques ?'; $a->strings['Should posts be public?'] = 'Les messages devraient être publiques ?';

View file

@ -8,32 +8,35 @@
"packages": [ "packages": [
{ {
"name": "akeeba/s3", "name": "akeeba/s3",
"version": "2.0.0", "version": "2.3.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/akeeba/s3.git", "url": "https://github.com/akeeba/s3.git",
"reference": "01520dae1f736555e08efda0ddc1044701bd340a" "reference": "7f5b3e929c93eb02ba24472560c0cbbef735aed9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/akeeba/s3/zipball/01520dae1f736555e08efda0ddc1044701bd340a", "url": "https://api.github.com/repos/akeeba/s3/zipball/7f5b3e929c93eb02ba24472560c0cbbef735aed9",
"reference": "01520dae1f736555e08efda0ddc1044701bd340a", "reference": "7f5b3e929c93eb02ba24472560c0cbbef735aed9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"php": ">=7.1.0 <8.1" "php": ">=7.1.0 <8.4"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"files": [
"src/aliasing.php"
],
"psr-4": { "psr-4": {
"Akeeba\\Engine\\Postproc\\Connector\\S3v4\\": "src" "Akeeba\\S3\\": "src"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"GPL-3.0+" "GPL-3.0-or-later"
], ],
"authors": [ "authors": [
{ {
@ -48,7 +51,7 @@
"keywords": [ "keywords": [
"s3" "s3"
], ],
"time": "2020-11-30T14:03:55+00:00" "time": "2023-09-26T11:40:10+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

View file

@ -1,5 +1,6 @@
/.idea/ /.idea/
/000/ /000/
/minitest/config.php /minitest/config.*
!/minitest/config.dist.php
/minitest/tmp /minitest/tmp
/vendor/ /vendor/

View file

@ -4,29 +4,53 @@ A compact, dependency-less Amazon S3 API client implementing the most commonly u
## Why reinvent the wheel ## Why reinvent the wheel
After having a lot of impossible to debug problems with Amazon's Guzzle-based AWS SDK we decided to roll our own connector for Amazon S3. This is by no means a complete implementation, just a small subset of S3's features which are required by our software. The design goals are simplicity, no external dependencies and low memory footprint. After having a lot of impossible to debug problems with Amazon's Guzzle-based AWS SDK we decided to roll our own connector for Amazon S3. This is by no means a complete implementation, just a small subset of S3's features which are required by our software. The design goals are simplicity, no external dependencies and a low memory footprint.
This code was originally based on [S3.php written by Donovan Schonknecht](http://undesigned.org.za/2007/10/22/amazon-s3-php-class) which is available under a BSD-like license. This repository no longer reflects the original author's work and should not be confused with it. This code was originally based on [S3.php written by Donovan Schonknecht](http://undesigned.org.za/2007/10/22/amazon-s3-php-class) which is available under a BSD-like license. This repository no longer reflects the original author's work and should not be confused with it.
This software is distributed under the GNU General Public License version 3 or, at your option, any later version published by the Free Software Foundation (FSF). In short, it's "GPLv3+". This software is distributed under the GNU General Public License version 3 or, at your option, any later version published by the Free Software Foundation (FSF). In short, it's GPL-3.0-or-later, as noted in composer.json.
## Important note about version 2 ## Important notes about version 2
Akeeba Amazon S3 Connector version 2 has dropped support for PPH 5.3 to 7.0 inclusive. It is only compatible with PHP 7.1 or later, up to and including PHP 8.0. ### PHP version support since 2.0
The most significant change in this version is that all methods use scalar type hints for parameters and return values. This _may_ break existing consumers which relied on implicit type conversion e.g. passing strings containing integer values instead of _actual_ integer values. Akeeba Amazon S3 Connector version 2 has dropped support for PHP 5.3 to 7.0 inclusive.
The most significant change in this version is that all methods use scalar type hints for parameters and return values. This _may_ break existing consumers which relied on implicit type conversion.
### Namespace change since 2.3
Up to and including version 2.2 of the library, the namespace was `\Akeeba\Engine\Postproc\Connector\S3v4`. From version 2.3 of the library the namespace has changed to `\Akeeba\S3`.
The library automatically registers aliases of the old classes to the new ones, thus ensuring updating the library will not introduce backwards incompatible changes. This is why it's not a major version update. Aliases will remain in place until at least version 3.0 of the library.
## Using the connector ## Using the connector
You need to define a constant before using or referencing any class in the library:
```php
defined('AKEEBAENGINE') or define('AKEEBAENGINE', 1);
```
All library files have a line similar to
```php
defined('AKEEBAENGINE') or die();
```
to prevent direct access to the libraries files. This is intentional. The primary use case for this library is mass-distributed software which gets installed in a publicly accessible subdirectory of the web root. This line prevents any accidental path disclosure from PHP error messages if someone were to access these files directly on misconfigured servers.
If you are writing a Joomla extension, especially a plugin or module, please _always_ check if the constant has already been defined before defining it yourself. Thank you!
### Get a connector object ### Get a connector object
```php ```php
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'YourAmazonAccessKey', 'YourAmazonAccessKey',
'YourAmazonSecretKey' 'YourAmazonSecretKey'
); );
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
If you are running inside an Amazon EC2 instance you can fetch temporary credentials from the instance's metadata If you are running inside an Amazon EC2 instance you can fetch temporary credentials from the instance's metadata
@ -37,7 +61,7 @@ IP hosting the instance's metadata cache service):
$role = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); $role = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/');
$jsonCredentials = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/' . $role); $jsonCredentials = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/' . $role);
$credentials = json_decode($jsonCredentials, true); $credentials = json_decode($jsonCredentials, true);
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
$credentials['AccessKeyId'], $credentials['AccessKeyId'],
$credentials['SecretAccessKey'], $credentials['SecretAccessKey'],
'v4', 'v4',
@ -45,14 +69,14 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
); );
$configuration->setToken($credentials['Token']); $configuration->setToken($credentials['Token']);
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
where `$yourRegion` is the AWS region of your bucket, e.g. `us-east-1`. Please note that we are passing the security where `$yourRegion` is the AWS region of your bucket, e.g. `us-east-1`. Please note that we are passing the security
token (`$credentials['Token']`) to the Configuration object. This is REQUIRED. The temporary credentials returned by token (`$credentials['Token']`) to the Configuration object. This is REQUIRED. The temporary credentials returned by
the metadata service won't work without it. the metadata service won't work without it.
Also worth noting is that the temporary credentials don't last forever. Check the `$credentials['Expiration']` to see Another point worth noting is that the temporary credentials don't last forever. Check the `$credentials['Expiration']` to see
when they are about to expire. Amazon recommends that you retry fetching new credentials from the metadata service when they are about to expire. Amazon recommends that you retry fetching new credentials from the metadata service
10 minutes before your cached credentials are set to expire. The metadata service is guaranteed to provision fresh 10 minutes before your cached credentials are set to expire. The metadata service is guaranteed to provision fresh
temporary credentials by that time. temporary credentials by that time.
@ -120,21 +144,21 @@ The last parameter (common prefixes) controls the listing of "subdirectories"
From a file: From a file:
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt'); $connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
``` ```
From a string: From a string:
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromData($sourceString); $input = \Akeeba\S3\Input::createFromData($sourceString);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt'); $connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
``` ```
From a stream resource: From a stream resource:
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromResource($streamHandle, false); $input = \Akeeba\S3\Input::createFromResource($streamHandle, false);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt'); $connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
``` ```
@ -145,7 +169,7 @@ In all cases the entirety of the file has to be loaded in memory.
Files are uploaded in 5Mb chunks. Files are uploaded in 5Mb chunks.
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$uploadId = $connector->startMultipart($input, 'mybucket', 'mypath/movie.mov'); $uploadId = $connector->startMultipart($input, 'mybucket', 'mypath/movie.mov');
$eTags = array(); $eTags = array();
@ -155,7 +179,7 @@ $partNumber = 0;
do do
{ {
// IMPORTANT: You MUST create the input afresh before each uploadMultipart call // IMPORTANT: You MUST create the input afresh before each uploadMultipart call
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$input->setUploadID($uploadId); $input->setUploadID($uploadId);
$input->setPartNumber(++$partNumber); $input->setPartNumber(++$partNumber);
@ -169,7 +193,7 @@ do
while (!is_null($eTag)); while (!is_null($eTag));
// IMPORTANT: You MUST create the input afresh before finalising the multipart upload // IMPORTANT: You MUST create the input afresh before finalising the multipart upload
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$input->setUploadID($uploadId); $input->setUploadID($uploadId);
$input->setEtags($eTags); $input->setEtags($eTags);
@ -209,6 +233,23 @@ $content = $connector->getObject('mybucket', 'path/to/file.jpg', false);
$connector->deleteObject('mybucket', 'path/to/file.jpg'); $connector->deleteObject('mybucket', 'path/to/file.jpg');
``` ```
### Test if an object exists
```php
try
{
$headers = $connector->headObject('mybucket', 'path/to/file.jpg');
$exists = true;
}
catch (\Akeeba\S3\Exception\CannotGetFile $e)
{
$headers = [];
$exists = false;
}
```
The `$headers` variable contains an array with the S3 headers returned by the [HeadObject(https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) API call. The header keys are always in lowercase. Please note that _not all_ of the headers Amazon describes in their documentation are returned in every request.
## Configuration options ## Configuration options
The Configuration option has optional methods which can be used to enable some useful features in the connector. The Configuration option has optional methods which can be used to enable some useful features in the connector.
@ -216,7 +257,7 @@ The Configuration option has optional methods which can be used to enable some u
You need to execute these methods against the Configuration object before passing it to the Connector's constructor. For example: You need to execute these methods against the Configuration object before passing it to the Connector's constructor. For example:
```php ```php
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'YourAmazonAccessKey', 'YourAmazonAccessKey',
'YourAmazonSecretKey' 'YourAmazonSecretKey'
); );
@ -225,7 +266,7 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
$configuration->setSignatureMethod('v4'); $configuration->setSignatureMethod('v4');
$configuration->setUseDualstackUrl(true); $configuration->setUseDualstackUrl(true);
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
### HTTPS vs plain HTTP ### HTTPS vs plain HTTP
@ -245,7 +286,7 @@ Please note that if the S3-compatible APi uses v4 signatures you need to enter t
```php ```php
// DigitalOcean Spaces using v4 signatures // DigitalOcean Spaces using v4 signatures
// The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/ // The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'532SZONTQ6ALKBCU94OU', '532SZONTQ6ALKBCU94OU',
'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ', 'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ',
'v4', 'v4',
@ -253,7 +294,7 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
); );
$configuration->setEndpoint('nyc3.digitaloceanspaces.com'); $configuration->setEndpoint('nyc3.digitaloceanspaces.com');
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
If your S3-compatible API uses v2 signatures you do not need to specify a region. If your S3-compatible API uses v2 signatures you do not need to specify a region.
@ -261,14 +302,14 @@ If your S3-compatible API uses v2 signatures you do not need to specify a region
```php ```php
// DigitalOcean Spaces using v2 signatures // DigitalOcean Spaces using v2 signatures
// The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/ // The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'532SZONTQ6ALKBCU94OU', '532SZONTQ6ALKBCU94OU',
'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ', 'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ',
'v2' 'v2'
); );
$configuration->setEndpoint('nyc3.digitaloceanspaces.com'); $configuration->setEndpoint('nyc3.digitaloceanspaces.com');
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
### Legacy path-style access ### Legacy path-style access
@ -282,7 +323,7 @@ You need to do:
$configuration->setUseLegacyPathStyle(true); $configuration->setUseLegacyPathStyle(true);
``` ```
Caveat: this will not work with v2 signatures if you are using Amazon AWS S3 proper. It will work with the v2 signatures if you are using a custom endpoint, though. In fact, most S3-compatible APIs implementing V2 signatures _expect_ you to use path-style access. Caveat: this will not work with v2 signatures if you are using Amazon AWS S3 proper. It will very likely work with the v2 signatures if you are using a custom endpoint, though.
### Dualstack (IPv4 and IPv6) support ### Dualstack (IPv4 and IPv6) support

View file

@ -1,13 +0,0 @@
Need to check:
endpoint in [amazon, custom]
signature in [v2, v4]
path style in [true, false]
upload
download
presigned URL generation
presigned URL access
USING VIRTUAL HOSTING, v4 SIGNATURES
presigned URL must use s3.amazonaws.com i.e. path-style hosting (because who needs logic?)

View file

@ -3,7 +3,7 @@
"type": "library", "type": "library",
"description": "A compact, dependency-less Amazon S3 API client implementing the most commonly used features", "description": "A compact, dependency-less Amazon S3 API client implementing the most commonly used features",
"require": { "require": {
"php": ">=7.1.0 <8.1", "php": ">=7.1.0 <8.4",
"ext-curl": "*", "ext-curl": "*",
"ext-simplexml": "*" "ext-simplexml": "*"
}, },
@ -11,7 +11,7 @@
"s3" "s3"
], ],
"homepage": "https://github.com/akeeba/s3", "homepage": "https://github.com/akeeba/s3",
"license": "GPL-3.0+", "license": "GPL-3.0-or-later",
"authors": [ "authors": [
{ {
"name": "Nicholas K. Dionysopoulos", "name": "Nicholas K. Dionysopoulos",
@ -22,7 +22,16 @@
], ],
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Akeeba\\Engine\\Postproc\\Connector\\S3v4\\": "src" "Akeeba\\S3\\": "src"
} },
"files": [
"src/aliasing.php"
]
},
"archive": {
"exclude": [
"minitest",
"TODO.md"
]
} }
} }

View file

@ -1,10 +1,10 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "1070071b351d45a80934e854f0725d64", "content-hash": "27f387a657b2784510b177f73c436346",
"packages": [], "packages": [],
"packages-dev": [], "packages-dev": [],
"aliases": [], "aliases": [],
@ -13,7 +13,10 @@
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=5.3.4" "php": ">=7.1.0 <8.4",
"ext-curl": "*",
"ext-simplexml": "*"
}, },
"platform-dev": [] "platform-dev": [],
"plugin-api-version": "2.3.0"
} }

View file

@ -0,0 +1,62 @@
# Testing notes
## Against Amazon S3 proper
This is the _canonical_ method for testing this library since Amazon S3 proper is the canonical provider of the S3 API (and not all of its quirks are fully documented, we might add).
Copy `config.dist.php` to `config.php` and enter the connection information to your Amazon S3 or compatible service.
## Against [LocalStack](https://localstack.cloud)
This method is very useful for development.
Install LocalStack [as per their documentation](https://docs.localstack.cloud/getting-started/installation/).
You will also need to install [`awslocal`](https://github.com/localstack/awscli-local) like so:
```php
pip install awscli
pip install awscli-local
```
Start LocalStack e.g. `localstack start -d`
Create a new bucket called `test` i.e. `awslocal s3 mk s3://test`
Copy `config.dist.php` to `config.php` and make the following changes:
```php
define('DEFAULT_ENDPOINT', 'localhost.localstack.cloud:4566');
define('DEFAULT_ACCESS_KEY', 'ANYRANDOMSTRINGWILLDO');
define('DEFAULT_SECRET_KEY', 'ThisIsAlwaysIgnoredByLocalStack');
define('DEFAULT_REGION', 'us-east-1');
define('DEFAULT_BUCKET', 'test');
define('DEFAULT_SIGNATURE', 'v4');
define('DEFAULT_PATH_ACCESS', true);
```
Note that single- and dualstack tests result in the same URLs for all S3-compatible services, including LocalStack. These tests are essentially duplicates in this use case.
## Against Wasabi
Wasabi nominally supports v4 signatures, but their implementation is actually _non-canonical_, as they only read the date from the optional `x-amz-date` header, without falling back to the standard HTTP `Date` header. We have added a workaround for this behaviour which necessitates testing with it.
Just like with Amazon S3 proper, copy `config.dist.php` to `config.php` and enter the connection information to your Wasabi storage. You will also need to set up the custom endpoint like so:
```php
define('DEFAULT_ENDPOINT', 's3.eu-central-2.wasabisys.com');
```
**IMPORTANT!** The above endpoint will be different, depending on which region you've created your bucket in. The example above assumes the `eu-central-2` region. If you use the wrong region the tests _will_ fail!
## Against Synology C2
Synology C2 is an S3-“compatible” storage service. It is not very “compatible” though, since they implemented Amazon's documentation of the v4 signatures instead of how the v4 signatures work in the real world (yeah, there's a very big difference). While Amazon S3 _in reality_ expects all dates to be formatted as per RFC1123, they document that they expect them to be formatted as per “ISO 8601” and they give their _completely wrong_ interpretation of what the “ISO 8601” format is. Synology did not catch that discrepancy, and they only expect the wrongly formatted dates which is totally NOT what S3 itself expects. Luckily, most third party implementations expect either format because they've caught the discrepancy between documentation and reality, therefore making it possible for us to come up with a viable workaround.
And that's why we need to test with C2 as well, folks.
Copy `config.dist.php` to `config.php` and enter the connection information to your Synology S3 service.
It is very important to note two things:
```php
define('DEFAULT_ENDPOINT', 'eu-002.s3.synologyc2.net');
define('DEFAULT_REGION', 'eu-002');
```
The endpoint URL is given in the Synology C2 Object Manager, next to each bucket. Note the part before `.s3.`. This is the **region** you need to use with v4 signatures. They do not document this anywhere.

View file

@ -3,13 +3,13 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use RuntimeException; use RuntimeException;
abstract class AbstractTest abstract class AbstractTest
@ -58,24 +58,24 @@ abstract class AbstractTest
*/ */
protected static function createFile(int $size = AbstractTest::SIX_HUNDRED_KB, int $blockSize = self::BLOCK_SIZE, bool $reuseBlock = true) protected static function createFile(int $size = AbstractTest::SIX_HUNDRED_KB, int $blockSize = self::BLOCK_SIZE, bool $reuseBlock = true)
{ {
$tempFilePath = tempnam(self::getTempFolder(), 'as3'); $tempFilePath = tempnam(static::getTempFolder(), 'as3');
if ($tempFilePath === false) if ($tempFilePath === false)
{ {
throw new RuntimeException("Cannot create a temporary file."); throw new RuntimeException("Cannot create a temporary file.");
} }
$fp = @fopen($tempFilePath, 'wb', false); $fp = @fopen($tempFilePath, 'w', false);
if ($fp === false) if ($fp === false)
{ {
throw new RuntimeException("Cannot write to the temporary file."); throw new RuntimeException("Cannot write to the temporary file.");
} }
$blockSize = self::BLOCK_SIZE; $blockSize = static::BLOCK_SIZE;
$lastBlockSize = $size % $blockSize; $lastBlockSize = $size % $blockSize;
$wholeBlocks = (int) (($size - $lastBlockSize) / $blockSize); $wholeBlocks = (int) (($size - $lastBlockSize) / $blockSize);
$blockData = self::getRandomData(); $blockData = static::getRandomData();
for ($i = 0; $i < $wholeBlocks; $i++) for ($i = 0; $i < $wholeBlocks; $i++)
{ {
@ -83,7 +83,7 @@ abstract class AbstractTest
if (!$reuseBlock) if (!$reuseBlock)
{ {
$blockData = self::getRandomData($blockSize); $blockData = static::getRandomData($blockSize);
} }
} }
@ -155,7 +155,7 @@ abstract class AbstractTest
return false; return false;
} }
return hash_file(self::FILE_HASHING_ALGORITHM, $referenceFilePath) === hash_file(self::FILE_HASHING_ALGORITHM, $unknownFilePath); return hash_file(static::FILE_HASHING_ALGORITHM, $referenceFilePath) === hash_file(static::FILE_HASHING_ALGORITHM, $unknownFilePath);
} }
/** /**

View file

@ -3,15 +3,15 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
/** /**
* Upload, download and delete big files (over 1MB), without multipart uploads. Uses string or file sources. * Upload, download and delete big files (over 1MB), without multipart uploads. Uses string or file sources.
@ -51,7 +51,7 @@ class BigFiles extends AbstractTest
/** /**
* Number of uploaded chunks. * Number of uploaded chunks.
* *
* This is set by self::upload(). Zero for single part uploads, non-zero for multipart uploads. * This is set by static::upload(). Zero for single part uploads, non-zero for multipart uploads.
* *
* @var int * @var int
*/ */
@ -59,42 +59,42 @@ class BigFiles extends AbstractTest
public static function upload5MBString(Connector $s3, array $options): bool public static function upload5MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::FIVE_MB, 'bigtest_5mb.dat'); return static::upload($s3, $options, static::FIVE_MB, 'bigtest_5mb.dat');
} }
public static function upload6MBString(Connector $s3, array $options): bool public static function upload6MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::SIX_MB, 'bigtest_6mb.dat'); return static::upload($s3, $options, static::SIX_MB, 'bigtest_6mb.dat');
} }
public static function upload10MBString(Connector $s3, array $options): bool public static function upload10MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_MB, 'bigtest_10mb.dat'); return static::upload($s3, $options, static::TEN_MB, 'bigtest_10mb.dat');
} }
public static function upload11MBString(Connector $s3, array $options): bool public static function upload11MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::ELEVEN_MB, 'bigtest_11mb.dat'); return static::upload($s3, $options, static::ELEVEN_MB, 'bigtest_11mb.dat');
} }
public static function upload5MBFile(Connector $s3, array $options): bool public static function upload5MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::FIVE_MB, 'bigtest_5mb.dat', false); return static::upload($s3, $options, static::FIVE_MB, 'bigtest_5mb.dat', false);
} }
public static function upload6MBFile(Connector $s3, array $options): bool public static function upload6MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::SIX_MB, 'bigtest_6mb.dat', false); return static::upload($s3, $options, static::SIX_MB, 'bigtest_6mb.dat', false);
} }
public static function upload10MBFile(Connector $s3, array $options): bool public static function upload10MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_MB, 'bigtest_10mb.dat', false); return static::upload($s3, $options, static::TEN_MB, 'bigtest_10mb.dat', false);
} }
public static function upload11MBFile(Connector $s3, array $options): bool public static function upload11MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::ELEVEN_MB, 'bigtest_11mb.dat', false); return static::upload($s3, $options, static::ELEVEN_MB, 'bigtest_11mb.dat', false);
} }
protected static function upload(Connector $s3, array $options, int $size, string $uri, bool $useString = true): bool protected static function upload(Connector $s3, array $options, int $size, string $uri, bool $useString = true): bool
@ -103,24 +103,24 @@ class BigFiles extends AbstractTest
$dotPos = strrpos($uri, '.'); $dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
self::$numberOfChunks = 0; static::$numberOfChunks = 0;
if ($useString) if ($useString)
{ {
$sourceData = self::getRandomData($size); $sourceData = static::getRandomData($size);
$input = Input::createFromData($sourceData); $input = Input::createFromData($sourceData);
} }
else else
{ {
// Create a file with random data // Create a file with random data
$sourceFile = self::createFile($size); $sourceFile = static::createFile($size);
$input = Input::createFromFile($sourceFile); $input = Input::createFromFile($sourceFile);
} }
// Upload the file. Throws exception if it fails. // Upload the file. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
if (!self::$multipart) if (!static::$multipart)
{ {
$s3->putObject($input, $bucket, $uri); $s3->putObject($input, $bucket, $uri);
} }
@ -149,7 +149,7 @@ class BigFiles extends AbstractTest
$input->setEtags($eTags); $input->setEtags($eTags);
$input->setPartNumber($partNumber); $input->setPartNumber($partNumber);
$etag = $s3->uploadMultipart($input, $bucket, $uri, [], self::$uploadChunkSize); $etag = $s3->uploadMultipart($input, $bucket, $uri, [], static::$uploadChunkSize);
// If the result was null we have no more file parts to process. // If the result was null we have no more file parts to process.
if (is_null($etag)) if (is_null($etag))
@ -166,7 +166,7 @@ class BigFiles extends AbstractTest
$partNumber++; $partNumber++;
} }
self::$numberOfChunks = count($eTags); static::$numberOfChunks = count($eTags);
// Finalize the multipart upload. Tells Amazon to construct the file from the uploaded parts. // Finalize the multipart upload. Tells Amazon to construct the file from the uploaded parts.
$s3->finalizeMultipart($input, $bucket, $uri); $s3->finalizeMultipart($input, $bucket, $uri);
@ -176,7 +176,7 @@ class BigFiles extends AbstractTest
$result = true; $result = true;
// Should I download the file and compare its contents? // Should I download the file and compare its contents?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
if ($useString) if ($useString)
{ {
@ -184,16 +184,16 @@ class BigFiles extends AbstractTest
$downloadedData = $s3->getObject($bucket, $uri); $downloadedData = $s3->getObject($bucket, $uri);
// Compare the file contents. // Compare the file contents.
$result = self::areStringsEqual($sourceData, $downloadedData); $result = static::areStringsEqual($sourceData, $downloadedData);
} }
else else
{ {
// Download the data. Throws exception if it fails. // Download the data. Throws exception if it fails.
$downloadedFile = tempnam(self::getTempFolder(), 'as3'); $downloadedFile = tempnam(static::getTempFolder(), 'as3');
$s3->getObject($bucket, $uri, $downloadedFile); $s3->getObject($bucket, $uri, $downloadedFile);
// Compare the file contents. // Compare the file contents.
$result = self::areFilesEqual($sourceFile, $downloadedFile); $result = static::areFilesEqual($sourceFile, $downloadedFile);
@unlink($downloadedFile); @unlink($downloadedFile);
} }
@ -206,7 +206,7 @@ class BigFiles extends AbstractTest
} }
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
class BucketLocation extends AbstractTest class BucketLocation extends AbstractTest
{ {
@ -18,7 +18,7 @@ class BucketLocation extends AbstractTest
{ {
$location = $s3->getBucketLocation($options['bucket']); $location = $s3->getBucketLocation($options['bucket']);
self::assert($location === $options['region'], "Bucket {$options['bucket']} reports being in region {$location} instead of expected {$options['region']}"); static::assert($location === $options['region'], "Bucket {$options['bucket']} reports being in region {$location} instead of expected {$options['region']}");
return true; return true;
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use RuntimeException; use RuntimeException;
class BucketsList extends AbstractTest class BucketsList extends AbstractTest
@ -19,16 +19,16 @@ class BucketsList extends AbstractTest
{ {
$buckets = $s3->listBuckets(true); $buckets = $s3->listBuckets(true);
self::assert(is_array($buckets), "Detailed buckets list is not an array"); static::assert(is_array($buckets), "Detailed buckets list is not an array");
self::assert(isset($buckets['owner']), "Detailed buckets list does not list an owner"); static::assert(isset($buckets['owner']), "Detailed buckets list does not list an owner");
self::assert(isset($buckets['owner']['id']), "Detailed buckets list does not list an owner's id"); static::assert(isset($buckets['owner']['id']), "Detailed buckets list does not list an owner's id");
self::assert(isset($buckets['owner']['name']), "Detailed buckets list does not list an owner's name"); static::assert(isset($buckets['owner']['name']), "Detailed buckets list does not list an owner's name");
self::assert(isset($buckets['buckets']), "Detailed buckets list does not list any buckets"); static::assert(isset($buckets['buckets']), "Detailed buckets list does not list any buckets");
foreach ($buckets['buckets'] as $bucketInfo) foreach ($buckets['buckets'] as $bucketInfo)
{ {
self::assert(isset($bucketInfo['name']), "Bucket information does not list a name"); static::assert(isset($bucketInfo['name']), "Bucket information does not list a name");
self::assert(isset($bucketInfo['time']), "Bucket information does not list a created times"); static::assert(isset($bucketInfo['time']), "Bucket information does not list a created times");
if ($bucketInfo['name'] === $options['bucket']) if ($bucketInfo['name'] === $options['bucket'])
{ {
@ -43,8 +43,8 @@ class BucketsList extends AbstractTest
{ {
$buckets = $s3->listBuckets(false); $buckets = $s3->listBuckets(false);
self::assert(is_array($buckets), "Simple buckets list is not an array"); static::assert(is_array($buckets), "Simple buckets list is not an array");
self::assert(in_array($options['bucket'], $buckets), "Simple buckets list does not include configured bucket {$options['bucket']}"); static::assert(in_array($options['bucket'], $buckets), "Simple buckets list does not include configured bucket {$options['bucket']}");
return true; return true;
} }

View file

@ -0,0 +1,67 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Exception\CannotDeleteFile;
use Akeeba\S3\Exception\CannotGetFile;
use Akeeba\S3\Input;
class HeadObject extends AbstractTest
{
public static function testExistingFile(Connector $s3, array $options): bool
{
$uri = 'head_test.dat';
// Randomize the name. Required for archive buckets where you cannot overwrite data.
$dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create a file with random data
$sourceFile = static::createFile(AbstractTest::TEN_KB);
// Upload the file. Throws exception if it fails.
$bucket = $options['bucket'];
$input = Input::createFromFile($sourceFile);
$s3->putObject($input, $bucket, $uri);
$headers = $s3->headObject($bucket, $uri);
static::assert(isset($headers['size']), 'The returned headers do not contain the object size');
static::assert($headers['size'] == AbstractTest::TEN_KB, 'The returned size does not match');
// Remove the local files
@unlink($sourceFile);
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);
return true;
}
public static function testMissingFile(Connector $s3, array $options): bool
{
$bucket = $options['bucket'];
try
{
$headers = $s3->headObject($bucket, md5(microtime(false)) . '_does_not_exist');
}
catch (CannotGetFile $e)
{
return true;
}
return false;
}
}

View file

@ -3,16 +3,16 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile; use Akeeba\S3\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
class ListFiles extends AbstractTest class ListFiles extends AbstractTest
{ {
@ -34,9 +34,9 @@ class ListFiles extends AbstractTest
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
$data = self::getRandomData(self::TEN_KB); $data = static::getRandomData(static::TEN_KB);
foreach (self::$paths as $uri) foreach (static::$paths as $uri)
{ {
$input = Input::createFromData($data); $input = Input::createFromData($data);
try try
@ -52,7 +52,7 @@ class ListFiles extends AbstractTest
public static function teardown(Connector $s3, array $options): void public static function teardown(Connector $s3, array $options): void
{ {
foreach (self::$paths as $uri) foreach (static::$paths as $uri)
{ {
try try
{ {
@ -69,29 +69,29 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'listtest_'); $listing = $s3->getBucket($options['bucket'], 'listtest_');
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing"); static::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing");
self::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing"); static::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing");
self::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing"); static::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing");
// I must not see the files not matching the prefix I gave // I must not see the files not matching the prefix I gave
self::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing"); static::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -101,37 +101,37 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'listtest_', null, 1); $listing = $s3->getBucket($options['bucket'], 'listtest_', null, 1);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing))); static::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing)));
$files = array_keys($listing); $files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'listtest_', array_shift($files)); $continued = $s3->getBucket($options['bucket'], 'listtest_', array_shift($files));
self::assert(is_array($continued), "The continued files listing must be an array"); static::assert(is_array($continued), "The continued files listing must be an array");
self::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued))); static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued); $listing = array_merge($listing, $continued);
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing"); static::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing");
self::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing"); static::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing");
self::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing"); static::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing");
// I must not see the files not matching the prefix I gave // I must not see the files not matching the prefix I gave
self::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing"); static::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -141,30 +141,30 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'list_deeper/test_'); $listing = $s3->getBucket($options['bucket'], 'list_deeper/test_');
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing"); static::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing");
self::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing"); static::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing");
self::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing"); static::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -174,41 +174,41 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'list_deeper/test_', null, 1); $listing = $s3->getBucket($options['bucket'], 'list_deeper/test_', null, 1);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing))); static::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing)));
$files = array_keys($listing); $files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'list_deeper/test_', array_shift($files)); $continued = $s3->getBucket($options['bucket'], 'list_deeper/test_', array_shift($files));
self::assert(is_array($continued), "The continued files listing must be an array"); static::assert(is_array($continued), "The continued files listing must be an array");
self::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued))); static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued); $listing = array_merge($listing, $continued);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing"); static::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing");
self::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing"); static::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing");
self::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing"); static::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -224,42 +224,42 @@ class ListFiles extends AbstractTest
*/ */
$listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, 1); $listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, 1);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 1, sprintf("I am expecting to see 1 files, %s seen", count($listing))); static::assert(count($listing) == 1, sprintf("I am expecting to see 1 files, %s seen", count($listing)));
$files = array_keys($listing); $files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', array_shift($files)); $continued = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', array_shift($files));
self::assert(is_array($continued), "The continued files listing must be an array"); static::assert(is_array($continued), "The continued files listing must be an array");
self::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued))); static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued); $listing = array_merge($listing, $continued);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat in listing"); static::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat in listing");
self::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat in listing"); static::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat in listing");
self::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat in listing"); static::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat in listing");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -269,37 +269,37 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, null, '/', true); $listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, null, '/', true);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 4, sprintf("I am expecting to see 4 entries, %s entries seen.", count($listing))); static::assert(count($listing) == 4, sprintf("I am expecting to see 4 entries, %s entries seen.", count($listing)));
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_deeper/', $listing), "Folder listtest_deeper not in listing"); static::assert(array_key_exists('list_deeper/listtest_deeper/', $listing), "Folder listtest_deeper not in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File seven.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File seven.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File eight.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File eight.dat in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing"); static::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing");
self::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing"); static::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing");
self::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing"); static::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
if (substr($fileName, -1) !== '/') if (substr($fileName, -1) !== '/')
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
else else
{ {
self::assert(isset($info['prefix']), "Folder entries must return a prefix"); static::assert(isset($info['prefix']), "Folder entries must return a prefix");
} }
} }

View file

@ -0,0 +1,111 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
class ListThousandsOfFiles extends AbstractTest
{
private const PATH_PREFIX = 'massive/';
public static function setup(Connector $s3, array $options): void
{
if (defined('CREATE_2100_FILES') && CREATE_2100_FILES === false)
{
return;
}
$data = static::getRandomData(128);
echo "\nPopulating with 2100 files\n";
for ($i = 1; $i <= 2100; $i++)
{
if ($i % 10 === 0)
{
echo "Uploading from $i...\n";
}
$uri = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
$input = Input::createFromData($data);
$s3->putObject($input, $options['bucket'], $uri);
}
}
public static function testGetAll(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 2100, "I am expecting to see 2100 files");
for ($i = 1; $i <= 2100; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
public static function testGetHundred(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX, null, 100);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 100, "I am expecting to see 100 files");
for ($i = 1; $i <= 100; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
public static function testGetElevenHundred(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX, null, 1100);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 1100, "I am expecting to see 1100 files");
for ($i = 1; $i <= 1100; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
public static function testGetLastHundred(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX . 'test_20', null);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 100, "I am expecting to see 100 files");
for ($i = 2000; $i <= 2099; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
}

View file

@ -1,16 +1,22 @@
<?php <?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
class Multipart extends BigFiles class Multipart extends BigFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$multipart = true; static::$multipart = true;
parent::setup($s3, $options); parent::setup($s3, $options);
} }
@ -20,7 +26,7 @@ class Multipart extends BigFiles
$result = parent::upload5MBString($s3, $options); $result = parent::upload5MBString($s3, $options);
$expectedChunks = 1; $expectedChunks = 1;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -30,7 +36,7 @@ class Multipart extends BigFiles
$result = parent::upload6MBString($s3, $options); $result = parent::upload6MBString($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -40,7 +46,7 @@ class Multipart extends BigFiles
$result = parent::upload10MBString($s3, $options); $result = parent::upload10MBString($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -50,7 +56,7 @@ class Multipart extends BigFiles
$result = parent::upload11MBString($s3, $options); $result = parent::upload11MBString($s3, $options);
$expectedChunks = 3; $expectedChunks = 3;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -60,7 +66,7 @@ class Multipart extends BigFiles
$result = parent::upload5MBFile($s3, $options); $result = parent::upload5MBFile($s3, $options);
$expectedChunks = 1; $expectedChunks = 1;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -70,7 +76,7 @@ class Multipart extends BigFiles
$result = parent::upload6MBFile($s3, $options); $result = parent::upload6MBFile($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -80,7 +86,7 @@ class Multipart extends BigFiles
$result = parent::upload10MBFile($s3, $options); $result = parent::upload10MBFile($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -90,7 +96,7 @@ class Multipart extends BigFiles
$result = parent::upload11MBFile($s3, $options); $result = parent::upload11MBFile($s3, $options);
$expectedChunks = 3; $expectedChunks = 3;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }

View file

@ -3,33 +3,33 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Acl; use Akeeba\S3\Acl;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
use RuntimeException; use RuntimeException;
class SignedURLs extends AbstractTest class SignedURLs extends AbstractTest
{ {
public static function signedURLPublicObject(Connector $s3, array $options): bool public static function signedURLPublicObject(Connector $s3, array $options): bool
{ {
return self::signedURL($s3, $options, Acl::ACL_PUBLIC_READ); return static::signedURL($s3, $options, Acl::ACL_PUBLIC_READ);
} }
public static function signedURLPrivateObject(Connector $s3, array $options): bool public static function signedURLPrivateObject(Connector $s3, array $options): bool
{ {
return self::signedURL($s3, $options, Acl::ACL_PRIVATE); return static::signedURL($s3, $options, Acl::ACL_PRIVATE);
} }
private static function signedURL(Connector $s3, array $options, string $aclPrivilege): bool private static function signedURL(Connector $s3, array $options, string $aclPrivilege): bool
{ {
$tempData = self::getRandomData(AbstractTest::TEN_KB); $tempData = static::getRandomData(AbstractTest::TEN_KB);
$input = Input::createFromData($tempData); $input = Input::createFromData($tempData);
$uri = 'test.' . md5(microtime(false)) . '.dat'; $uri = 'test.' . md5(microtime(false)) . '.dat';
@ -52,7 +52,7 @@ class SignedURLs extends AbstractTest
throw new RuntimeException("Failed to download from signed URL {$downloadURL}"); throw new RuntimeException("Failed to download from signed URL {$downloadURL}");
} }
self::assert(self::areStringsEqual($tempData, $downloadedData), "Wrong data received from signed URL {$downloadURL}"); static::assert(static::areStringsEqual($tempData, $downloadedData), "Wrong data received from signed URL {$downloadURL}");
return true; return true;
} }

View file

@ -0,0 +1,43 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* Upload, download and delete small files (under 1MB) using a string source
*
* @package Akeeba\MiniTest\Test
*/
class SingleSmallFile extends AbstractTest
{
public static function upload(Connector $s3, array $options): bool
{
$uri = 'test.txt';
$sourceData = <<< TEXT
This is a small text file.
TEXT;
// Upload the data. Throws exception if it fails.
$bucket = $options['bucket'];
$input = Input::createFromData($sourceData);
$s3->putObject($input, $bucket, $uri);
$downloadedData = $s3->getObject($bucket, $uri);
$result = static::areStringsEqual($sourceData, $downloadedData);
$s3->deleteObject($bucket, $uri);
return $result ?? true;
}
}

View file

@ -3,15 +3,15 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
/** /**
* Upload, download and delete small files (under 1MB) using a file source * Upload, download and delete small files (under 1MB) using a file source
@ -36,32 +36,32 @@ class SmallFiles extends AbstractTest
public static function upload10KbRoot(Connector $s3, array $options): bool public static function upload10KbRoot(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, 'root_10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, 'root_10kb.dat');
} }
public static function upload10KbRootGreek(Connector $s3, array $options): bool public static function upload10KbRootGreek(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, οκιμή_10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, οκιμή_10kb.dat');
} }
public static function upload10KbFolderGreek(Connector $s3, array $options): bool public static function upload10KbFolderGreek(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, 'ο_φάκελός_μουοκιμή_10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, 'ο_φάκελός_μουοκιμή_10kb.dat');
} }
public static function upload600KbRoot(Connector $s3, array $options): bool public static function upload600KbRoot(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'root_600kb.dat'); return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'root_600kb.dat');
} }
public static function upload10KbFolder(Connector $s3, array $options): bool public static function upload10KbFolder(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, 'my_folder/10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, 'my_folder/10kb.dat');
} }
public static function upload600KbFolder(Connector $s3, array $options): bool public static function upload600KbFolder(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'my_folder/600kb.dat'); return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'my_folder/600kb.dat');
} }
protected static function upload(Connector $s3, array $options, int $size, string $uri): bool protected static function upload(Connector $s3, array $options, int $size, string $uri): bool
@ -71,7 +71,7 @@ class SmallFiles extends AbstractTest
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create a file with random data // Create a file with random data
$sourceFile = self::createFile($size); $sourceFile = static::createFile($size);
// Upload the file. Throws exception if it fails. // Upload the file. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
@ -83,14 +83,14 @@ class SmallFiles extends AbstractTest
$result = true; $result = true;
// Should I download the file and compare its contents? // Should I download the file and compare its contents?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
// Donwload the data. Throws exception if it fails. // Donwload the data. Throws exception if it fails.
$downloadedFile = tempnam(self::getTempFolder(), 'as3'); $downloadedFile = tempnam(static::getTempFolder(), 'as3');
$s3->getObject($bucket, $uri, $downloadedFile); $s3->getObject($bucket, $uri, $downloadedFile);
// Compare the file contents. // Compare the file contents.
$result = self::areFilesEqual($sourceFile, $downloadedFile); $result = static::areFilesEqual($sourceFile, $downloadedFile);
} }
// Remove the local files // Remove the local files
@ -98,7 +98,7 @@ class SmallFiles extends AbstractTest
@unlink($downloadedFile); @unlink($downloadedFile);
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload and download small files (under 1MB) using a file source * Upload and download small files (under 1MB) using a file source
@ -21,7 +21,7 @@ class SmallFilesNoDelete extends SmallFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$deleteRemote = false; static::$deleteRemote = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload small files (under 1MB) using a file source * Upload small files (under 1MB) using a file source
@ -21,8 +21,8 @@ class SmallFilesOnlyUpload extends SmallFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$deleteRemote = false; static::$deleteRemote = false;
self::$downloadAfter = false; static::$downloadAfter = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -3,15 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Input;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
/** /**
* Upload, download and delete small files (under 1MB) using a string source * Upload, download and delete small files (under 1MB) using a string source
@ -27,7 +26,7 @@ class SmallInlineFiles extends SmallFiles
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload // Create some random data to upload
$sourceData = self::getRandomData($size); $sourceData = static::getRandomData($size);
// Upload the data. Throws exception if it fails. // Upload the data. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
@ -39,15 +38,15 @@ class SmallInlineFiles extends SmallFiles
$result = true; $result = true;
// Should I download the file and compare its contents with my random data? // Should I download the file and compare its contents with my random data?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
$downloadedData = $s3->getObject($bucket, $uri); $downloadedData = $s3->getObject($bucket, $uri);
$result = self::areStringsEqual($sourceData, $downloadedData); $result = static::areStringsEqual($sourceData, $downloadedData);
} }
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload and download small files (under 1MB) using a string source * Upload and download small files (under 1MB) using a string source
@ -21,7 +21,7 @@ class SmallInlineFilesNoDelete extends SmallInlineFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self:: $deleteRemote = false; static:: $deleteRemote = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload small files (under 1MB) using a string source * Upload small files (under 1MB) using a string source
@ -21,8 +21,8 @@ class SmallInlineFilesOnlyUpload extends SmallInlineFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$deleteRemote = false; static::$deleteRemote = false;
self::$downloadAfter = false; static::$downloadAfter = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -0,0 +1,131 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* Upload, download and delete small XML files (under 1MB) using a string source
*
* @package Akeeba\MiniTest\Test
*/
class SmallInlineXMLFiles extends SmallFiles
{
public static function upload10KbRoot(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, 'root_10kb.xml');
}
public static function upload10KbRootGreek(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, οκιμή_10kb.xml');
}
public static function upload10KbFolderGreek(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, 'ο_φάκελός_μουοκιμή_10kb.xml');
}
public static function upload600KbRoot(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'root_600kb.xml');
}
public static function upload10KbFolder(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, 'my_folder/10kb.xml');
}
public static function upload600KbFolder(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'my_folder/600kb.xml');
}
protected static function upload(Connector $s3, array $options, int $size, string $uri): bool
{
// Randomize the name. Required for archive buckets where you cannot overwrite data.
$dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload
$sourceData = static::createXMLFile($size);
// Upload the data. Throws exception if it fails.
$bucket = $options['bucket'];
$input = Input::createFromData($sourceData);
$s3->putObject($input, $bucket, $uri);
// Tentatively accept that this method succeeded.
$result = true;
// Should I download the file and compare its contents with my random data?
if (static::$downloadAfter)
{
$downloadedData = $s3->getObject($bucket, $uri);
$result = static::areStringsEqual($sourceData, $downloadedData);
}
// Should I delete the remotely stored file?
if (static::$deleteRemote)
{
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);
}
return $result;
}
private static function createXMLFile(int $size): string
{
$out = <<< XML
<?xml version="1.0" encoding="utf-8" ?>
<root>
XML;
$chunks = floor(($size - 55) / 1024);
for ($i = 1; $i <= $chunks; $i++)
{
$randomBlock = static::genRandomData(1024 - 63);
$out .= <<< XML
<element>
<id>$i</id>
<data><![CDATA[$randomBlock]]></data>
</element>
XML;
}
$out .= <<< XML
</root>
XML;
return $out;
}
private static function genRandomData(int $length): string
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890';
$maxLength = strlen($chars) - 1;
$salt = '';
for ($i = 0; $i < $length; $i++)
{
$salt .= substr($chars, random_int(0, $maxLength), 1);
}
return $salt;
}
}

View file

@ -3,17 +3,17 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Acl; use Akeeba\S3\Acl;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
use Akeeba\Engine\Postproc\Connector\S3v4\StorageClass; use Akeeba\S3\StorageClass;
class StorageClasses extends AbstractTest class StorageClasses extends AbstractTest
{ {
@ -23,12 +23,12 @@ class StorageClasses extends AbstractTest
public static function uploadRRS(Connector $s3, array $options): bool public static function uploadRRS(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_KB, 'rrs_test_10kb.dat', StorageClass::REDUCED_REDUNDANCY); return static::upload($s3, $options, static::TEN_KB, 'rrs_test_10kb.dat', StorageClass::REDUCED_REDUNDANCY);
} }
public static function uploadIntelligentTiering(Connector $s3, array $options): bool public static function uploadIntelligentTiering(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_KB, 'rrs_test_10kb.dat', StorageClass::INTELLIGENT_TIERING); return static::upload($s3, $options, static::TEN_KB, 'rrs_test_10kb.dat', StorageClass::INTELLIGENT_TIERING);
} }
protected static function upload(Connector $s3, array $options, int $size, string $uri, string $storageClass = null) protected static function upload(Connector $s3, array $options, int $size, string $uri, string $storageClass = null)
@ -38,7 +38,7 @@ class StorageClasses extends AbstractTest
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload // Create some random data to upload
$sourceData = self::getRandomData($size); $sourceData = static::getRandomData($size);
// Upload the data. Throws exception if it fails. // Upload the data. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
@ -54,15 +54,15 @@ class StorageClasses extends AbstractTest
$result = true; $result = true;
// Should I download the file and compare its contents with my random data? // Should I download the file and compare its contents with my random data?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
$downloadedData = $s3->getObject($bucket, $uri); $downloadedData = $s3->getObject($bucket, $uri);
$result = self::areStringsEqual($sourceData, $downloadedData); $result = static::areStringsEqual($sourceData, $downloadedData);
} }
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,10 +3,12 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
// Custom Endpoint. The example below is for using LocalStack, see https://localstack.cloud/
// define('DEFAULT_ENDPOINT', 'localhost.localstack.cloud:4566');
// Default Amazon S3 Access Key // Default Amazon S3 Access Key
define('DEFAULT_ACCESS_KEY', 'your s3 access key'); define('DEFAULT_ACCESS_KEY', 'your s3 access key');
// Default Amazon S3 Secret Key // Default Amazon S3 Secret Key
@ -23,6 +25,8 @@ define('DEFAULT_DUALSTACK', false);
define('DEFAULT_PATH_ACCESS', false); define('DEFAULT_PATH_ACCESS', false);
// Should I use SSL by default? // Should I use SSL by default?
define('DEFAULT_SSL', true); define('DEFAULT_SSL', true);
// Create the 2100 test files in the bucket?
define('CREATE_2100_FILES', true);
/** /**
* Tests for standard key pairs allowing us to read, write and delete * Tests for standard key pairs allowing us to read, write and delete
@ -33,7 +37,9 @@ $standardTests = [
'BucketsList', 'BucketsList',
'BucketLocation', 'BucketLocation',
'SmallFiles', 'SmallFiles',
'HeadObject',
'SmallInlineFiles', 'SmallInlineFiles',
'SmallInlineXMLFiles',
'SignedURLs', 'SignedURLs',
'StorageClasses', 'StorageClasses',
'ListFiles', 'ListFiles',

View file

@ -3,13 +3,13 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
use Akeeba\Engine\Postproc\Connector\S3v4\Configuration; use Akeeba\S3\Configuration;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
// Necessary for including the library // Necessary for including the library
define('AKEEBAENGINE', 1); define('AKEEBAENGINE', 1);
@ -167,7 +167,7 @@ foreach ($testConfigurations as $description => $setup)
'dualstack' => DEFAULT_DUALSTACK, 'dualstack' => DEFAULT_DUALSTACK,
'path_access' => DEFAULT_PATH_ACCESS, 'path_access' => DEFAULT_PATH_ACCESS,
'ssl' => DEFAULT_SSL, 'ssl' => DEFAULT_SSL,
'endpoint' => null, 'endpoint' => defined('DEFAULT_ENDPOINT') ? constant('DEFAULT_ENDPOINT') : null,
], $setup['configuration']); ], $setup['configuration']);
// Extract the test classes/methods to run // Extract the test classes/methods to run
@ -185,15 +185,21 @@ foreach ($testConfigurations as $description => $setup)
// Create the S3 configuration object // Create the S3 configuration object
$s3Configuration = new Configuration($configOptions['access'], $configOptions['secret'], $configOptions['signature'], $configOptions['region']); $s3Configuration = new Configuration($configOptions['access'], $configOptions['secret'], $configOptions['signature'], $configOptions['region']);
$s3Configuration->setUseDualstackUrl($configOptions['dualstack']); $s3Configuration->setRegion($configOptions['region']);
$s3Configuration->setUseLegacyPathStyle($configOptions['path_access']); $s3Configuration->setSignatureMethod($configOptions['signature']);
$s3Configuration->setSSL($configOptions['ssl']);
if (!is_null($configOptions['endpoint'])) if (!is_null($configOptions['endpoint']))
{ {
$s3Configuration->setEndpoint($configOptions['endpoint']); $s3Configuration->setEndpoint($configOptions['endpoint']);
// We need to redo this because setting the endpoint may reset these options
$s3Configuration->setRegion($configOptions['region']);
$s3Configuration->setSignatureMethod($configOptions['signature']);
} }
$s3Configuration->setUseDualstackUrl($configOptions['dualstack']);
$s3Configuration->setUseLegacyPathStyle($configOptions['path_access']);
$s3Configuration->setSSL($configOptions['ssl']);
// Create the connector object // Create the connector object
$s3Connector = new Connector($s3Configuration); $s3Connector = new Connector($s3Configuration);

View file

@ -3,29 +3,29 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Shortcuts to often used access control privileges * Shortcuts to often used access control privileges
*/ */
class Acl class Acl
{ {
const ACL_PRIVATE = 'private'; public const ACL_PRIVATE = 'private';
const ACL_PUBLIC_READ = 'public-read'; public const ACL_PUBLIC_READ = 'public-read';
const ACL_PUBLIC_READ_WRITE = 'public-read-write'; public const ACL_PUBLIC_READ_WRITE = 'public-read-write';
const ACL_AUTHENTICATED_READ = 'authenticated-read'; public const ACL_AUTHENTICATED_READ = 'authenticated-read';
const ACL_BUCKET_OWNER_READ = 'bucket-owner-read'; public const ACL_BUCKET_OWNER_READ = 'bucket-owner-read';
const ACL_BUCKET_OWNER_FULL_CONTROL = 'bucket-owner-full-control'; public const ACL_BUCKET_OWNER_FULL_CONTROL = 'bucket-owner-full-control';
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Holds the Amazon S3 confiugration credentials * Holds the Amazon S3 confiugration credentials
@ -199,6 +199,8 @@ class Configuration
throw new Exception\InvalidSignatureMethod; throw new Exception\InvalidSignatureMethod;
} }
$this->signatureMethod = $signatureMethod;
// If you switch to v2 signatures we unset the region. // If you switch to v2 signatures we unset the region.
if ($signatureMethod == 'v2') if ($signatureMethod == 'v2')
{ {
@ -214,15 +216,9 @@ class Configuration
$this->setUseLegacyPathStyle(false); $this->setUseLegacyPathStyle(false);
} }
} else {
if (empty($this->getRegion())) {
$this->setRegion('us-east-1');
} }
} }
$this->signatureMethod = $signatureMethod;
}
/** /**
* Get the Amazon S3 region * Get the Amazon S3 region
* *

View file

@ -3,22 +3,22 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotDeleteFile; use Akeeba\S3\Exception\CannotDeleteFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetBucket; use Akeeba\S3\Exception\CannotGetBucket;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetFile; use Akeeba\S3\Exception\CannotGetFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotListBuckets; use Akeeba\S3\Exception\CannotListBuckets;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotOpenFileForWrite; use Akeeba\S3\Exception\CannotOpenFileForWrite;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile; use Akeeba\S3\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error; use Akeeba\S3\Response\Error;
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
class Connector class Connector
{ {
@ -80,9 +80,12 @@ class Connector
} }
if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference())))) if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference()))))
{
if (substr($uri, -1) !== '/')
{ {
throw new CannotPutFile('Missing input parameters', 0); throw new CannotPutFile('Missing input parameters', 0);
} }
}
// We need to post with Content-Length and Content-Type, MD5 is optional // We need to post with Content-Length and Content-Type, MD5 is optional
$request->setHeader('Content-Type', $input->getType()); $request->setHeader('Content-Type', $input->getType());
@ -169,7 +172,7 @@ class Connector
if (!is_resource($saveTo) && is_string($saveTo)) if (!is_resource($saveTo) && is_string($saveTo))
{ {
$fp = @fopen($saveTo, 'wb'); $fp = @fopen($saveTo, 'w');
if ($fp === false) if ($fp === false)
{ {
@ -193,6 +196,53 @@ class Connector
$request->setHeader('Range', "bytes=$from-$to"); $request->setHeader('Range', "bytes=$from-$to");
} }
$response = $request->getResponse(true);
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetFile(
sprintf(
__METHOD__ . "({%s}, {%s}): [%s] %s\n\nDebug info:\n%s",
$bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage(),
print_r($response->body, true)
)
);
}
if (!is_resource($fp))
{
return $response->body;
}
return null;
}
/**
* Get information about an object.
*
* @param string $bucket Bucket name
* @param string $uri Object URI
*
* @return array The headers returned by Amazon S3
*
* @throws CannotGetFile If the file does not exist
* @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
*/
public function headObject(string $bucket, string $uri): array
{
$request = new Request('HEAD', $bucket, $uri, $this->configuration);
$response = $request->getResponse(); $response = $request->getResponse();
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206))) if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
@ -206,19 +256,20 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotGetFile( throw new CannotGetFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s\n\nDebug info:\n%s", sprintf(
$response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)), __METHOD__ . "({%s}, {%s}): [%s] %s\n\nDebug info:\n%s",
$response->error->getCode() $bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage(),
print_r($response->body, true)
)
); );
} }
if (!is_resource($fp)) return $response->getHeaders();
{
return $response->body;
} }
return null;
}
/** /**
* Delete an object * Delete an object
@ -244,9 +295,13 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotDeleteFile( throw new CannotDeleteFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s", sprintf(
$response->error->getCode(), $response->error->getMessage()), __METHOD__ . "({%s}, {%s}): [%s] %s",
$response->error->getCode() $bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage()
)
); );
} }
} }
@ -358,8 +413,7 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotGetBucket( throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()), sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
$response->error->getCode()
); );
} }
@ -403,168 +457,47 @@ class Connector
*/ */
public function getBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array public function getBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array
{ {
$request = new Request('GET', $bucket, '', $this->configuration); $internalResult = $this->internalGetBucket($bucket, $prefix, $marker, $maxKeys, $delimiter, $returnCommonPrefixes);
if (!empty($prefix)) /**
{ * @var array $objects
$request->setParameter('prefix', $prefix); * @var ?string $nextMarker
} */
extract($internalResult);
unset($internalResult);
if (!empty($marker)) // Loop through truncated results if maxKeys isn't specified or we don't have enough object records yet.
{ if ($nextMarker !== null && ($maxKeys === null || count($objects) < $maxKeys))
$request->setParameter('marker', $marker);
}
if (!empty($maxKeys))
{
$request->setParameter('max-keys', $maxKeys);
}
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
$response = $request->getResponse();
if (!$response->error->isError() && $response->code !== 200)
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
);
}
$results = [];
$nextMarker = null;
if ($response->hasBody() && isset($response->body->Contents))
{
foreach ($response->body->Contents as $c)
{
$results[(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$nextMarker = (string) $c->Key;
}
}
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
{
foreach ($response->body->CommonPrefixes as $c)
{
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
}
if ($response->hasBody() && isset($response->body->IsTruncated) &&
((string) $response->body->IsTruncated == 'false')
)
{
return $results;
}
if ($response->hasBody() && isset($response->body->NextMarker))
{
$nextMarker = (string) $response->body->NextMarker;
}
// Is it a truncated result?
$isTruncated = ($nextMarker !== null) && ((string) $response->body->IsTruncated == 'true');
// Is this a truncated result and no maxKeys specified?
$isTruncatedAndNoMaxKeys = ($maxKeys == null) && $isTruncated;
// Is this a truncated result with less keys than the specified maxKeys; and common prefixes found but not returned to the caller?
$isTruncatedAndNeedsContinue = ($maxKeys != null) && $isTruncated && (count($results) < $maxKeys);
// Loop through truncated results if maxKeys isn't specified
if ($isTruncatedAndNoMaxKeys || $isTruncatedAndNeedsContinue)
{ {
do do
{ {
$request = new Request('GET', $bucket, '', $this->configuration); $internalResult = $this->internalGetBucket($bucket, $prefix, $nextMarker, $maxKeys, $delimiter, $returnCommonPrefixes);
if (!empty($prefix)) $nextMarker = $internalResult['nextMarker'];
{ $objects = array_merge($objects, $internalResult['objects']);
$request->setParameter('prefix', $prefix);
}
$request->setParameter('marker', $nextMarker); unset($internalResult);
if (!empty($delimiter)) // If the last call did not return a nextMarker I am done iterating.
{ if ($nextMarker === null)
$request->setParameter('delimiter', $delimiter);
}
try
{
$response = $request->getResponse();
}
catch (\Exception $e)
{ {
break; break;
} }
if ($response->hasBody() && isset($response->body->Contents)) // If we have maxKeys AND the number of objects is at least this many I am done iterating.
if ($maxKeys !== null && count($objects) >= $maxKeys)
{ {
foreach ($response->body->Contents as $c) break;
{
$results[(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$nextMarker = (string) $c->Key;
} }
} while (true);
} }
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes)) if ($maxKeys !== null)
{ {
foreach ($response->body->CommonPrefixes as $c) return array_splice($objects, 0, $maxKeys);
{
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
} }
if ($response->hasBody() && isset($response->body->NextMarker)) return $objects;
{
$nextMarker = (string) $response->body->NextMarker;
}
$continueCondition = false;
if ($isTruncatedAndNoMaxKeys)
{
$continueCondition = !$response->error->isError() && $isTruncated;
}
if ($isTruncatedAndNeedsContinue)
{
$continueCondition = !$response->error->isError() && $isTruncated && (count($results) < $maxKeys);
}
} while ($continueCondition);
}
if (!is_null($maxKeys))
{
$results = array_splice($results, 0, $maxKeys);
}
return $results;
} }
/** /**
@ -594,8 +527,7 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotListBuckets( throw new CannotListBuckets(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()), sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
$response->error->getCode()
); );
} }
@ -691,7 +623,12 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotPutFile( throw new CannotPutFile(
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)) sprintf(
__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s",
$response->error->getCode(),
$response->error->getMessage(),
print_r($response->body, true)
)
); );
} }
@ -958,4 +895,90 @@ class Connector
{ {
return $this->configuration; return $this->configuration;
} }
private function internalGetBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array
{
$request = new Request('GET', $bucket, '', $this->configuration);
if (!empty($prefix))
{
$request->setParameter('prefix', $prefix);
}
if (!empty($marker))
{
$request->setParameter('marker', $marker);
}
if (!empty($maxKeys))
{
$request->setParameter('max-keys', $maxKeys);
}
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
$response = $request->getResponse();
if (!$response->error->isError() && $response->code !== 200)
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
);
}
$results = [
'objects' => [],
'nextMarker' => null,
];
if ($response->hasBody() && isset($response->body->Contents))
{
foreach ($response->body->Contents as $c)
{
$results['objects'][(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$results['nextMarker'] = (string) $c->Key;
}
}
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
{
foreach ($response->body->CommonPrefixes as $c)
{
$results['objects'][(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
}
if ($response->hasBody() && isset($response->body->IsTruncated) &&
((string) $response->body->IsTruncated == 'false')
)
{
$results['nextMarker'] = null;
return $results;
}
if ($response->hasBody() && isset($response->body->NextMarker))
{
$results['nextMarker'] = (string) $response->body->NextMarker;
}
return $results;
}
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use LogicException; use LogicException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Defines an input source for PUT/POST requests to Amazon S3 * Defines an input source for PUT/POST requests to Amazon S3
@ -20,17 +20,17 @@ class Input
/** /**
* Input type: resource * Input type: resource
*/ */
const INPUT_RESOURCE = 1; public const INPUT_RESOURCE = 1;
/** /**
* Input type: file * Input type: file
*/ */
const INPUT_FILE = 2; public const INPUT_FILE = 2;
/** /**
* Input type: raw data * Input type: raw data
*/ */
const INPUT_DATA = 3; public const INPUT_DATA = 3;
/** /**
* File pointer, in case we have a resource * File pointer, in case we have a resource
@ -176,9 +176,15 @@ class Input
function __destruct() function __destruct()
{ {
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
} }
/** /**
@ -257,11 +263,17 @@ class Input
$this->data = null; $this->data = null;
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
$this->fp = @fopen($file, 'rb'); $this->fp = @fopen($file, 'r');
if ($this->fp === false) if ($this->fp === false)
{ {
@ -294,9 +306,15 @@ class Input
$this->data = $data; $this->data = $data;
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
$this->file = null; $this->file = null;
$this->fp = null; $this->fp = null;
@ -328,9 +346,15 @@ class Input
$this->data = $data; $this->data = $data;
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
$this->file = null; $this->file = null;
$this->fp = null; $this->fp = null;
@ -450,7 +474,7 @@ class Input
*/ */
public function setSha256(?string $sha256): void public function setSha256(?string $sha256): void
{ {
$this->sha256 = strtolower($sha256); $this->sha256 = is_null($sha256) ? null : strtolower($sha256);
} }
/** /**
@ -532,7 +556,7 @@ class Input
switch ($this->getInputType()) switch ($this->getInputType())
{ {
case self::INPUT_DATA: case self::INPUT_DATA:
return function_exists('mb_strlen') ? mb_strlen($this->data, '8bit') : strlen($this->data); return function_exists('mb_strlen') ? mb_strlen($this->data ?? '', '8bit') : strlen($this->data ?? '');
break; break;
case self::INPUT_FILE: case self::INPUT_FILE:
@ -635,7 +659,7 @@ class Input
$ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; return $exts[$ext] ?? 'application/octet-stream';
} }
/** /**

View file

@ -3,16 +3,16 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error; use Akeeba\S3\Response\Error;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
class Request class Request
@ -142,6 +142,12 @@ class Request
// The date must always be added as a header // The date must always be added as a header
$this->headers['Date'] = gmdate('D, d M Y H:i:s O'); $this->headers['Date'] = gmdate('D, d M Y H:i:s O');
// S3-"compatible" services use a different date format. Because why not?
if (strpos($this->headers['Host'], '.amazonaws.com') === false)
{
$this->headers['Date'] = gmdate('D, d M Y H:i:s T');
}
// If there is a security token we need to set up the X-Amz-Security-Token header // If there is a security token we need to set up the X-Amz-Security-Token header
$token = $this->configuration->getToken(); $token = $this->configuration->getToken();
@ -367,7 +373,7 @@ class Request
* *
* @return Response * @return Response
*/ */
public function getResponse(): Response public function getResponse(bool $rawResponse = false): Response
{ {
$this->processParametersIntoResource(); $this->processParametersIntoResource();
@ -417,8 +423,10 @@ class Request
* Caveat: if your bucket contains dots in the name we have to turn off host verification due to the way the * Caveat: if your bucket contains dots in the name we have to turn off host verification due to the way the
* S3 SSL certificates are set up. * S3 SSL certificates are set up.
*/ */
$isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com') || $isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com')
substr($this->headers['Host'], -16) == 'amazonaws.com.cn'; || substr(
$this->headers['Host'], -16
) == 'amazonaws.com.cn';
$tooManyDots = substr_count($this->headers['Host'], '.') > 4; $tooManyDots = substr_count($this->headers['Host'], '.') > 4;
$verifyHost = ($isAmazonS3 && $tooManyDots) ? 0 : 2; $verifyHost = ($isAmazonS3 && $tooManyDots) ? 0 : 2;
@ -429,6 +437,27 @@ class Request
curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_URL, $url);
/**
* Set the optional x-amz-date header for third party services.
*
* Amazon S3 proper expects to get the date from the Date header. Third party services typically implement the
* (wrongly) documented behaviour of using the x-amz-date header but, if it's missing, fall back to the Date
* header. Wasabi does not fall back; it only uses the x-amz-date header which is why we have to set it here if
* the request iss not made to Amazon S3 proper.
*/
$this->headers['x-amz-date'] = strpos($this->headers['Host'], '.amazonaws.com') !== false
? ''
: (new \DateTime($this->headers['Date']))->format('Ymd\THis\Z');
/**
* Remove empty headers.
*
* While Amazon S3 proper and most third party implementations have no problem with that, there a few of them
* (such as Synology C2) which choke on empty headers.
*/
$this->headers = array_filter($this->headers);
// Get the request signature
$signer = Signature::getSignatureObject($this, $this->configuration->getSignatureMethod()); $signer = Signature::getSignatureObject($this, $this->configuration->getSignatureMethod());
$signer->preProcessHeaders($this->headers, $this->amzHeaders); $signer->preProcessHeaders($this->headers, $this->amzHeaders);
@ -482,7 +511,7 @@ class Request
$data = $this->input->getDataReference(); $data = $this->input->getDataReference();
if (strlen($data)) if (strlen($data ?? ''))
{ {
curl_setopt($curl, CURLOPT_POSTFIELDS, $data); curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
} }
@ -538,12 +567,18 @@ class Request
@curl_close($curl); @curl_close($curl);
// Set the body data // Set the body data
$this->response->finaliseBody(); $this->response->finaliseBody($rawResponse);
// Clean up file resources // Clean up file resources
if (!is_null($this->fp) && is_resource($this->fp)) if (!is_null($this->fp) && is_resource($this->fp))
{ {
fclose($this->fp); try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
} }
return $this->response; return $this->response;
@ -560,7 +595,7 @@ class Request
*/ */
protected function __responseWriteCallback($curl, string $data): int protected function __responseWriteCallback($curl, string $data): int
{ {
if (in_array($this->response->code, [200, 206]) && !is_null($this->fp) && is_resource($this->fp)) if (in_array($this->response->code, [0, 200, 206]) && !is_null($this->fp) && is_resource($this->fp))
{ {
return fwrite($this->fp, $data); return fwrite($this->fp, $data);
} }
@ -592,7 +627,15 @@ class Request
return $strlen; return $strlen;
} }
[$header, $value] = explode(': ', trim($data), 2); // Ignore malformed headers without a value.
if (strpos($data, ':') === false)
{
return $strlen;
}
[$header, $value] = explode(':', trim($data), 2);
$header = trim($header ?? '');
$value = trim($value ?? '');
switch (strtolower($header)) switch (strtolower($header))
{ {
@ -609,10 +652,12 @@ class Request
break; break;
case 'etag': case 'etag':
$this->response->setHeader('hash', $value[0] == '"' ? substr($value, 1, -1) : $value); $this->response->setHeader('hash', trim($value, '"'));
break; break;
default: default:
$this->response->setHeader(strtolower($header), is_numeric($value) ? (int) $value : $value);
if (preg_match('/^x-amz-meta-.*$/', $header)) if (preg_match('/^x-amz-meta-.*$/', $header))
{ {
$this->setHeader($header, is_numeric($value) ? (int) $value : $value); $this->setHeader($header, is_numeric($value) ? (int) $value : $value);
@ -652,13 +697,12 @@ class Request
$query = substr($query, 0, -1); $query = substr($query, 0, -1);
$this->uri .= $query; $this->uri .= $query;
if (array_key_exists('acl', $this->parameters) || if (array_key_exists('acl', $this->parameters) || array_key_exists('location', $this->parameters)
array_key_exists('location', $this->parameters) || || array_key_exists('torrent', $this->parameters)
array_key_exists('torrent', $this->parameters) || || array_key_exists('logging', $this->parameters)
array_key_exists('logging', $this->parameters) || || array_key_exists('uploads', $this->parameters)
array_key_exists('uploads', $this->parameters) || || array_key_exists('uploadId', $this->parameters)
array_key_exists('uploadId', $this->parameters) || || array_key_exists('partNumber', $this->parameters)
array_key_exists('partNumber', $this->parameters)
) )
{ {
$this->resource .= $query; $this->resource .= $query;
@ -720,6 +764,8 @@ class Request
} }
/** /**
* Only applies to Amazon S3 proper.
*
* When using the Amazon S3 with the v4 signature API we have to use a different hostname per region. The * When using the Amazon S3 with the v4 signature API we have to use a different hostname per region. The
* mapping can be found in https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region * mapping can be found in https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region
* *
@ -728,7 +774,8 @@ class Request
* *
* v4 signing does NOT support non-Amazon endpoints. * v4 signing does NOT support non-Amazon endpoints.
*/ */
if (in_array($endpoint, ['s3.amazonaws.com', 'amazonaws.com.cn']))
{
// Most endpoints: s3-REGION.amazonaws.com // Most endpoints: s3-REGION.amazonaws.com
$regionalEndpoint = $region . '.amazonaws.com'; $regionalEndpoint = $region . '.amazonaws.com';
@ -748,6 +795,7 @@ class Request
{ {
$endpoint = 's3.' . $regionalEndpoint; $endpoint = 's3.' . $regionalEndpoint;
} }
}
// Legacy path style access: return just the endpoint // Legacy path style access: return just the endpoint
if ($configuration->getUseLegacyPathStyle()) if ($configuration->getUseLegacyPathStyle())

View file

@ -3,18 +3,18 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\PropertyNotFound; use Akeeba\S3\Exception\PropertyNotFound;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error; use Akeeba\S3\Response\Error;
use SimpleXMLElement; use SimpleXMLElement;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Amazon S3 API response object * Amazon S3 API response object
@ -124,7 +124,7 @@ class Response
* *
* @param string|SimpleXMLElement|null $body * @param string|SimpleXMLElement|null $body
*/ */
public function setBody($body): void public function setBody($body, bool $rawResponse = false): void
{ {
$this->body = null; $this->body = null;
@ -135,7 +135,7 @@ class Response
$this->body = $body; $this->body = $body;
$this->finaliseBody(); $this->finaliseBody($rawResponse);
} }
public function resetBody(): void public function resetBody(): void
@ -153,7 +153,7 @@ class Response
$this->body .= $data; $this->body .= $data;
} }
public function finaliseBody(): void public function finaliseBody(bool $rawResponse = false): void
{ {
if (!$this->hasBody()) if (!$this->hasBody())
{ {
@ -165,8 +165,14 @@ class Response
$this->headers['type'] = 'text/plain'; $this->headers['type'] = 'text/plain';
} }
if (is_string($this->body) && if (
(($this->headers['type'] == 'application/xml') || (substr($this->body, 0, 5) == '<?xml')) !$rawResponse
&& is_string($this->body)
&&
(
($this->headers['type'] == 'application/xml')
|| (substr($this->body, 0, 5) == '<?xml')
)
) )
{ {
$this->body = simplexml_load_string($this->body); $this->body = simplexml_load_string($this->body);
@ -332,8 +338,8 @@ class Response
) )
{ {
$this->error = new Error( $this->error = new Error(
$this->code, 500,
(string) $this->body->Message (string) $this->body->Code . ':' . (string) $this->body->Message
); );
if (isset($this->body->Resource)) if (isset($this->body->Resource))

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Response; namespace Akeeba\S3\Response;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* S3 response error object * S3 response error object

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