Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Languages/en_US/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@
$txt['installed_packages'] = 'Installed Packages';
$txt['package_file_perms'] = 'File Permissions';
$txt['package_settings'] = 'Options';
$txt['package_services'] = 'Service Access';
$txt['themeadmin_admin_title'] = 'Manage and Install';
$txt['themeadmin_list_title'] = 'Theme Settings';
$txt['themeadmin_reset_title'] = 'Member Options';
Expand Down
15 changes: 15 additions & 0 deletions Languages/en_US/Packages.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@

$txt['package_install_options'] = 'Installation Options';
$txt['package_install_options_desc'] = 'Set various options for how the package manager installs modifications, including backups and FTP access';

$txt['package_services'] = 'Service Access';
$txt['package_services_desc'] = 'See which services each installed modification provides and uses, and withdraw that access';
$txt['package_services_info'] = 'Services are the parts of the forum a modification asks to work with. A modification declares them in its package, you approve them when you install it, and you can withdraw them here without uninstalling anything. This describes what a modification asks for; it does not stop a badly behaved one from reaching the forum in other ways.';
$txt['package_services_none'] = 'No installed modification provides or uses any service.';
$txt['package_service_provides'] = 'Provides';
$txt['package_service_uses'] = 'Uses';
$txt['package_services_status'] = 'Access';
$txt['package_services_granted'] = 'Allowed';
$txt['package_services_revoked'] = 'Withdrawn';
$txt['package_services_grant'] = 'Allow';
$txt['package_services_revoke'] = 'Withdraw';
$txt['package_services_not_registered_revoked'] = 'not available, access withdrawn';
$txt['package_services_not_registered_taken'] = 'not available, another modification provides this service';
$txt['package_services_core'] = 'Services provided by SMF';
$txt['package_install_options_ftp_why'] = 'Using the package manager’s FTP functionality is the easiest way to avoid having to manually chmod the files writable through FTP yourself for the package manager to work.<br>Here you can set the default values for some fields.';
$txt['package_install_options_ftp_server'] = 'FTP Server';
$txt['package_install_options_ftp_port'] = 'Port';
Expand Down
3 changes: 3 additions & 0 deletions Sources/Actions/Admin/ACP.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class ACP implements ActionInterface, Routable
'options' => [
'label' => 'package_settings',
],
'services' => [
'label' => 'package_services',
],
],
],
'search' => [
Expand Down
149 changes: 149 additions & 0 deletions Sources/Infrastructure/PackageServices.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2026 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 3.0 Alpha 5-dev
*/

declare(strict_types=1);

namespace SMF\Infrastructure;

use SMF\Config;
use SMF\Utils;

/**
* What each installed package does with services.
*
* A package says in its package-info.xml which services it provides and which
* it wants to use. That is recorded when the package is installed, so the
* answer is a setting rather than a trip through the package files, and the
* administrator can take the access away again without uninstalling the mod.
*/
class PackageServices
{
/***********************
* Public static methods
***********************/

/**
* Gets what every installed package does with services.
*
* @return array Each package's name, the services it provides, the ones it
* uses, and whether it is allowed to, keyed by package ID.
*/
public static function all(): array
{
if (empty(Config::$modSettings['package_services'])) {
return [];
}

$manifests = Utils::jsonDecode(Config::$modSettings['package_services'], true);

return \is_array($manifests) ? $manifests : [];
}

/**
* Gets what one package does with services.
*
* @param string $package_id The package's ID.
* @return array The package's entry, or an empty array if it has none.
*/
public static function get(string $package_id): array
{
return self::all()[$package_id] ?? [];
}

/**
* Records what a package does with services.
*
* Access is granted at this point, because this is where the administrator
* installed the package after being shown what it asked for.
*
* @param string $package_id The package's ID.
* @param string $name The package's name, as the admin knows it.
* @param array $provides Service IDs the package registers.
* @param array $uses Service IDs the package wants to use.
*/
public static function record(string $package_id, string $name, array $provides, array $uses): void
{
if ($provides === [] && $uses === []) {
self::forget($package_id);

return;
}

$manifests = self::all();

$manifests[$package_id] = [
'name' => $name,
'provides' => array_values(array_unique($provides)),
'uses' => array_values(array_unique($uses)),
'granted' => true,
];

self::save($manifests);
}

/**
* Forgets a package, which is what uninstalling it means here.
*
* @param string $package_id The package's ID.
*/
public static function forget(string $package_id): void
{
$manifests = self::all();

if (!isset($manifests[$package_id])) {
return;
}

unset($manifests[$package_id]);

self::save($manifests);
}

/**
* Allows or refuses a package the services it asked for.
*
* A package that has been refused keeps its entry, so the administrator can
* see what it wanted and can change their mind.
*
* @param string $package_id The package's ID.
* @param bool $granted Whether the package may use services.
*/
public static function setGranted(string $package_id, bool $granted): void
{
$manifests = self::all();

if (!isset($manifests[$package_id])) {
return;
}

$manifests[$package_id]['granted'] = $granted;

self::save($manifests);
}

/*************************
* Internal static methods
*************************/

/**
* Writes the manifests back to the settings.
*
* @param array $manifests The manifests of every package.
*/
protected static function save(array $manifests): void
{
Config::updateModSettings([
'package_services' => $manifests === [] ? '' : Utils::jsonEncode($manifests),
]);
}
}
26 changes: 26 additions & 0 deletions Sources/Infrastructure/ServiceAccessException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2026 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 3.0 Alpha 5-dev
*/

declare(strict_types=1);

namespace SMF\Infrastructure;

use Psr\Container\ContainerExceptionInterface;

/**
* Thrown when something asks for a service it has no access to.
*
* The services a package may use are the ones it declared in its
* package-info.xml and the administrator granted when installing it.
*/
class ServiceAccessException extends \RuntimeException implements ContainerExceptionInterface {}
26 changes: 26 additions & 0 deletions Sources/Infrastructure/ServiceNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2026 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 3.0 Alpha 5-dev
*/

declare(strict_types=1);

namespace SMF\Infrastructure;

use Psr\Container\NotFoundExceptionInterface;

/**
* Thrown when nothing provides the service that was asked for.
*
* Either no package provides it, or the one that did has had its access
* withdrawn, which looks the same to whoever asked.
*/
class ServiceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface {}
Loading
Loading