1
0
mirror of https://github.com/flarum/core.git synced 2025-08-17 13:54:18 +02:00

Compare commits

...

9 Commits

Author SHA1 Message Date
StyleCI Bot
70135459ce Apply fixes from StyleCI 2023-03-26 20:07:37 +00:00
Daniël Klabbers
3c82a887a8 fix: only allow no one if configured, fallback to admin otherwise 2023-03-26 22:07:08 +02:00
Daniël Klabbers
7dcbbf29ab feat(permission): add the no one permission
One thing that has been very frustrating while developing extensions is
that you cannot easily create a feature that you would like to allow
disabling through the permission grid.

This Proof of Concept PR is a question, whether I'm the only one running
into this limitation for Flarum.

It does:

- introduce a fake permission, similar to Guest
- removes the whole permission entry from the db when selected
- restores the permission visually on the frontend

It has to:

- confirm admins also cannot use the permission if selected
- not break v1.x functionality or be moved to v2.0
- appeal to core and community
2023-03-26 10:03:09 +02:00
flarum-bot
bddc9d96f2 Bundled output for commit d684248492
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2023-03-23 08:10:20 +00:00
IanM
d684248492 fix: empty string displayed as SelectDropdown title (#3773)
* fix: empty string displayed as SelectDropdown title

* chore: remove import

* chore: ts-ignore

* Update framework/core/js/src/common/components/SelectDropdown.tsx

Co-authored-by: David Wheatley <david@davwheat.dev>

---------

Co-authored-by: David Wheatley <david@davwheat.dev>
2023-03-23 08:02:59 +00:00
Rafael Horvat
85b63681ae Fix: Integrity constraint violation: 1052 Column 'id' in where clause is ambiguous (#3772) 2023-03-21 10:32:23 +00:00
Sami Mazouz
8372363cc2 feat: conditional extenders (#3759) 2023-03-14 21:53:16 +01:00
Ngô Quốc Đạt
a6a067ad48 chore: update to PHP 8.2 in frontend workflow (#3755) 2023-03-12 13:58:06 +01:00
Sami Mazouz
241eba4d0c chore: mark start of 1.8.0 development
Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>
2023-03-12 13:56:39 +01:00
24 changed files with 69486 additions and 1150 deletions

View File

@@ -118,7 +118,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
php-version: '8.2'
extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip
tools: composer:v2

View File

@@ -103,7 +103,7 @@ class UpdateMentionsMetadataWhenVisible
$post->unsetRelation('mentionsGroups');
$users = User::whereHas('groups', function ($query) use ($mentioned) {
$query->whereIn('id', $mentioned);
$query->whereIn('groups.id', $mentioned);
})
->get()
->filter(function (User $user) use ($post) {

30362
framework/core/js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

37513
framework/core/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -52,11 +52,14 @@ export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownA
groupIds = filterByRequiredPermissions(groupIds, this.attrs.permission);
const no_one = groupIds.length === 0;
const everyone = groupIds.includes(Group.GUEST_ID);
const members = groupIds.includes(Group.MEMBER_ID);
const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!;
if (everyone) {
if (this.attrs.allowNoOne && no_one) {
this.attrs.label = Badge.component({ icon: 'fas fa-user-slash' });
} else if (everyone) {
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
} else if (members) {
this.attrs.label = Badge.component({ icon: 'fas fa-user' });
@@ -65,6 +68,17 @@ export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownA
}
if (this.showing) {
if (this.attrs.allowNoOne) {
children.push(
Button.component(
{
icon: no_one ? 'fas fa-check' : true,
onclick: () => this.save([Group.NO_ONE_ID]),
},
[Badge.component({ icon: 'fas fa-user-slash' }), ' ', app.translator.trans('core.admin.permissions_controls.no_one_button')]
)
);
}
if (this.attrs.allowGuest) {
children.push(
Button.component(

View File

@@ -12,6 +12,7 @@ export interface PermissionConfig {
icon: string;
label: Mithril.Children;
allowGuest?: boolean;
allowNoOne?: boolean;
}
export interface PermissionSetting {
@@ -435,6 +436,7 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
return PermissionDropdown.component({
permission: item.permission,
allowGuest: item.allowGuest,
allowNoOne: item.allowNoOne,
});
}

View File

@@ -1,6 +1,5 @@
import Dropdown, { IDropdownAttrs } from './Dropdown';
import icon from '../helpers/icon';
import extractText from '../utils/extractText';
import classList from '../utils/classList';
import type Component from '../Component';
import type Mithril from 'mithril';
@@ -50,7 +49,8 @@ export default class SelectDropdown<CustomAttrs extends ISelectDropdownAttrs = I
const activeChild = children.find(isActive);
let label = (activeChild && typeof activeChild === 'object' && 'children' in activeChild && activeChild.children) || this.attrs.defaultLabel;
label = extractText(label);
// @ts-ignore
if (Array.isArray(label)) label = label[0];
return [<span className="Button-label">{label}</span>, this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : null];
}

View File

@@ -1,6 +1,7 @@
import Model from '../Model';
export default class Group extends Model {
static NO_ONE_ID = '-1';
static ADMINISTRATOR_ID = '1';
static GUEST_ID = '2';
static MEMBER_ID = '3';

View File

@@ -235,6 +235,7 @@ core:
members_button: => core.group.members
signup_closed_button: Closed
signup_open_button: Open
no_one_button: No one
# These translations are used generically in setting fields.
settings:

View File

@@ -9,6 +9,7 @@
namespace Flarum\Api\Controller;
use Flarum\Group\Group;
use Flarum\Group\Permission;
use Flarum\Http\RequestUtil;
use Illuminate\Support\Arr;
@@ -32,6 +33,11 @@ class SetPermissionController implements RequestHandlerInterface
Permission::where('permission', $permission)->delete();
// Permission set to no one.
if (count($groupIds) === 1 && intval($groupIds[0]) === Group::NO_ONE_ID) {
return new EmptyResponse(204);
}
Permission::insert(array_map(function ($groupId) use ($permission) {
return [
'permission' => $permission,

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Illuminate\Contracts\Container\Container;
class Conditional implements ExtenderInterface
{
/**
* @var array<array{condition: bool|callable, extenders: ExtenderInterface[]}>
*/
protected $conditions = [];
/**
* @param ExtenderInterface[] $extenders
*/
public function whenExtensionEnabled(string $extensionId, array $extenders): self
{
return $this->when(function (ExtensionManager $extensions) use ($extensionId) {
return $extensions->isEnabled($extensionId);
}, $extenders);
}
/**
* @param bool|callable $condition
* @param ExtenderInterface[] $extenders
*/
public function when($condition, array $extenders): self
{
$this->conditions[] = [
'condition' => $condition,
'extenders' => $extenders,
];
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
foreach ($this->conditions as $condition) {
if (is_callable($condition['condition'])) {
$condition['condition'] = $container->call($condition['condition']);
}
if ($condition['condition']) {
foreach ($condition['extenders'] as $extender) {
$extender->extend($container, $extension);
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Extend;
use Flarum\Extension\Extension;
use Flarum\User\Access\AdminPolicy;
use Illuminate\Contracts\Container\Container;
class Permission implements ExtenderInterface
{
public function extend(Container $container, Extension $extension = null)
{
// TODO: Implement extend() method.
}
public function allowsNoOne(string $permission): self
{
AdminPolicy::allowNoOneOnPermission($permission);
return $this;
}
}

View File

@@ -414,19 +414,19 @@ class Extension implements Arrayable
$links['source'] = $sourceUrl;
}
if (($discussUrl = $this->composerJsonAttribute('support.forum'))) {
if ($discussUrl = $this->composerJsonAttribute('support.forum')) {
$links['discuss'] = $discussUrl;
}
if (($documentationUrl = $this->composerJsonAttribute('support.docs'))) {
if ($documentationUrl = $this->composerJsonAttribute('support.docs')) {
$links['documentation'] = $documentationUrl;
}
if (($websiteUrl = $this->composerJsonAttribute('homepage'))) {
if ($websiteUrl = $this->composerJsonAttribute('homepage')) {
$links['website'] = $websiteUrl;
}
if (($supportEmail = $this->composerJsonAttribute('support.email'))) {
if ($supportEmail = $this->composerJsonAttribute('support.email')) {
$links['support'] = "mailto:$supportEmail";
}

View File

@@ -21,7 +21,7 @@ class Application
*
* @var string
*/
const VERSION = '1.7.1';
const VERSION = '1.8.0-dev';
/**
* The IoC container for the Flarum application.

View File

@@ -32,6 +32,11 @@ class Group extends AbstractModel
use EventGeneratorTrait;
use ScopeVisibilityTrait;
/**
* The temporary ID for when no one can do something.
*/
const NO_ONE_ID = -1;
/**
* The ID of the administrator group.
*/

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\User\Access;
use Flarum\User\User;
class AdminPolicy extends AbstractPolicy
{
public static array $allowNoOnePermissions = [];
/**
* @param string|array<string> $permissions
* @return void
*/
public static function allowNoOneOnPermission($permissions)
{
self::$allowNoOnePermissions[] = array_merge(
static::$allowNoOnePermissions,
(array) $permissions
);
}
/**
* @param User $actor
* @param string $ability
* @return bool|null
*/
public function can(User $actor, $ability)
{
if (! in_array($ability, static::$allowNoOnePermissions)
&& $actor->isAdmin()) {
return $this->forceAllow();
}
}
}

View File

@@ -65,7 +65,7 @@ class Gate
$appliedPolicies = [];
if ($model) {
$modelClasses = is_string($model) ? [$model] : array_merge(class_parents(($model)), [get_class($model)]);
$modelClasses = is_string($model) ? [$model] : array_merge(class_parents($model), [get_class($model)]);
foreach ($modelClasses as $class) {
$appliedPolicies = array_merge($appliedPolicies, $this->getPolicies($class));
@@ -87,11 +87,7 @@ class Gate
// If no policy covered this permission query, we will only grant
// the permission if the actor's groups have it. Otherwise, we will
// not allow the user to perform this action.
if ($actor->isAdmin() || ($actor->hasPermission($ability))) {
return true;
}
return false;
return $actor->hasPermission($ability);
}
/**

View File

@@ -380,10 +380,6 @@ class User extends AbstractModel
*/
public function hasPermission($permission)
{
if ($this->isAdmin()) {
return true;
}
return in_array($permission, $this->getPermissions());
}
@@ -396,10 +392,6 @@ class User extends AbstractModel
*/
public function hasPermissionLike($match)
{
if ($this->isAdmin()) {
return true;
}
foreach ($this->getPermissions() as $permission) {
if (substr($permission, -strlen($match)) === $match) {
return true;

View File

@@ -20,6 +20,7 @@ use Flarum\Http\AccessToken;
use Flarum\Post\Access\PostPolicy;
use Flarum\Post\Post;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Access\AdminPolicy;
use Flarum\User\Access\ScopeUserVisibility;
use Flarum\User\DisplayName\DriverInterface;
use Flarum\User\DisplayName\UsernameDriver;
@@ -49,7 +50,7 @@ class UserServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.policies', function () {
return [
Access\AbstractPolicy::GLOBAL => [],
Access\AbstractPolicy::GLOBAL => [AdminPolicy::class],
AccessToken::class => [AccessTokenPolicy::class],
Discussion::class => [DiscussionPolicy::class],
Group::class => [GroupPolicy::class],

View File

@@ -0,0 +1,162 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Tests\integration\extenders;
use Exception;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Extend;
use Flarum\Extension\ExtensionManager;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
class ConditionalTest extends TestCase
{
use RetrievesAuthorizedUsers;
/** @test */
public function conditional_works_if_condition_is_primitive_true()
{
$this->extend(
(new Extend\Conditional())
->when(true, [
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(function () {
return [
'customConditionalAttribute' => true
];
})
])
);
$this->app();
$response = $this->send(
$this->request('GET', '/api', [
'authenticatedAs' => 1,
])
);
$payload = json_decode($response->getBody()->getContents(), true);
$this->assertArrayHasKey('customConditionalAttribute', $payload['data']['attributes']);
}
/** @test */
public function conditional_does_not_work_if_condition_is_primitive_false()
{
$this->extend(
(new Extend\Conditional())
->when(false, [
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(function () {
return [
'customConditionalAttribute' => true
];
})
])
);
$this->app();
$response = $this->send(
$this->request('GET', '/api', [
'authenticatedAs' => 1,
])
);
$payload = json_decode($response->getBody()->getContents(), true);
$this->assertArrayNotHasKey('customConditionalAttribute', $payload['data']['attributes']);
}
/** @test */
public function conditional_works_if_condition_is_callable_true()
{
$this->extend(
(new Extend\Conditional())
->when(function () {
return true;
}, [
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(function () {
return [
'customConditionalAttribute' => true
];
})
])
);
$this->app();
$response = $this->send(
$this->request('GET', '/api', [
'authenticatedAs' => 1,
])
);
$payload = json_decode($response->getBody()->getContents(), true);
$this->assertArrayHasKey('customConditionalAttribute', $payload['data']['attributes']);
}
/** @test */
public function conditional_does_not_work_if_condition_is_callable_false()
{
$this->extend(
(new Extend\Conditional())
->when(function () {
return false;
}, [
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(function () {
return [
'customConditionalAttribute' => true
];
})
])
);
$this->app();
$response = $this->send(
$this->request('GET', '/api', [
'authenticatedAs' => 1,
])
);
$payload = json_decode($response->getBody()->getContents(), true);
$this->assertArrayNotHasKey('customConditionalAttribute', $payload['data']['attributes']);
}
/** @test */
public function conditional_injects_dependencies_to_condition_callable()
{
$this->expectNotToPerformAssertions();
$this->extend(
(new Extend\Conditional())
->when(function (?ExtensionManager $extensions) {
if (! $extensions) {
throw new Exception('ExtensionManager not injected');
}
}, [
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(function () {
return [
'customConditionalAttribute' => true
];
})
])
);
$this->app();
}
}

View File

@@ -42,7 +42,7 @@ class LocalesTest extends TestCase
public function custom_translation_exists_if_added()
{
$this->extend(
(new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales'))
new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales')
);
$this->app()->getContainer()->make('flarum.locales');
@@ -57,7 +57,7 @@ class LocalesTest extends TestCase
public function custom_translation_exists_if_added_with_intl_suffix()
{
$this->extend(
(new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales'))
new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales')
);
$this->app()->getContainer()->make('flarum.locales');
@@ -72,7 +72,7 @@ class LocalesTest extends TestCase
public function messageformat_works_in_translations()
{
$this->extend(
(new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales'))
new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales')
);
$this->app()->getContainer()->make('flarum.locales');
@@ -87,7 +87,7 @@ class LocalesTest extends TestCase
public function laravel_interface_methods_work()
{
$this->extend(
(new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales'))
new Extend\Locales(dirname(__FILE__, 3).'/fixtures/locales')
);
$this->app()->getContainer()->make('flarum.locales');

2387
yarn.lock

File diff suppressed because it is too large Load Diff