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:
include:
- PHP_MAJOR_VERSION: 7.3
PHP_VERSION: 7.3.33
- PHP_MAJOR_VERSION: 7.4
PHP_VERSION: 7.4.33
- PHP_MAJOR_VERSION: 8.0
PHP_VERSION: 8.0.29
PHP_VERSION: 8.0.30
- PHP_MAJOR_VERSION: 8.1
PHP_VERSION: 8.1.21
PHP_VERSION: 8.1.23
- 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...)
labels:

View File

@ -455,7 +455,7 @@ function advancedcontentfilter_prepare_item_row(array $item_row): array
$item_row['tags'] = $tags['tags'];
$item_row['hashtags'] = $tags['hashtags'];
$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;
}

View File

@ -32,15 +32,16 @@ use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Model\Photo;
use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Object\Image;
@ -50,9 +51,23 @@ use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
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 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()
{
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') {
$did = $hookData['uri'];
} elseif (preg_match('#^' . BLUESKY_HOST . '/profile/(.+)#', $hookData['uri'], $matches)) {
$did = bluesky_get_did($pconfig['uid'], $matches[1]);
} elseif (preg_match('#^' . BLUESKY_WEB . '/profile/(.+)#', $hookData['uri'], $matches)) {
$did = bluesky_get_did($matches[1]);
if (empty($did)) {
return;
}
@ -128,6 +143,8 @@ function bluesky_probe_detect(array &$hookData)
$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
$hookData['result']['about'] = HTML::toBBCode($data->description ?? '');
$hookData['result']['photo'] = $data->avatar ?? '';
@ -153,11 +170,11 @@ function bluesky_item_by_link(array &$hookData)
return;
}
if (!preg_match('#^' . BLUESKY_HOST . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) {
if (!preg_match('#^' . BLUESKY_WEB . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) {
return;
}
$did = bluesky_get_did($hookData['uid'], $matches[1]);
$did = bluesky_get_did($matches[1]);
if (empty($did)) {
return;
}
@ -166,7 +183,7 @@ function bluesky_item_by_link(array &$hookData)
$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]);
if (!empty($uri)) {
$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;
$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');
$did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
$token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token');
$import = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import') ?? 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/');
$html = Renderer::replaceMacros($t, [
'$enable' => ['bluesky', DI::l10n()->t('Enable Bluesky Post Addon'), $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_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],
'$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.")],
'$status' => $status
'$status' => bluesky_get_status($handle, $did, $pds, $token),
]);
$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)
{
if (empty($_POST['bluesky-submit'])) {
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_did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
$host = $_POST['bluesky_host'];
$handle = $_POST['bluesky_handle'];
$handle = trim($_POST['bluesky_handle'], ' @');
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', 'host', $host);
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_feeds', intval($_POST['bluesky_import_feeds']));
if (!empty($host) && !empty($handle)) {
if (empty($old_did) || $old_host != $host || $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')));
if (!empty($handle)) {
if (empty($old_did) || $old_handle != $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 {
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']);
}
}
@ -391,7 +463,7 @@ function bluesky_jot_nets(array &$jotnets_fields)
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'));
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
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_notifications.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'], $last);
if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import_feeds')) {
$feeds = bluesky_get_feeds($pconfig['uid']);
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;
}
// 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');
$urls = bluesky_get_urls(Post\Media::removeFromBody($item['body']));
$item['body'] = $urls['body'];
@ -622,10 +701,14 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
$record = [
'text' => $facets['body'],
'$type' => 'app.bsky.feed.post',
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => 'app.bsky.feed.post'
];
if (!empty($language)) {
$record['langs'] = [$language];
}
if (!empty($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
{
// 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);
$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
if (preg_match_all("/\[url\](https?:.*?)\[\/url\]/ism", $body, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
@ -742,9 +834,17 @@ function bluesky_get_facets(string $body, array $urls): array
$facet->index->byteStart = $pos;
$feature = new stdClass;
$feature->uri = $url['url'];
$type = '$type';
$feature->$type = 'app.bsky.richtext.facet#link';
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';
} else {
continue;
}
$facet->features = [$feature];
$facets[] = $facet;
@ -830,7 +930,7 @@ function bluesky_delete_post(string $uri, int $uid)
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');
if (empty($data)) {
@ -842,7 +942,7 @@ function bluesky_fetch_timeline(int $uid)
}
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)) {
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');
if (empty($data->notifications)) {
@ -912,7 +1012,7 @@ function bluesky_fetch_notifications(int $uid)
$item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::LIKE;
$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'])) {
$data = Item::insert($item);
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['body'] = $item['verb'] = Activity::ANNOUNCE;
$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'])) {
$data = Item::insert($item);
Logger::debug('Got repost', ['uid' => $uid, 'result' => $data, 'uri' => $uri]);
@ -941,17 +1041,17 @@ function bluesky_fetch_notifications(int $uid)
break;
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]);
break;
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]);
break;
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]);
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]);
if (empty($data)) {
@ -983,15 +1083,22 @@ function bluesky_fetch_feed(int $uid, string $feed)
}
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]);
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)) {
$post = Post::selectFirst(['uri-id'], ['id' => $id]);
$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]);
if (!empty($post['uri-id'])) {
$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]);
} else {
Logger::notice('Post not found', ['id' => $id, 'entry' => $entry]);
}
}
if (!empty($entry->reason)) {
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);
if ($id = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $uid]) || $id = Post::selectFirst(['id'], ['extid' => $uri, 'uid' => $uid])) {
return $id;
if ($id = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $uid])) {
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 ?? '']);
$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)) {
return 0;
}
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'])) {
$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
@ -1061,7 +1172,7 @@ function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_ui
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)) {
return [];
@ -1070,7 +1181,7 @@ function bluesky_get_content(array $item, stdClass $record, string $uri, int $ui
if (!empty($record->reply)) {
$item['parent-uri'] = bluesky_get_uri($record->reply->root);
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'])) {
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);
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'])) {
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['transmitted-languages'] = $record->langs ?? [];
if (($last_poll != 0) && strtotime($item['created']) > $last_poll) {
$item['received'] = $item['created'];
}
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)) {
return $text;
@ -1129,8 +1246,14 @@ function bluesky_get_text(stdClass $record): string
}
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:
Logger::notice('Unhandled feature type', ['type' => $feature->$type, 'record' => $record]);
Logger::notice('Unhandled feature type', ['type' => $feature->$type, 'feature' => $feature, 'record' => $record]);
break;
}
}
@ -1141,7 +1264,7 @@ function bluesky_get_text(stdClass $record): string
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';
switch ($embed->$type) {
@ -1178,18 +1301,17 @@ function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $le
break;
}
$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($embed->record->embeds)) {
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);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
Item::insert($shared);
}
}
if (!empty($shared)) {
if (!empty($shared['uri-id'])) {
$item['quote-uri-id'] = $shared['uri-id'];
}
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']]);
if (empty($shared)) {
$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($embed->record->record->embeds)) {
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);
}
}
$id = Item::insert($shared);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
Item::insert($shared);
}
}
if (!empty($shared)) {
if (!empty($shared['uri-id'])) {
$item['quote-uri-id'] = $shared['uri-id'];
}
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;
@ -1230,7 +1350,7 @@ function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $le
function bluesky_get_uri(stdClass $post): string
{
if (empty($post->cid)) {
Logger::info('Invalid URI', ['post' => $post, 'callstack' => System::callstack(10, 0, true)]);
Logger::info('Invalid URI', ['post' => $post]);
return '';
}
return $post->uri . ':' . $post->cid;
@ -1279,7 +1399,7 @@ function bluesky_get_uri_parts(string $uri): ?stdClass
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);
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;
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)) {
Logger::info('Thread was not fetched', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $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) {
$cdata = Contact::getPublicAndUserContactID($causer, $uid);
@ -1311,7 +1431,7 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, int $lev
$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
@ -1329,14 +1449,19 @@ function bluesky_fetch_post(string $uri, int $uid): string
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);
$fetched_uri = bluesky_fetch_post($uri, $uid);
if (empty($fetched_uri)) {
Logger::debug('Process missing post', ['uri' => $uri]);
$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)) {
$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)) {
$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);
if (!$id) {
@ -1363,7 +1488,7 @@ function bluesky_process_thread(stdClass $thread, int $uid, array $cdata, int $l
}
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]);
}
@ -1428,10 +1553,9 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): a
'blocked' => false,
'readonly' => false,
'pending' => false,
'baseurl' => BLUESKY_HOST,
'url' => $author->did,
'nurl' => $author->did,
'alias' => BLUESKY_HOST . '/profile/' . $author->handle,
'alias' => BLUESKY_WEB . '/profile/' . $author->handle,
'name' => $author->displayName ?? $author->handle,
'nick' => $author->handle,
'addr' => $author->handle,
@ -1442,6 +1566,12 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): a
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]);
if (empty($data)) {
Logger::debug('Error fetching contact fields', ['uid' => $uid, 'url' => $fields['url']]);
@ -1500,9 +1630,9 @@ function bluesky_get_preferences(int $uid): stdClass
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)) {
return '';
}
@ -1510,6 +1640,38 @@ function bluesky_get_did(int $uid, string $handle): string
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
{
$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']);
if (empty($data)) {
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_TOKEN_FAIL);
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', 'refresh_token', $data->refreshJwt);
DI::pConfig()->set($uid, 'bluesky', 'token_created', time());
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_TOKEN_OK);
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
{
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) {
Logger::notice('Exception on post', ['exception' => $e]);
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_API_FAIL);
return null;
}
if (!$curlResult->isSuccess()) {
Logger::notice('API Error', ['error' => json_decode($curlResult->getBody()) ?: $curlResult->getBody()]);
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_API_FAIL);
return null;
}
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_SUCCESS);
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);
}
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 {
$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) {
Logger::notice('Exception on get', ['exception' => $e]);
return null;

View File

@ -6,11 +6,11 @@ function bluesky_feed_run($argv, $argc)
{
require_once 'addon/bluesky/bluesky.php';
if ($argc != 3) {
if ($argc != 4) {
return;
}
Logger::debug('Importing feed - start', ['user' => $argv[1], 'feed' => $argv[2]]);
bluesky_fetch_feed($argv[1], $argv[2]);
Logger::debug('Importing feed - done', ['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], $argv[3]);
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';
if ($argc != 2) {
if ($argc != 3) {
return;
}
Logger::notice('importing notifications - start', ['user' => $argv[1]]);
bluesky_fetch_notifications($argv[1]);
Logger::notice('importing notifications - done', ['user' => $argv[1]]);
Logger::notice('importing notifications - start', ['user' => $argv[1], 'last_poll' => $argv[2]]);
bluesky_fetch_notifications($argv[1], $argv[2]);
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';
if ($argc != 2) {
if ($argc != 3) {
return;
}
Logger::notice('importing timeline - start', ['user' => $argv[1]]);
bluesky_fetch_timeline($argv[1]);
Logger::notice('importing timeline - done', ['user' => $argv[1]]);
Logger::notice('importing timeline - start', ['user' => $argv[1], 'last_poll' => $argv[2]]);
bluesky_fetch_timeline($argv[1], $argv[2]);
Logger::notice('importing timeline - done', ['user' => $argv[1], 'last_poll' => $argv[2]]);
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,70 +17,100 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: bluesky.php:314
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
#: bluesky.php:336
msgid "Enable Bluesky Post Addon"
msgstr ""
#: bluesky.php:319
#: bluesky.php:337
msgid "Post to Bluesky by default"
msgstr ""
#: bluesky.php:320
#: bluesky.php:338
msgid "Import the remote timeline"
msgstr ""
#: bluesky.php:321
#: bluesky.php:339
msgid "Import the pinned feeds"
msgstr ""
#: bluesky.php:321
#: bluesky.php:339
msgid ""
"When activated, Posts will be imported from all the feeds that you pinned in "
"Bluesky."
msgstr ""
#: bluesky.php:322
msgid "Bluesky host"
#: bluesky.php:340
msgid "Personal Data Server"
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"
msgstr ""
#: bluesky.php:324
#: bluesky.php:342
msgid "Bluesky DID"
msgstr ""
#: bluesky.php:324
#: bluesky.php:342
msgid ""
"This is the unique identifier. It will be fetched automatically, when the "
"handle is entered."
msgstr ""
#: bluesky.php:325
#: bluesky.php:343
msgid "Bluesky app password"
msgstr ""
#: bluesky.php:325
#: bluesky.php:343
msgid ""
"Please don't add your real password here, but instead create a specific app "
"password in the Bluesky settings."
msgstr ""
#: bluesky.php:331
#: bluesky.php:349
msgid "Bluesky Import/Export"
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"
msgstr ""

View File

@ -3,7 +3,7 @@
{{include file="field_checkbox.tpl" field=$bydefault}}
{{include file="field_checkbox.tpl" field=$import}}
{{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=$did}}
{{include file="field_input.tpl" field=$password}}

View File

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

View File

@ -5,11 +5,11 @@ function string_plural_select_fr($n){
$n = intval($n);
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['Use Cat as Avatar'] = 'Utiliser Chat comme avatar';
$a->strings['Another random Cat!'] = 'Un autre chat aléatoire !';
$a->strings['Reset to email Cat'] = 'Réinitialiser à Chat courriel';
$a->strings['Use Cat as Avatar'] = 'Utiliser ce Chat';
$a->strings['Another random Cat!'] = 'Un autre Chat aléatoire !';
$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['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';

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

View File

@ -5,8 +5,7 @@ function string_plural_select_ru($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; }
}}
$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['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".';
@ -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 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['Language Filter'] = 'Языковой фильтр';
$a->strings['Save Settings'] = 'Сохранить настройки';
$a->strings['Filtered language: %s'] = 'Отфильтрованный язык: %s';

View File

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

View File

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

View File

@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: friendica\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"
"Last-Translator: Aditoo, 2018\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"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Globální fórum podpory"
msgid "Global Support Group"
msgstr ""
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Místní fórum podpory"
msgid "Local Support Group"
msgstr ""
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Uložit nastavení"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Zpráva"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
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."
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "Přidejte odkaz na globální fórum podpory"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr ""
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "Má být zobrazen odkaz na globální fórum podpory?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr ""
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "Přidejte odkaz na místní fórum podpory"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr ""
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
msgstr ""
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid "Name of the local support group"
msgstr "Název místního fóra podpory"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['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['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['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['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:
# Raroun, 2023
# Tobias Diekershoff <tobias.diekershoff@gmx.net>, 2021
# Ulf Rompe <transifex.com@rompe.org>, 2019
msgid ""
msgstr ""
"Project-Id-Version: friendica\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"
"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"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -29,48 +30,48 @@ msgid "Tips for New Members"
msgstr "Tipps für neue Nutzer"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Globales Forum für Hilfsanfragen"
msgid "Global Support Group"
msgstr "Globale Support-Gruppe"
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Lokales Forum für Hilfsanfragen"
msgid "Local Support Group"
msgstr "Lokale Support-Gruppe"
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Einstellungen speichern"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Nachricht"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr "Deine Nachricht für neue Nutzer. BBCode kann verwendet werden."
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "Link zum globalen Support-Forum anzeigen"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr "Fügen Sie einen Link der globalen Support-Gruppe hinzu"
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "Soll ein Link zum globalen Support-Forum angezeigt werden?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr "Soll ein Link zur globalen Support-Gruppe angezeigt werden?"
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "Link zum lokalen Support-Forum anzeigen"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr "Fügen Sie einen Link der lokalen Support-Gruppe hinzu"
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
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"
msgstr "Name des lokalen Support-Forums"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['Tips for New Members'] = 'Tipps für neue Nutzer';
$a->strings['Global Support Forum'] = 'Globales Forum für Hilfsanfragen';
$a->strings['Local Support Forum'] = 'Lokales Forum für Hilfsanfragen';
$a->strings['Global Support Group'] = 'Globale Support-Gruppe';
$a->strings['Local Support Group'] = 'Lokale Support-Gruppe';
$a->strings['Save Settings'] = 'Einstellungen speichern';
$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['Add a link to global support forum'] = 'Link zum globalen Support-Forum anzeigen';
$a->strings['Should a link to the global support forum be displayed?'] = 'Soll ein Link zum globalen Support-Forum angezeigt werden?';
$a->strings['Add a link to the local support forum'] = 'Link zum lokalen Support-Forum anzeigen';
$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['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 group be displayed?'] = 'Soll ein Link zur globalen Support-Gruppe angezeigt werden?';
$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 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['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 ""
"Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n"
"PO-Revision-Date: 2021-04-06 01:46+0000\n"
"Last-Translator: Senex Petrovic <javierruizo@hotmail.com>\n"
"Language-Team: Spanish (http://www.transifex.com/Friendica/friendica/language/es/)\n"
"POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Senex Petrovic <javierruizo@hotmail.com>, 2021\n"
"Language-Team: Spanish (http://app.transifex.com/Friendica/friendica/language/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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
msgid "New Member"
@ -29,48 +29,48 @@ msgid "Tips for New Members"
msgstr "Consejos para Nuevos Miembros"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Foro de Soporte Global"
msgid "Global Support Group"
msgstr ""
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Foro de Soporte Local"
msgid "Local Support Group"
msgstr ""
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Guardar Ajustes"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Mensaje"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr "Su mensaje para los nuevos miembros. Puede usar bbcode aquí"
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "Añadir un enlace al foro de soporte global"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr ""
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "¿Debería mostrarse un enlace al foro de soporte global?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr ""
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "Añadir un enlace al foro de soporte local"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr ""
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
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"
msgstr "Nombre del grupo de soporte local"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)"

View File

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

View File

@ -4,6 +4,7 @@
#
#
# Translators:
# Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# Nicolas Derive, 2022
# StefOfficiel <pichard.stephane@free.fr>, 2015
@ -11,10 +12,10 @@ msgid ""
msgstr ""
"Project-Id-Version: friendica\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"
"Last-Translator: Nicolas Derive, 2022\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n"
"Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -30,48 +31,48 @@ msgid "Tips for New Members"
msgstr "Conseils aux nouveaux venus"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Forum de support global"
msgid "Global Support Group"
msgstr "Groupe de support global"
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Forum de support local"
msgid "Local Support Group"
msgstr "Groupe de support local"
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Enregistrer les paramètres"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Message"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr "Votre messages aux nouveaux venus. Vous pouvez utiliser des BBCodes."
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "Ajouter un lien vers le forum de support global"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr "Ajouter un lien vers le groupe de support global"
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "Montrer un lien vers le forum de support global?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr "Montrer un lien vers le groupe de support global ?"
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "Ajouter un lien vers le forum de support local"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr "Ajouter un lien vers le groupe de support local"
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
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"
msgstr "Nom du groupe de support local"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['Tips for New Members'] = 'Conseils aux nouveaux venus';
$a->strings['Global Support Forum'] = 'Forum de support global';
$a->strings['Local Support Forum'] = 'Forum de support local';
$a->strings['Global Support Group'] = 'Groupe de support global';
$a->strings['Local Support Group'] = 'Groupe de support local';
$a->strings['Save Settings'] = 'Enregistrer les paramètres';
$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['Add a link to global support forum'] = 'Ajouter un lien vers le forum 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['Add a link to the local support forum'] = 'Ajouter un lien vers le forum 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['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 group be displayed?'] = 'Montrer un lien vers le groupe de support global ?';
$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 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['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:
# Balázs Úr, 2020-2021
# Balázs Úr, 2020-2021,2023
msgid ""
msgstr ""
"Project-Id-Version: friendica\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"
"Last-Translator: Balázs Úr, 2020-2021\n"
"Language-Team: Hungarian (http://www.transifex.com/Friendica/friendica/language/hu/)\n"
"Last-Translator: Balázs Úr, 2020-2021,2023\n"
"Language-Team: Hungarian (http://app.transifex.com/Friendica/friendica/language/hu/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -28,48 +28,48 @@ msgid "Tips for New Members"
msgstr "Tippek új tagoknak"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Globális támogató fórum"
msgid "Global Support Group"
msgstr "Globális támogatási csoport"
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Helyi támogató fórum"
msgid "Local Support Group"
msgstr "Helyi támogatási csoport"
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Beállítások mentése"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Üzenet"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr "Az Ön üzenete az új tagoknak. Itt használhat BBCode-ot."
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "A globális támogató fórumra mutató hivatkozás hozzáadása"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr "A globális támogatási csoportra mutató hivatkozás hozzáadása"
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "Meg kell jeleníteni a globális támogató fórumra mutató hivatkozást?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr "Meg kell jeleníteni a globális támogatási csoportra mutató hivatkozást?"
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "A helyi támogató fórumra mutató hivatkozás hozzáadása"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr "A helyi támogatási csoportra mutató hivatkozás hozzáadása"
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
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"
msgstr "A helyi támogató csoport neve"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['Tips for New Members'] = 'Tippek új tagoknak';
$a->strings['Global Support Forum'] = 'Globális támogató fórum';
$a->strings['Local Support Forum'] = 'Helyi támogató fórum';
$a->strings['Global Support Group'] = 'Globális támogatási csoport';
$a->strings['Local Support Group'] = 'Helyi támogatási csoport';
$a->strings['Save Settings'] = 'Beállítások mentése';
$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['Add a link to global support forum'] = 'A globális támogató fórumra 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['Add a link to the local support forum'] = 'A helyi támogató fórumra 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['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 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 group'] = 'A helyi támogatási csoportra mutató hivatkozás hozzáadása';
$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['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 ""
"Project-Id-Version: friendica\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"
"Last-Translator: Sylke Vicious <silkevicious@gmail.com>, 2020-2021\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"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Forum Globale di Supporto"
msgid "Global Support Group"
msgstr ""
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Forum Locale di Supporto"
msgid "Local Support Group"
msgstr ""
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Salva Impostazioni"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Messaggio"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr "Il tuo messaggio per i nuovi utenti. Puoi usare BBCode"
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "Aggiunge un collegamento al forum di supporto globale"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr ""
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "Mostrare il collegamento al forum di supporto globale?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr ""
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "Aggiunge un collegamento al forum di supporto locale"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr ""
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
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"
msgstr "Nome del gruppo locale di supporto"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['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['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['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['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 ""
"Project-Id-Version: friendica\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"
"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"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -29,48 +29,48 @@ msgid "Tips for New Members"
msgstr "Wskazówki dla nowych użytkowników"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgstr "Globalne forum pomocy technicznej"
msgid "Global Support Group"
msgstr ""
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Lokalne Forum Wsparcia"
msgid "Local Support Group"
msgstr ""
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Zapisz ustawienia"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Wiadomość"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
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."
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
msgstr "Dodaj odnośnik do globalnego forum pomocy technicznej"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr ""
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
msgstr "Czy powinien być wyświetlany odnośnik do globalnego forum pomocy technicznej?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr ""
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
msgstr "Dodaj odnośnik do lokalnego forum pomocy technicznej"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr ""
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
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"
msgstr "Nazwa grupy lokalnej pomocy technicznej"
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['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['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['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['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 ""
"Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-06-01 14:12+0200\n"
"PO-Revision-Date: 2020-04-23 14:23+0000\n"
"Last-Translator: Alexander An <ravnina@gmail.com>\n"
"Language-Team: Russian (http://www.transifex.com/Friendica/friendica/language/ru/)\n"
"POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Alexander An <ravnina@gmail.com>, 2020\n"
"Language-Team: Russian (http://app.transifex.com/Friendica/friendica/language/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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"
#: newmemberwidget.php:21
#: newmemberwidget.php:29
msgid "New Member"
msgstr "Новичок"
#: newmemberwidget.php:22
#: newmemberwidget.php:30
msgid "Tips for New Members"
msgstr "Советы новичкам"
#: newmemberwidget.php:24
msgid "Global Support Forum"
msgstr "Общий форум поддержки"
#: newmemberwidget.php:33
msgid "Global Support Group"
msgstr ""
#: newmemberwidget.php:26
msgid "Local Support Forum"
msgstr "Местный форум поддержки"
#: newmemberwidget.php:37
msgid "Local Support Group"
msgstr ""
#: newmemberwidget.php:49
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Сохранить настройки"
#: newmemberwidget.php:50
#: newmemberwidget.php:63
msgid "Message"
msgstr "Сообщение"
#: newmemberwidget.php:50
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr "Ваше сообщение новичкам. Вы можете использовать BBCode."
#: newmemberwidget.php:51
msgid "Add a link to global support forum"
msgstr "Добавить ссылку на общий форум поддержки"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr ""
#: newmemberwidget.php:51
msgid "Should a link to the global support forum be displayed?"
msgstr "Показывать ссылку на общий форум поддержки?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr ""
#: newmemberwidget.php:52
msgid "Add a link to the local support forum"
msgstr "Добавить ссылку на местный форум поддержки"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr ""
#: newmemberwidget.php:52
#: newmemberwidget.php:65
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."
msgstr "Если у вас есть местный форум поддержки и вы хотите добавить ссылку на него, включите это."
msgstr ""
#: newmemberwidget.php:53
#: newmemberwidget.php:66
msgid "Name of the local support group"
msgstr "Название местной группы поддержки"
#: newmemberwidget.php:53
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" group here (i.e. helpers)"

View File

@ -7,14 +7,8 @@ function string_plural_select_ru($n){
}}
$a->strings['New Member'] = 'Новичок';
$a->strings['Tips for New Members'] = 'Советы новичкам';
$a->strings['Global Support Forum'] = 'Общий форум поддержки';
$a->strings['Local Support Forum'] = 'Местный форум поддержки';
$a->strings['Save Settings'] = 'Сохранить настройки';
$a->strings['Message'] = 'Сообщение';
$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['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 ""
"Project-Id-Version: friendica\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-02-01 18:15+0100\n"
"PO-Revision-Date: 2022-01-16 00:48+0000\n"
"Last-Translator: Kristoffer Grundström <lovaren@gmail.com>\n"
"Language-Team: Swedish (http://www.transifex.com/Friendica/friendica/language/sv/)\n"
"POT-Creation-Date: 2023-06-03 15:50-0400\n"
"PO-Revision-Date: 2014-06-23 10:26+0000\n"
"Last-Translator: Kristoffer Grundström <lovaren@gmail.com>, 2022\n"
"Language-Team: Swedish (http://app.transifex.com/Friendica/friendica/language/sv/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -28,48 +28,48 @@ msgid "Tips for New Members"
msgstr "Tips för nya medlemmar"
#: newmemberwidget.php:33
msgid "Global Support Forum"
msgid "Global Support Group"
msgstr ""
#: newmemberwidget.php:37
msgid "Local Support Forum"
msgstr "Lokalt hjälpforum"
msgid "Local Support Group"
msgstr ""
#: newmemberwidget.php:65
#: newmemberwidget.php:62
msgid "Save Settings"
msgstr "Spara inställningar"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Message"
msgstr "Meddelande"
#: newmemberwidget.php:66
#: newmemberwidget.php:63
msgid "Your message for new members. You can use bbcode here."
msgstr ""
#: newmemberwidget.php:67
msgid "Add a link to global support forum"
#: newmemberwidget.php:64
msgid "Add a link to global support group"
msgstr ""
#: newmemberwidget.php:67
msgid "Should a link to the global support forum be displayed?"
#: newmemberwidget.php:64
msgid "Should a link to the global support group be displayed?"
msgstr ""
#: newmemberwidget.php:68
msgid "Add a link to the local support forum"
#: newmemberwidget.php:65
msgid "Add a link to the local support group"
msgstr ""
#: newmemberwidget.php:68
#: newmemberwidget.php:65
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."
msgstr ""
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid "Name of the local support group"
msgstr ""
#: newmemberwidget.php:69
#: newmemberwidget.php:66
msgid ""
"If you checked the above, specify the <em>nickname</em> of the local support"
" 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['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['Message'] = 'Meddelande';

View File

@ -4,6 +4,7 @@
#
#
# Translators:
# Florent C., 2023
# Nicolas Derive, 2022-2023
# StefOfficiel <pichard.stephane@free.fr>, 2015
# Vincent Vindarel <vindarel@mailz.org>, 2018
@ -13,7 +14,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-10 14:42-0500\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"
"MIME-Version: 1.0\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 ""
"Use /expression/ to provide regular expressions, #tag to specfically match "
"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
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
#, php-format
msgid "Filtered tag: %s"
msgstr "Tag filtré: %s"
msgstr "Tag filtré : %s"
#: nsfw.php:156
#, 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['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['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['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';

View File

@ -20,4 +20,9 @@
width: 100%;
margin-top: 25px;
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['Show opt-out cookie link?'] = 'Show opt-out cookie link?';
$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["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:
# Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# Nicolas Derive, 2022
# ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015
@ -12,23 +13,23 @@ msgid ""
msgstr ""
"Project-Id-Version: friendica\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"
"Last-Translator: Nicolas Derive, 2022\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n"
"Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\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 ""
"This website is tracked using the <a href='http://www.matomo.org'>Matomo</a>"
" analytics tool."
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
msgid ""
"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)."
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"
msgstr "Sauvegarder les paramètres"
#: piwik.php:98
#: piwik.php:109
msgid "Matomo (Piwik) Base URL"
msgstr "URL de base de Matomo (Piwik)"
#: piwik.php:98
#: piwik.php:109
msgid ""
"Absolute path to your Matomo (Piwik) installation. (without protocol "
"(http/s), with trailing slash)"
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"
msgstr "ID du site"
#: piwik.php:100
#: piwik.php:111
msgid "Show opt-out cookie link?"
msgstr "Montrer le lien d'opt-out pour les cookies ?"
#: piwik.php:101
#: piwik.php:112
msgid "Asynchronous tracking"
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['Show opt-out cookie link?'] = 'Montrer le lien d\'opt-out pour les cookies ?';
$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:
# Florent C., 2023
# Hypolite Petovan <hypolite@mrpetovan.com>, 2022
# ea1cd8241cb389ffb6f92bc6891eff5d_dc12308 <70dced5587d47e18d88f9298024d96f8_93383>, 2015
# StefOfficiel <pichard.stephane@free.fr>, 2015
@ -11,86 +12,86 @@ msgid ""
msgstr ""
"Project-Id-Version: friendica\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"
"Last-Translator: Hypolite Petovan <hypolite@mrpetovan.com>, 2022\n"
"Language-Team: French (http://www.transifex.com/Friendica/friendica/language/fr/)\n"
"Last-Translator: Florent C., 2023\n"
"Language-Team: French (http://app.transifex.com/Friendica/friendica/language/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\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."
msgstr "Permission refusée."
#: pumpio.php:152
#: pumpio.php:156
#, php-format
msgid "Unable to register the client at the pump.io server '%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."
msgstr "Vous êtes maintenant authentifié sur pump.io."
#: pumpio.php:193
#: pumpio.php:197
msgid "return to the connector page"
msgstr "Retourner à la page du connecteur"
#: pumpio.php:213
#: pumpio.php:217
msgid "Post to pumpio"
msgstr "Publier sur pump.io"
#: pumpio.php:237
#: pumpio.php:241
msgid "Save Settings"
msgstr "Sauvegarder les paramètres"
#: pumpio.php:239
#: pumpio.php:243
msgid "Delete this preset"
msgstr "Supprimer ce préréglage"
#: pumpio.php:245
#: pumpio.php:249
msgid "Authenticate your pump.io connection"
msgstr "Identifiez votre connexion à pump.io"
#: pumpio.php:252
#: pumpio.php:256
msgid "Pump.io servername (without \"http://\" or \"https://\" )"
msgstr "Domaine du serveur Pump.io (sans \"http://\" ou \"https://\")"
#: pumpio.php:253
#: pumpio.php:257
msgid "Pump.io username (without the servername)"
msgstr "Nom d'utilisateur Pump.io (sans le domaine de serveur)"
#: pumpio.php:254
#: pumpio.php:258
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"
msgstr "Activer l'extension Pump.io"
#: pumpio.php:256
#: pumpio.php:260
msgid "Post to Pump.io by default"
msgstr "Publier sur Pump.io par défaut"
#: pumpio.php:257
#: pumpio.php:261
msgid "Should posts be public?"
msgstr "Les messages devraient être publiques ?"
#: pumpio.php:258
#: pumpio.php:262
msgid "Mirror all public posts"
msgstr "Refléter toutes les publications publiques"
#: pumpio.php:263
#: pumpio.php:267
msgid "Pump.io Import/Export/Mirror"
msgstr "Import/Export/Miroir Pump.io"
#: pumpio.php:920
#: pumpio.php:924
msgid "status"
msgstr "statut"
#: pumpio.php:924
#: pumpio.php:928
#, php-format
msgid "%1$s likes %2$s's %3$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['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['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['Post to Pump.io by default'] = 'Publier sur Pump.io par défaut';
$a->strings['Should posts be public?'] = 'Les messages devraient être publiques ?';

View File

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

View File

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

View File

@ -4,29 +4,53 @@ A compact, dependency-less Amazon S3 API client implementing the most commonly u
## 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 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
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
```php
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
$configuration = new \Akeeba\S3\Configuration(
'YourAmazonAccessKey',
'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
@ -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/');
$jsonCredentials = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/' . $role);
$credentials = json_decode($jsonCredentials, true);
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
$configuration = new \Akeeba\S3\Configuration(
$credentials['AccessKeyId'],
$credentials['SecretAccessKey'],
'v4',
@ -45,14 +69,14 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
);
$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
token (`$credentials['Token']`) to the Configuration object. This is REQUIRED. The temporary credentials returned by
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
10 minutes before your cached credentials are set to expire. The metadata service is guaranteed to provision fresh
temporary credentials by that time.
@ -120,21 +144,21 @@ The last parameter (common prefixes) controls the listing of "subdirectories"
From a file:
```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile);
$input = \Akeeba\S3\Input::createFromFile($sourceFile);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
```
From a string:
```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromData($sourceString);
$input = \Akeeba\S3\Input::createFromData($sourceString);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
```
From a stream resource:
```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');
```
@ -145,7 +169,7 @@ In all cases the entirety of the file has to be loaded in memory.
Files are uploaded in 5Mb chunks.
```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile);
$input = \Akeeba\S3\Input::createFromFile($sourceFile);
$uploadId = $connector->startMultipart($input, 'mybucket', 'mypath/movie.mov');
$eTags = array();
@ -155,7 +179,7 @@ $partNumber = 0;
do
{
// 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->setPartNumber(++$partNumber);
@ -169,7 +193,7 @@ do
while (!is_null($eTag));
// 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->setEtags($eTags);
@ -209,6 +233,23 @@ $content = $connector->getObject('mybucket', 'path/to/file.jpg', false);
$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
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:
```php
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
$configuration = new \Akeeba\S3\Configuration(
'YourAmazonAccessKey',
'YourAmazonSecretKey'
);
@ -225,7 +266,7 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
$configuration->setSignatureMethod('v4');
$configuration->setUseDualstackUrl(true);
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration);
$connector = new \Akeeba\S3\Connector($configuration);
```
### 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
// DigitalOcean Spaces using v4 signatures
// 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',
'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ',
'v4',
@ -253,7 +294,7 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
);
$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.
@ -261,14 +302,14 @@ If your S3-compatible API uses v2 signatures you do not need to specify a region
```php
// DigitalOcean Spaces using v2 signatures
// 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',
'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ',
'v2'
);
$configuration->setEndpoint('nyc3.digitaloceanspaces.com');
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration);
$connector = new \Akeeba\S3\Connector($configuration);
```
### Legacy path-style access
@ -282,7 +323,7 @@ You need to do:
$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

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

View File

@ -1,19 +1,22 @@
{
"_readme": [
"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",
"This file is @generated automatically"
],
"content-hash": "1070071b351d45a80934e854f0725d64",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.3.4"
},
"platform-dev": []
"_readme": [
"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#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "27f387a657b2784510b177f73c436346",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.1.0 <8.4",
"ext-curl": "*",
"ext-simplexml": "*"
},
"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
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\S3\Connector;
use RuntimeException;
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)
{
$tempFilePath = tempnam(self::getTempFolder(), 'as3');
$tempFilePath = tempnam(static::getTempFolder(), 'as3');
if ($tempFilePath === false)
{
throw new RuntimeException("Cannot create a temporary file.");
}
$fp = @fopen($tempFilePath, 'wb', false);
$fp = @fopen($tempFilePath, 'w', false);
if ($fp === false)
{
throw new RuntimeException("Cannot write to the temporary file.");
}
$blockSize = self::BLOCK_SIZE;
$blockSize = static::BLOCK_SIZE;
$lastBlockSize = $size % $blockSize;
$wholeBlocks = (int) (($size - $lastBlockSize) / $blockSize);
$blockData = self::getRandomData();
$blockData = static::getRandomData();
for ($i = 0; $i < $wholeBlocks; $i++)
{
@ -83,7 +83,7 @@ abstract class AbstractTest
if (!$reuseBlock)
{
$blockData = self::getRandomData($blockSize);
$blockData = static::getRandomData($blockSize);
}
}
@ -155,7 +155,7 @@ abstract class AbstractTest
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
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* 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.
*
* 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
*/
@ -59,42 +59,42 @@ class BigFiles extends AbstractTest
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
{
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
{
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
{
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
{
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
{
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
{
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
{
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
@ -103,24 +103,24 @@ class BigFiles extends AbstractTest
$dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
self::$numberOfChunks = 0;
static::$numberOfChunks = 0;
if ($useString)
{
$sourceData = self::getRandomData($size);
$sourceData = static::getRandomData($size);
$input = Input::createFromData($sourceData);
}
else
{
// Create a file with random data
$sourceFile = self::createFile($size);
$sourceFile = static::createFile($size);
$input = Input::createFromFile($sourceFile);
}
// Upload the file. Throws exception if it fails.
$bucket = $options['bucket'];
if (!self::$multipart)
if (!static::$multipart)
{
$s3->putObject($input, $bucket, $uri);
}
@ -149,7 +149,7 @@ class BigFiles extends AbstractTest
$input->setEtags($eTags);
$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 (is_null($etag))
@ -166,7 +166,7 @@ class BigFiles extends AbstractTest
$partNumber++;
}
self::$numberOfChunks = count($eTags);
static::$numberOfChunks = count($eTags);
// Finalize the multipart upload. Tells Amazon to construct the file from the uploaded parts.
$s3->finalizeMultipart($input, $bucket, $uri);
@ -176,7 +176,7 @@ class BigFiles extends AbstractTest
$result = true;
// Should I download the file and compare its contents?
if (self::$downloadAfter)
if (static::$downloadAfter)
{
if ($useString)
{
@ -184,16 +184,16 @@ class BigFiles extends AbstractTest
$downloadedData = $s3->getObject($bucket, $uri);
// Compare the file contents.
$result = self::areStringsEqual($sourceData, $downloadedData);
$result = static::areStringsEqual($sourceData, $downloadedData);
}
else
{
// Download the data. Throws exception if it fails.
$downloadedFile = tempnam(self::getTempFolder(), 'as3');
$downloadedFile = tempnam(static::getTempFolder(), 'as3');
$s3->getObject($bucket, $uri, $downloadedFile);
// Compare the file contents.
$result = self::areFilesEqual($sourceFile, $downloadedFile);
$result = static::areFilesEqual($sourceFile, $downloadedFile);
@unlink($downloadedFile);
}
@ -206,7 +206,7 @@ class BigFiles extends AbstractTest
}
// Should I delete the remotely stored file?
if (self::$deleteRemote)
if (static::$deleteRemote)
{
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\S3\Connector;
class BucketLocation extends AbstractTest
{
@ -18,7 +18,7 @@ class BucketLocation extends AbstractTest
{
$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;
}

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\S3\Connector;
use RuntimeException;
class BucketsList extends AbstractTest
@ -19,16 +19,16 @@ class BucketsList extends AbstractTest
{
$buckets = $s3->listBuckets(true);
self::assert(is_array($buckets), "Detailed buckets list is not an array");
self::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");
self::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(is_array($buckets), "Detailed buckets list is not an array");
static::assert(isset($buckets['owner']), "Detailed buckets list does not list an owner");
static::assert(isset($buckets['owner']['id']), "Detailed buckets list does not list an owner's id");
static::assert(isset($buckets['owner']['name']), "Detailed buckets list does not list an owner's name");
static::assert(isset($buckets['buckets']), "Detailed buckets list does not list any buckets");
foreach ($buckets['buckets'] as $bucketInfo)
{
self::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['name']), "Bucket information does not list a name");
static::assert(isset($bucketInfo['time']), "Bucket information does not list a created times");
if ($bucketInfo['name'] === $options['bucket'])
{
@ -43,8 +43,8 @@ class BucketsList extends AbstractTest
{
$buckets = $s3->listBuckets(false);
self::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(is_array($buckets), "Simple buckets list is not an array");
static::assert(in_array($options['bucket'], $buckets), "Simple buckets list does not include configured bucket {$options['bucket']}");
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
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\S3\Connector;
use Akeeba\S3\Exception\CannotPutFile;
use Akeeba\S3\Input;
class ListFiles extends AbstractTest
{
@ -34,9 +34,9 @@ class ListFiles extends AbstractTest
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);
try
@ -52,7 +52,7 @@ class ListFiles extends AbstractTest
public static function teardown(Connector $s3, array $options): void
{
foreach (self::$paths as $uri)
foreach (static::$paths as $uri)
{
try
{
@ -69,29 +69,29 @@ class ListFiles extends AbstractTest
{
$listing = $s3->getBucket($options['bucket'], 'listtest_');
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files
self::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");
self::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing");
static::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing");
static::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.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
self::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");
self::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing");
static::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing");
static::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.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
self::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('spam.dat', $listing), "File spam.dat in listing");
static::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing");
foreach ($listing as $fileName => $info)
{
self::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time");
self::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['name']), "File entries must have a name");
static::assert(isset($info['time']), "File entries must have a time");
static::assert(isset($info['size']), "File entries must have a size");
static::assert(isset($info['hash']), "File entries must have a hash");
}
return true;
@ -101,37 +101,37 @@ class ListFiles extends AbstractTest
{
$listing = $s3->getBucket($options['bucket'], 'listtest_', null, 1);
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing)));
$files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'listtest_', array_shift($files));
self::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(is_array($continued), "The continued files listing must be an array");
static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued);
// Make sure I have the expected files
self::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");
self::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing");
static::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing");
static::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.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
self::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");
self::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing");
static::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing");
static::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.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
self::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('spam.dat', $listing), "File spam.dat in listing");
static::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing");
foreach ($listing as $fileName => $info)
{
self::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time");
self::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['name']), "File entries must have a name");
static::assert(isset($info['time']), "File entries must have a time");
static::assert(isset($info['size']), "File entries must have a size");
static::assert(isset($info['hash']), "File entries must have a hash");
}
return true;
@ -141,30 +141,30 @@ class ListFiles extends AbstractTest
{
$listing = $s3->getBucket($options['bucket'], 'list_deeper/test_');
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 3, "I am expecting to see 3 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");
self::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_one.dat', $listing), "File test_one.dat not in listing");
static::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_three.dat', $listing), "File test_three.dat not in listing");
// 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");
self::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");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
static::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing");
static::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing");
static::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.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
self::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/seven.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)
{
self::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time");
self::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['name']), "File entries must have a name");
static::assert(isset($info['time']), "File entries must have a time");
static::assert(isset($info['size']), "File entries must have a size");
static::assert(isset($info['hash']), "File entries must have a hash");
}
return true;
@ -174,41 +174,41 @@ class ListFiles extends AbstractTest
{
$listing = $s3->getBucket($options['bucket'], 'list_deeper/test_', null, 1);
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing)));
$files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'list_deeper/test_', array_shift($files));
self::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(is_array($continued), "The continued files listing must be an array");
static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued);
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 3, "I am expecting to see 3 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");
self::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_one.dat', $listing), "File test_one.dat not in listing");
static::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_three.dat', $listing), "File test_three.dat not in listing");
// 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");
self::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");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
static::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing");
static::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing");
static::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.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
self::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/seven.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)
{
self::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time");
self::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['name']), "File entries must have a name");
static::assert(isset($info['time']), "File entries must have a time");
static::assert(isset($info['size']), "File entries must have a size");
static::assert(isset($info['hash']), "File entries must have a hash");
}
return true;
@ -224,42 +224,42 @@ class ListFiles extends AbstractTest
*/
$listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, 1);
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 1, sprintf("I am expecting to see 1 files, %s seen", count($listing)));
$files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', array_shift($files));
self::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(is_array($continued), "The continued files listing must be an array");
static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued);
self::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(is_array($listing), "The files listing must be an array");
static::assert(count($listing) == 3, "I am expecting to see 3 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");
self::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_four.dat', $listing), "File listtest_four.dat not in listing");
static::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_six.dat', $listing), "File listtest_six.dat not in listing");
// 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");
self::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");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
static::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat in listing");
static::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat in listing");
static::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.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
self::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/seven.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)
{
self::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time");
self::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['name']), "File entries must have a name");
static::assert(isset($info['time']), "File entries must have a time");
static::assert(isset($info['size']), "File entries must have a size");
static::assert(isset($info['hash']), "File entries must have a hash");
}
return true;
@ -269,37 +269,37 @@ class ListFiles extends AbstractTest
{
$listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, null, '/', true);
self::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(is_array($listing), "The files listing must be an array");
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
self::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");
self::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_four.dat', $listing), "File listtest_four.dat not in listing");
static::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_six.dat', $listing), "File listtest_six.dat 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
self::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/seven.dat', $listing), "File seven.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
self::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");
self::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/spam.dat', $listing), "File spam.dat in listing");
static::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_two.dat', $listing), "File test_two.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)
{
if (substr($fileName, -1) !== '/')
{
self::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time");
self::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['name']), "File entries must have a name");
static::assert(isset($info['time']), "File entries must have a time");
static::assert(isset($info['size']), "File entries must have a size");
static::assert(isset($info['hash']), "File entries must have a hash");
}
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
/**
* 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\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\S3\Connector;
class Multipart extends BigFiles
{
public static function setup(Connector $s3, array $options): void
{
self::$multipart = true;
static::$multipart = true;
parent::setup($s3, $options);
}
@ -20,7 +26,7 @@ class Multipart extends BigFiles
$result = parent::upload5MBString($s3, $options);
$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;
}
@ -30,7 +36,7 @@ class Multipart extends BigFiles
$result = parent::upload6MBString($s3, $options);
$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;
}
@ -40,7 +46,7 @@ class Multipart extends BigFiles
$result = parent::upload10MBString($s3, $options);
$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;
}
@ -50,7 +56,7 @@ class Multipart extends BigFiles
$result = parent::upload11MBString($s3, $options);
$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;
}
@ -60,7 +66,7 @@ class Multipart extends BigFiles
$result = parent::upload5MBFile($s3, $options);
$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;
}
@ -70,7 +76,7 @@ class Multipart extends BigFiles
$result = parent::upload6MBFile($s3, $options);
$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;
}
@ -80,7 +86,7 @@ class Multipart extends BigFiles
$result = parent::upload10MBFile($s3, $options);
$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;
}
@ -90,7 +96,7 @@ class Multipart extends BigFiles
$result = parent::upload11MBFile($s3, $options);
$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;
}

View File

@ -3,33 +3,33 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Acl;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\S3\Acl;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
use RuntimeException;
class SignedURLs extends AbstractTest
{
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
{
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
{
$tempData = self::getRandomData(AbstractTest::TEN_KB);
$tempData = static::getRandomData(AbstractTest::TEN_KB);
$input = Input::createFromData($tempData);
$uri = 'test.' . md5(microtime(false)) . '.dat';
@ -52,7 +52,7 @@ class SignedURLs extends AbstractTest
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;
}

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
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* 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
{
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
{
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
{
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
{
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
{
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
{
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
@ -71,7 +71,7 @@ class SmallFiles extends AbstractTest
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create a file with random data
$sourceFile = self::createFile($size);
$sourceFile = static::createFile($size);
// Upload the file. Throws exception if it fails.
$bucket = $options['bucket'];
@ -83,14 +83,14 @@ class SmallFiles extends AbstractTest
$result = true;
// Should I download the file and compare its contents?
if (self::$downloadAfter)
if (static::$downloadAfter)
{
// Donwload the data. Throws exception if it fails.
$downloadedFile = tempnam(self::getTempFolder(), 'as3');
$downloadedFile = tempnam(static::getTempFolder(), 'as3');
$s3->getObject($bucket, $uri, $downloadedFile);
// Compare the file contents.
$result = self::areFilesEqual($sourceFile, $downloadedFile);
$result = static::areFilesEqual($sourceFile, $downloadedFile);
}
// Remove the local files
@ -98,7 +98,7 @@ class SmallFiles extends AbstractTest
@unlink($downloadedFile);
// Should I delete the remotely stored file?
if (self::$deleteRemote)
if (static::$deleteRemote)
{
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
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
@ -21,7 +21,7 @@ class SmallFilesNoDelete extends SmallFiles
{
public static function setup(Connector $s3, array $options): void
{
self::$deleteRemote = false;
static::$deleteRemote = false;
parent::setup($s3, $options);
}

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\S3\Connector;
/**
* 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
{
self::$deleteRemote = false;
self::$downloadAfter = false;
static::$deleteRemote = false;
static::$downloadAfter = false;
parent::setup($s3, $options);
}

View File

@ -3,15 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* 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);
// Create some random data to upload
$sourceData = self::getRandomData($size);
$sourceData = static::getRandomData($size);
// Upload the data. Throws exception if it fails.
$bucket = $options['bucket'];
@ -39,15 +38,15 @@ class SmallInlineFiles extends SmallFiles
$result = true;
// Should I download the file and compare its contents with my random data?
if (self::$downloadAfter)
if (static::$downloadAfter)
{
$downloadedData = $s3->getObject($bucket, $uri);
$result = self::areStringsEqual($sourceData, $downloadedData);
$result = static::areStringsEqual($sourceData, $downloadedData);
}
// Should I delete the remotely stored file?
if (self::$deleteRemote)
if (static::$deleteRemote)
{
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
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
@ -21,7 +21,7 @@ class SmallInlineFilesNoDelete extends SmallInlineFiles
{
public static function setup(Connector $s3, array $options): void
{
self:: $deleteRemote = false;
static:: $deleteRemote = false;
parent::setup($s3, $options);
}

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\S3\Connector;
/**
* 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
{
self::$deleteRemote = false;
self::$downloadAfter = false;
static::$deleteRemote = false;
static::$downloadAfter = false;
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
*
* @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
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Acl;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\Engine\Postproc\Connector\S3v4\StorageClass;
use Akeeba\S3\Acl;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
use Akeeba\S3\StorageClass;
class StorageClasses extends AbstractTest
{
@ -23,12 +23,12 @@ class StorageClasses extends AbstractTest
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
{
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)
@ -38,7 +38,7 @@ class StorageClasses extends AbstractTest
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload
$sourceData = self::getRandomData($size);
$sourceData = static::getRandomData($size);
// Upload the data. Throws exception if it fails.
$bucket = $options['bucket'];
@ -54,15 +54,15 @@ class StorageClasses extends AbstractTest
$result = true;
// Should I download the file and compare its contents with my random data?
if (self::$downloadAfter)
if (static::$downloadAfter)
{
$downloadedData = $s3->getObject($bucket, $uri);
$result = self::areStringsEqual($sourceData, $downloadedData);
$result = static::areStringsEqual($sourceData, $downloadedData);
}
// Should I delete the remotely stored file?
if (self::$deleteRemote)
if (static::$deleteRemote)
{
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);

View File

@ -3,10 +3,12 @@
* Akeeba Engine
*
* @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
*/
// 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
define('DEFAULT_ACCESS_KEY', 'your s3 access key');
// Default Amazon S3 Secret Key
@ -23,6 +25,8 @@ define('DEFAULT_DUALSTACK', false);
define('DEFAULT_PATH_ACCESS', false);
// Should I use SSL by default?
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
@ -33,7 +37,9 @@ $standardTests = [
'BucketsList',
'BucketLocation',
'SmallFiles',
'HeadObject',
'SmallInlineFiles',
'SmallInlineXMLFiles',
'SignedURLs',
'StorageClasses',
'ListFiles',

View File

@ -3,13 +3,13 @@
* Akeeba Engine
*
* @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
*/
use Akeeba\Engine\Postproc\Connector\S3v4\Configuration;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
use Akeeba\S3\Configuration;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
// Necessary for including the library
define('AKEEBAENGINE', 1);
@ -167,7 +167,7 @@ foreach ($testConfigurations as $description => $setup)
'dualstack' => DEFAULT_DUALSTACK,
'path_access' => DEFAULT_PATH_ACCESS,
'ssl' => DEFAULT_SSL,
'endpoint' => null,
'endpoint' => defined('DEFAULT_ENDPOINT') ? constant('DEFAULT_ENDPOINT') : null,
], $setup['configuration']);
// Extract the test classes/methods to run
@ -185,15 +185,21 @@ foreach ($testConfigurations as $description => $setup)
// Create the S3 configuration object
$s3Configuration = new Configuration($configOptions['access'], $configOptions['secret'], $configOptions['signature'], $configOptions['region']);
$s3Configuration->setUseDualstackUrl($configOptions['dualstack']);
$s3Configuration->setUseLegacyPathStyle($configOptions['path_access']);
$s3Configuration->setSSL($configOptions['ssl']);
$s3Configuration->setRegion($configOptions['region']);
$s3Configuration->setSignatureMethod($configOptions['signature']);
if (!is_null($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
$s3Connector = new Connector($s3Configuration);

View File

@ -3,29 +3,29 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4;
namespace Akeeba\S3;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
/**
* Shortcuts to often used access control privileges
*/
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
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4;
namespace Akeeba\S3;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
/**
* Holds the Amazon S3 confiugration credentials
@ -199,6 +199,8 @@ class Configuration
throw new Exception\InvalidSignatureMethod;
}
$this->signatureMethod = $signatureMethod;
// If you switch to v2 signatures we unset the region.
if ($signatureMethod == 'v2')
{
@ -214,13 +216,7 @@ class Configuration
$this->setUseLegacyPathStyle(false);
}
} else {
if (empty($this->getRegion())) {
$this->setRegion('us-east-1');
}
}
$this->signatureMethod = $signatureMethod;
}
/**

View File

@ -3,22 +3,22 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4;
namespace Akeeba\S3;
// Protection against direct access
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotDeleteFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetBucket;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotListBuckets;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotOpenFileForWrite;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error;
use Akeeba\S3\Exception\CannotDeleteFile;
use Akeeba\S3\Exception\CannotGetBucket;
use Akeeba\S3\Exception\CannotGetFile;
use Akeeba\S3\Exception\CannotListBuckets;
use Akeeba\S3\Exception\CannotOpenFileForWrite;
use Akeeba\S3\Exception\CannotPutFile;
use Akeeba\S3\Response\Error;
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
class Connector
{
@ -81,7 +81,10 @@ class Connector
if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference()))))
{
throw new CannotPutFile('Missing input parameters', 0);
if (substr($uri, -1) !== '/')
{
throw new CannotPutFile('Missing input parameters', 0);
}
}
// We need to post with Content-Length and Content-Type, MD5 is optional
@ -169,7 +172,7 @@ class Connector
if (!is_resource($saveTo) && is_string($saveTo))
{
$fp = @fopen($saveTo, 'wb');
$fp = @fopen($saveTo, 'w');
if ($fp === false)
{
@ -193,6 +196,53 @@ class Connector
$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();
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
@ -206,20 +256,21 @@ class Connector
if ($response->error->isError())
{
throw new CannotGetFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s\n\nDebug info:\n%s",
$response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)),
$response->error->getCode()
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;
return $response->getHeaders();
}
/**
* Delete an object
*
@ -244,9 +295,13 @@ class Connector
if ($response->error->isError())
{
throw new CannotDeleteFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s",
$response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
sprintf(
__METHOD__ . "({%s}, {%s}): [%s] %s",
$bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage()
)
);
}
}
@ -358,8 +413,7 @@ class Connector
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
);
}
@ -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
{
$request = new Request('GET', $bucket, '', $this->configuration);
$internalResult = $this->internalGetBucket($bucket, $prefix, $marker, $maxKeys, $delimiter, $returnCommonPrefixes);
if (!empty($prefix))
{
$request->setParameter('prefix', $prefix);
}
/**
* @var array $objects
* @var ?string $nextMarker
*/
extract($internalResult);
unset($internalResult);
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()),
$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)
// 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))
{
do
{
$request = new Request('GET', $bucket, '', $this->configuration);
$internalResult = $this->internalGetBucket($bucket, $prefix, $nextMarker, $maxKeys, $delimiter, $returnCommonPrefixes);
if (!empty($prefix))
{
$request->setParameter('prefix', $prefix);
}
$nextMarker = $internalResult['nextMarker'];
$objects = array_merge($objects, $internalResult['objects']);
$request->setParameter('marker', $nextMarker);
unset($internalResult);
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
try
{
$response = $request->getResponse();
}
catch (\Exception $e)
// If the last call did not return a nextMarker I am done iterating.
if ($nextMarker === null)
{
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)
{
$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;
}
break;
}
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->NextMarker))
{
$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);
} while (true);
}
if (!is_null($maxKeys))
if ($maxKeys !== null)
{
$results = array_splice($results, 0, $maxKeys);
return array_splice($objects, 0, $maxKeys);
}
return $results;
return $objects;
}
/**
@ -594,8 +527,7 @@ class Connector
if ($response->error->isError())
{
throw new CannotListBuckets(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
);
}
@ -691,7 +623,12 @@ class Connector
if ($response->error->isError())
{
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;
}
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
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;
use RuntimeException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;
use InvalidArgumentException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use Exception;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception;
namespace Akeeba\S3\Exception;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
use LogicException;

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4;
namespace Akeeba\S3;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
/**
* Defines an input source for PUT/POST requests to Amazon S3
@ -20,17 +20,17 @@ class Input
/**
* Input type: resource
*/
const INPUT_RESOURCE = 1;
public const INPUT_RESOURCE = 1;
/**
* Input type: file
*/
const INPUT_FILE = 2;
public const INPUT_FILE = 2;
/**
* Input type: raw data
*/
const INPUT_DATA = 3;
public const INPUT_DATA = 3;
/**
* File pointer, in case we have a resource
@ -177,7 +177,13 @@ class Input
{
if (is_resource($this->fp))
{
@fclose($this->fp);
try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
}
}
@ -258,10 +264,16 @@ class Input
if (is_resource($this->fp))
{
@fclose($this->fp);
try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
}
$this->fp = @fopen($file, 'rb');
$this->fp = @fopen($file, 'r');
if ($this->fp === false)
{
@ -295,7 +307,13 @@ class Input
if (is_resource($this->fp))
{
@fclose($this->fp);
try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
}
$this->file = null;
@ -329,7 +347,13 @@ class Input
if (is_resource($this->fp))
{
@fclose($this->fp);
try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
}
$this->file = null;
@ -450,7 +474,7 @@ class Input
*/
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())
{
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;
case self::INPUT_FILE:
@ -635,7 +659,7 @@ class Input
$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
*
* @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
*/
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
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
class Request
@ -142,6 +142,12 @@ class Request
// The date must always be added as a header
$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
$token = $this->configuration->getToken();
@ -367,7 +373,7 @@ class Request
*
* @return Response
*/
public function getResponse(): Response
public function getResponse(bool $rawResponse = false): Response
{
$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
* S3 SSL certificates are set up.
*/
$isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com') ||
substr($this->headers['Host'], -16) == 'amazonaws.com.cn';
$isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com')
|| substr(
$this->headers['Host'], -16
) == 'amazonaws.com.cn';
$tooManyDots = substr_count($this->headers['Host'], '.') > 4;
$verifyHost = ($isAmazonS3 && $tooManyDots) ? 0 : 2;
@ -429,6 +437,27 @@ class Request
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->preProcessHeaders($this->headers, $this->amzHeaders);
@ -482,7 +511,7 @@ class Request
$data = $this->input->getDataReference();
if (strlen($data))
if (strlen($data ?? ''))
{
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
@ -538,12 +567,18 @@ class Request
@curl_close($curl);
// Set the body data
$this->response->finaliseBody();
$this->response->finaliseBody($rawResponse);
// Clean up file resources
if (!is_null($this->fp) && is_resource($this->fp))
{
fclose($this->fp);
try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
}
return $this->response;
@ -560,7 +595,7 @@ class Request
*/
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);
}
@ -573,7 +608,7 @@ class Request
/**
* cURL header callback
*
* @param resource $curl cURL resource
* @param resource $curl cURL resource
* @param string &$data Data
*
* @return int Length in bytes
@ -592,7 +627,15 @@ class Request
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))
{
@ -609,10 +652,12 @@ class Request
break;
case 'etag':
$this->response->setHeader('hash', $value[0] == '"' ? substr($value, 1, -1) : $value);
$this->response->setHeader('hash', trim($value, '"'));
break;
default:
$this->response->setHeader(strtolower($header), is_numeric($value) ? (int) $value : $value);
if (preg_match('/^x-amz-meta-.*$/', $header))
{
$this->setHeader($header, is_numeric($value) ? (int) $value : $value);
@ -652,13 +697,12 @@ class Request
$query = substr($query, 0, -1);
$this->uri .= $query;
if (array_key_exists('acl', $this->parameters) ||
array_key_exists('location', $this->parameters) ||
array_key_exists('torrent', $this->parameters) ||
array_key_exists('logging', $this->parameters) ||
array_key_exists('uploads', $this->parameters) ||
array_key_exists('uploadId', $this->parameters) ||
array_key_exists('partNumber', $this->parameters)
if (array_key_exists('acl', $this->parameters) || array_key_exists('location', $this->parameters)
|| array_key_exists('torrent', $this->parameters)
|| array_key_exists('logging', $this->parameters)
|| array_key_exists('uploads', $this->parameters)
|| array_key_exists('uploadId', $this->parameters)
|| array_key_exists('partNumber', $this->parameters)
)
{
$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
* mapping can be found in https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region
*
@ -728,25 +774,27 @@ class Request
*
* v4 signing does NOT support non-Amazon endpoints.
*/
// Most endpoints: s3-REGION.amazonaws.com
$regionalEndpoint = $region . '.amazonaws.com';
// Exception: China
if (substr($region, 0, 3) == 'cn-')
if (in_array($endpoint, ['s3.amazonaws.com', 'amazonaws.com.cn']))
{
// Chinese endpoint, e.g.: s3.cn-north-1.amazonaws.com.cn
$regionalEndpoint = $regionalEndpoint . '.cn';
}
// Most endpoints: s3-REGION.amazonaws.com
$regionalEndpoint = $region . '.amazonaws.com';
// If dual-stack URLs are enabled then prepend the endpoint
if ($configuration->getDualstackUrl())
{
$endpoint = 's3.dualstack.' . $regionalEndpoint;
}
else
{
$endpoint = 's3.' . $regionalEndpoint;
// Exception: China
if (substr($region, 0, 3) == 'cn-')
{
// Chinese endpoint, e.g.: s3.cn-north-1.amazonaws.com.cn
$regionalEndpoint = $regionalEndpoint . '.cn';
}
// If dual-stack URLs are enabled then prepend the endpoint
if ($configuration->getDualstackUrl())
{
$endpoint = 's3.dualstack.' . $regionalEndpoint;
}
else
{
$endpoint = 's3.' . $regionalEndpoint;
}
}
// Legacy path style access: return just the endpoint

View File

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

View File

@ -3,14 +3,14 @@
* Akeeba Engine
*
* @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
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Response;
namespace Akeeba\S3\Response;
// Protection against direct access
defined('AKEEBAENGINE') or die();
defined('AKEEBAENGINE') || die();
/**
* S3 response error object

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