1
0
mirror of https://github.com/flarum/core.git synced 2025-08-13 20:04:24 +02:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Daniël Klabbers
fd371c1203 Release v0.1.0-beta.13 2020-05-04 12:52:03 +02:00
Franz Liedke
d72416cd8a Make two more tests compatible with PHPUnit 8 2020-05-02 15:35:18 +02:00
Franz Liedke
937ff1a0d5 Remove obsolete method 2020-05-02 15:34:03 +02:00
Alexander Skvortsov
097a87dbb6 Added simply confirmation popup for hiding / deleting posts (#2135) 2020-04-24 23:26:48 +02:00
Alexander Skvortsov
7794546845 Model extender: Fix inheritance (#2132)
This ensures that default values, date attributes and relationships are properly inherited, when we have deeper model class hierarchies.

This also adds test cases to ensure that inheritance order is honored for relationship and default attribute extender. As there's no way to remove date attributes, the order of evaluation there doesn't matter.
2020-04-24 21:17:31 +02:00
Franz Liedke
c43cc874ee Model extender: Add failing test
We determined that child classes are not properly affected when
extending the parent classes.

Refs #2100.
2020-04-24 17:54:30 +02:00
Franz Liedke
33cf94c192 Fix test to match its description
Refs #2100.
2020-04-24 17:31:08 +02:00
Franz Liedke
036e519865 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-04-24 14:56:37 +00:00
Franz Liedke
9386c91af9 Tweak model extender tests
- Format code
- Reorder methods
- Test a different scenario to avoid the use of sleep()

Refs #2100.
2020-04-24 16:55:04 +02:00
Franz Liedke
8306cef963 Clean up model extender
- Remove unused private attributes
- Complete docblocks
- Add scalar type hints
- Format code
- Reorder methods

Refs #2100.
2020-04-24 16:33:08 +02:00
Franz Liedke
51ea326959 Apply fixes from StyleCI
[ci skip] [skip ci]
2020-04-24 13:10:36 +00:00
Alexander Skvortsov
15bed971e6 Add model extender (#2100)
This covers default attribute values, date attributes and custom relationships.
2020-04-24 15:10:24 +02:00
Franz Liedke
c896cd8696 npm audit fix 2020-04-24 14:30:16 +02:00
flarum-bot
54ac83d0b6 Bundled output for commit 1592cd1013 [skip ci] 2020-04-22 21:38:57 +00:00
Franz Liedke
1592cd1013 CI: Shorten the lint job name 2020-04-22 23:37:37 +02:00
Alexander Skvortsov
6e8884f190 Implement hidden permission groups (#2129)
Only users that have the new `viewHiddenGroups` permissions will be able to see these groups.

You might want this when you want to give certain users special permissions, but don't want to make your authorization scheme public to regular users.

Co-authored-by: luceos <daniel+github@klabbers.email>
2020-04-21 17:49:53 +02:00
Franz Liedke
df8f73bd3d Statically access Flarum version everywhere
One less reason to inject the huge Application class.

Refs #2055.
2020-04-21 16:48:36 +02:00
Franz Liedke
3f0f89afb1 Use Container contract where easily possible
Less usages of the Application god-class simplifies splitting it up.

Refs #2055.
2020-04-21 16:48:06 +02:00
Franz Liedke
f0f301c5f4 Add compatiblity with Composer 2.0
- The structure of vendor/composer/installed.json will change.
- The same file will now contain the relative path to package locations.

References:
- https://github.com/composer/composer/blob/master/UPGRADE-2.0.md
- https://php.watch/articles/composer-2
2020-04-21 15:47:58 +02:00
Franz Liedke
3045bde167 Format code
- Early returns
- Comments
- Write variables only when needed

Refs #2020.
2020-04-19 16:53:52 +02:00
Robert Korulczyk
ee7a4627d8 Load only translations for enabled extensions from language packs (#2020)
fix #1837

Co-authored-by: Daniel Klabbers <daniel+git@klabbers.email>
2020-04-19 16:29:45 +02:00
Franz Liedke
b9fb92d49a Inline test class
Refs #1977.
2020-04-19 15:55:10 +02:00
Clark Winkelmann
b5accca957 Make AbstractPolicy compatible with both object and class as $model (#1977) 2020-04-19 15:52:59 +02:00
39 changed files with 1006 additions and 85 deletions

View File

@@ -1,4 +1,4 @@
name: Lint code
name: Lint
on:
push:
@@ -12,7 +12,7 @@ jobs:
prettier:
runs-on: ubuntu-latest
name: Lint JS code with Prettier
name: JS / Prettier
steps:
- uses: actions/checkout@master

View File

@@ -1,5 +1,50 @@
# Changelog
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
### Added
- Middleware extender (#2017, #2063, #2084)
- Console extender (#2057)
- CSRF extender (#2095)
- Event extender (#2097)
- Mail extender (#2012)
- Model extender (#2100)
- Show discussion start user as html class on post
- PHPUnit 8 compatibility.
- Composer 2 compatibility
- Permission groups can now be hidden (#2129)
- Confirmation popup when hiding or deleting posts (#2135)
### Changed
- Updated less.php dependency version to 3.0.
- All notifications now processed through the queue (#1931)
- Updated JS dependencies
- Simplified uploads, removing need to store intermediate files (#2117)
- Improved date handling for dates older than 1 year (#2034)
- Linting and automatic formatting for JS (#2099)
- Translation files from Language Packs are only loaded for extensions that are enabled (#2020)
### Fixed
- Users can no longer restore discussions hidden by others (#2037)
- Issues of the Modal not showing or auto hiding (#2080)
- Extensions page in admin showning columns incorrectly (#2111)
- Non dismissable modals can be dismissed using the ESC key (#1917)
- New post injected above unread sticky (#1868)
- New discussions not visible to users when using Pusher (#2077)
- Icons on admin permissions page (#2016, #2018)
- Notification bubble contrast on mobile with colored header (#2109)
- PostStreamScrubber click jumps back to first position (#1945)
- Loading state of Switch toggle component is hard to see (#2039, #1491)
- Allowing permission check to use class name based gate checks (#1977)
### Removed
- Zend compatibility bridge (#2010)
- SES mail support (#2011)
- Backward compatibility dropped for mail drivers
- Support for PHP 7.1
- Deprecated Flarum\Util\Str helper class
- Deprecated ConfigureMiddleware event
## [0.1.0-beta.12](https://github.com/flarum/core/compare/v0.1.0-beta.11.1...v0.1.0-beta.12)
### Added

4
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
js/package-lock.json generated
View File

@@ -4943,8 +4943,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
"resolved": ""
},
"tapable": {
"version": "1.1.3",

View File

@@ -3,6 +3,7 @@ import Button from '../../common/components/Button';
import Badge from '../../common/components/Badge';
import Group from '../../common/models/Group';
import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch';
/**
* The `EditGroupModal` component shows a modal dialog which allows the user
@@ -16,6 +17,7 @@ export default class EditGroupModal extends Modal {
this.namePlural = m.prop(this.group.namePlural() || '');
this.icon = m.prop(this.group.icon() || '');
this.color = m.prop(this.group.color() || '');
this.isHidden = m.prop(this.group.isHidden() || false);
}
className() {
@@ -89,6 +91,18 @@ export default class EditGroupModal extends Modal {
10
);
items.add(
'hidden',
<div className="Form-group">
{Switch.component({
state: !!Number(this.isHidden()),
children: app.translator.trans('core.admin.edit_group.hide_label'),
onchange: this.isHidden,
})}
</div>,
10
);
items.add(
'submit',
<div className="Form-group">
@@ -118,6 +132,7 @@ export default class EditGroupModal extends Modal {
namePlural: this.namePlural(),
color: this.color(),
icon: this.icon(),
isHidden: this.isHidden(),
};
}

View File

@@ -112,6 +112,16 @@ export default class PermissionGrid extends Component {
100
);
items.add(
'viewHiddenGroups',
{
icon: 'fas fa-users',
label: app.translator.trans('core.admin.permissions.view_hidden_groups_label'),
permission: 'viewHiddenGroups',
},
100
);
items.add(
'viewUserList',
{

View File

@@ -7,6 +7,7 @@ Object.assign(Group.prototype, {
namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'),
icon: Model.attribute('icon'),
isHidden: Model.attribute('isHidden'),
});
Group.ADMINISTRATOR_ID = '1';

View File

@@ -2,6 +2,7 @@ import EditPostComposer from '../components/EditPostComposer';
import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator';
import ItemList from '../../common/utils/ItemList';
import extractText from '../../common/utils/extractText';
/**
* The `PostControls` utility constructs a list of buttons for a post which
@@ -145,6 +146,7 @@ export default {
* @return {Promise}
*/
hideAction() {
if (!confirm(extractText(app.translator.trans('core.forum.post_controls.hide_confirmation')))) return;
this.pushAttributes({ hiddenAt: new Date(), hiddenUser: app.session.user });
return this.save({ isHidden: true }).then(() => m.redraw());
@@ -167,6 +169,7 @@ export default {
* @return {Promise}
*/
deleteAction(context) {
if (!confirm(extractText(app.translator.trans('core.forum.post_controls.delete_confirmation')))) return;
if (context) context.loading = true;
return this.delete()

View File

@@ -0,0 +1,14 @@
<?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.
*/
use Flarum\Database\Migration;
return Migration::addColumns('groups', [
'is_hidden' => ['boolean', 'default' => false]
]);

View File

@@ -26,6 +26,8 @@ class ListGroupsController extends AbstractListController
*/
protected function data(ServerRequestInterface $request, Document $document)
{
return Group::all();
$actor = $request->getAttribute('actor');
return Group::whereVisibleTo($actor)->get();
}
}

View File

@@ -45,6 +45,10 @@ class BasicUserSerializer extends AbstractSerializer
*/
protected function groups($user)
{
return $this->hasMany($user, GroupSerializer::class);
if ($this->getActor()->can('viewHiddenGroups')) {
return $this->hasMany($user, GroupSerializer::class);
}
return $this->hasMany($user, GroupSerializer::class, 'visibleGroups');
}
}

View File

@@ -85,7 +85,7 @@ class ForumSerializer extends AbstractSerializer
if ($this->actor->can('administrate')) {
$attributes['adminUrl'] = $this->url->to('admin')->base();
$attributes['version'] = $this->app->version();
$attributes['version'] = Application::VERSION;
}
return $attributes;

View File

@@ -52,6 +52,7 @@ class GroupSerializer extends AbstractSerializer
'namePlural' => $this->translateGroupName($group->name_plural),
'color' => $group->color,
'icon' => $group->icon,
'isHidden' => $group->is_hidden
];
}

View File

@@ -9,9 +9,9 @@
namespace Flarum\Console\Event;
use Flarum\Foundation\Application;
use Illuminate\Console\Command;
use Symfony\Component\Console\Application as ConsoleApplication;
use Illuminate\Contracts\Container\Container;
use Symfony\Component\Console\Application;
/**
* @deprecated
@@ -19,22 +19,22 @@ use Symfony\Component\Console\Application as ConsoleApplication;
class Configuring
{
/**
* @var Application
* @var Container
*/
public $app;
/**
* @var ConsoleApplication
* @var Application
*/
public $console;
/**
* @param Application $app
* @param ConsoleApplication $console
* @param Container $container
* @param Application $console
*/
public function __construct(Application $app, ConsoleApplication $console)
public function __construct(Container $container, Application $console)
{
$this->app = $app;
$this->app = $container;
$this->console = $console;
}

View File

@@ -10,12 +10,12 @@
namespace Flarum\Console;
use Flarum\Console\Event\Configuring;
use Flarum\Foundation\Application;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\SiteInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -33,7 +33,7 @@ class Server
{
$app = $this->site->bootApp();
$console = new ConsoleApplication('Flarum', Application::VERSION);
$console = new Application('Flarum', \Flarum\Foundation\Application::VERSION);
foreach ($app->getConsoleCommands() as $command) {
$console->add($command);
@@ -47,29 +47,28 @@ class Server
/**
* @deprecated
*/
private function extend(ConsoleApplication $console)
private function extend(Application $console)
{
$app = Application::getInstance();
$container = \Illuminate\Container\Container::getInstance();
$this->handleErrors($app, $console);
$this->handleErrors($container, $console);
$events = $app->make(Dispatcher::class);
$events->dispatch(new Configuring($app, $console));
$events = $container->make(Dispatcher::class);
$events->dispatch(new Configuring($container, $console));
}
private function handleErrors(Application $app, ConsoleApplication $console)
private function handleErrors(Container $container, Application $console)
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) use ($app) {
$dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) use ($container) {
/** @var Registry $registry */
$registry = $app->make(Registry::class);
$registry = $container->make(Registry::class);
$error = $registry->handle($event->getError());
/** @var Reporter[] $reporters */
$reporters = $app->tagged(Reporter::class);
$reporters = $container->tagged(Reporter::class);
if ($error->shouldBeReported()) {
foreach ($reporters as $reporter) {

View File

@@ -14,6 +14,7 @@ use Flarum\Event\ConfigureModelDefaultAttributes;
use Flarum\Event\GetModelRelationship;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Arr;
use LogicException;
/**
@@ -46,6 +47,12 @@ abstract class AbstractModel extends Eloquent
*/
protected $afterDeleteCallbacks = [];
public static $customRelations = [];
public static $dateAttributes = [];
public static $defaults = [];
/**
* {@inheritdoc}
*/
@@ -71,13 +78,20 @@ abstract class AbstractModel extends Eloquent
*/
public function __construct(array $attributes = [])
{
$defaults = [];
$this->attributes = [];
foreach (array_merge(array_reverse(class_parents($this)), [static::class]) as $class) {
$this->attributes = array_merge($this->attributes, Arr::get(static::$defaults, $class, []));
}
// Deprecated in beta 13, remove in beta 14.
static::$dispatcher->dispatch(
new ConfigureModelDefaultAttributes($this, $defaults)
new ConfigureModelDefaultAttributes($this, $this->attributes)
);
$this->attributes = $defaults;
$this->attributes = array_map(function ($item) {
return is_callable($item) ? $item() : $item;
}, $this->attributes);
parent::__construct($attributes);
}
@@ -89,19 +103,17 @@ abstract class AbstractModel extends Eloquent
*/
public function getDates()
{
static $dates = [];
static::$dispatcher->dispatch(
new ConfigureModelDates($this, $this->dates)
);
$class = get_class($this);
$dates = $this->dates;
if (! isset($dates[$class])) {
static::$dispatcher->dispatch(
new ConfigureModelDates($this, $this->dates)
);
$dates[$class] = $this->dates;
foreach (array_merge(array_reverse(class_parents($this)), [static::class]) as $class) {
$dates = array_merge($dates, Arr::get(static::$dateAttributes, $class, []));
}
return $dates[$class];
return $dates;
}
/**
@@ -139,6 +151,14 @@ abstract class AbstractModel extends Eloquent
*/
protected function getCustomRelation($name)
{
foreach (array_merge([static::class], class_parents($this)) as $class) {
$relation = Arr::get(static::$customRelations, $class.".$name", null);
if (! is_null($relation)) {
return $relation($this);
}
}
// Deprecated, remove in beta 14
return static::$dispatcher->until(
new GetModelRelationship($this, $name)
);

View File

@@ -14,21 +14,29 @@ use Flarum\Database\Migrator;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Schema\Builder;
class MigrateCommand extends AbstractCommand
{
/**
* @var Container
*/
protected $container;
/**
* @var Application
*/
protected $app;
/**
* @param Container $container
* @param Application $application
*/
public function __construct(Application $application)
public function __construct(Container $container, Application $application)
{
$this->container = $container;
$this->app = $application;
parent::__construct();
@@ -58,16 +66,16 @@ class MigrateCommand extends AbstractCommand
public function upgrade()
{
$this->app->bind(Builder::class, function ($app) {
return $app->make(ConnectionInterface::class)->getSchemaBuilder();
$this->container->bind(Builder::class, function ($container) {
return $container->make(ConnectionInterface::class)->getSchemaBuilder();
});
$migrator = $this->app->make(Migrator::class);
$migrator = $this->container->make(Migrator::class);
$migrator->setOutput($this->output);
$migrator->run(__DIR__.'/../../../migrations');
$extensions = $this->app->make(ExtensionManager::class);
$extensions = $this->container->make(ExtensionManager::class);
$extensions->getMigrator()->setOutput($this->output);
foreach ($extensions->getEnabledExtensions() as $name => $extension) {
@@ -78,11 +86,11 @@ class MigrateCommand extends AbstractCommand
}
}
$this->app->make(SettingsRepositoryInterface::class)->set('version', $this->app->version());
$this->container->make(SettingsRepositoryInterface::class)->set('version', Application::VERSION);
$this->info('Publishing assets...');
$this->app->make('files')->copyDirectory(
$this->container->make('files')->copyDirectory(
$this->app->vendorPath().'/components/font-awesome/webfonts',
$this->app->publicPath().'/assets/fonts'
);

View File

@@ -12,6 +12,8 @@ namespace Flarum\Event;
use Flarum\Database\AbstractModel;
/**
* @deprecated in beta 13, removed in beta 14
*
* The `ConfigureModelDates` event is called to retrieve a list of fields for a model
* that should be converted into date objects.
*/

View File

@@ -11,6 +11,9 @@ namespace Flarum\Event;
use Flarum\Database\AbstractModel;
/**
* @deprecated in beta 13, removed in beta 14
*/
class ConfigureModelDefaultAttributes
{
/**

View File

@@ -12,6 +12,8 @@ namespace Flarum\Event;
use Flarum\Database\AbstractModel;
/**
* @deprecated beta 13, use the Model extender instead.
*
* The `GetModelRelationship` event is called to retrieve Relation object for a
* model. Listeners should return an Eloquent Relation object.
*/

View File

@@ -11,13 +11,20 @@ namespace Flarum\Extend;
use DirectoryIterator;
use Flarum\Extension\Extension;
use Flarum\Extension\ExtensionManager;
use Flarum\Locale\LocaleManager;
use Illuminate\Contracts\Container\Container;
use InvalidArgumentException;
use RuntimeException;
use SplFileInfo;
class LanguagePack implements ExtenderInterface, LifecycleInterface
{
private const CORE_LOCALE_FILES = [
'core',
'validation',
];
private $path;
/**
@@ -49,13 +56,13 @@ class LanguagePack implements ExtenderInterface, LifecycleInterface
$container->resolving(
LocaleManager::class,
function (LocaleManager $locales) use ($extension, $locale, $title) {
$this->registerLocale($locales, $extension, $locale, $title);
function (LocaleManager $locales, Container $container) use ($extension, $locale, $title) {
$this->registerLocale($container, $locales, $extension, $locale, $title);
}
);
}
private function registerLocale(LocaleManager $locales, Extension $extension, $locale, $title)
private function registerLocale(Container $container, LocaleManager $locales, Extension $extension, $locale, $title)
{
$locales->addLocale($locale, $title);
@@ -76,12 +83,41 @@ class LanguagePack implements ExtenderInterface, LifecycleInterface
}
foreach (new DirectoryIterator($directory) as $file) {
if ($file->isFile() && in_array($file->getExtension(), ['yml', 'yaml'])) {
if ($this->shouldLoad($file, $container)) {
$locales->addTranslations($locale, $file->getPathname());
}
}
}
private function shouldLoad(SplFileInfo $file, Container $container)
{
if (! $file->isFile()) {
return false;
}
// We are only interested in YAML files
if (! in_array($file->getExtension(), ['yml', 'yaml'], true)) {
return false;
}
// Some language packs include translations for many extensions
// from the ecosystems. For performance reasons, we should only
// load those that belong to core, or extensions that are enabled.
// To identify them, we compare the filename (without the YAML
// extension) with the list of known names and all extension IDs.
$slug = $file->getBasename(".{$file->getExtension()}");
if (in_array($slug, self::CORE_LOCALE_FILES, true)) {
return true;
}
/** @var ExtensionManager $extensions */
static $extensions;
$extensions = $extensions ?? $container->make(ExtensionManager::class);
return $extensions->isEnabled($slug);
}
public function onEnable(Container $container, Extension $extension)
{
$container->make('flarum.locales')->clearCache();

182
src/Extend/Model.php Normal file
View File

@@ -0,0 +1,182 @@
<?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\Database\AbstractModel;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Arr;
class Model implements ExtenderInterface
{
private $modelClass;
/**
* @param string $modelClass The ::class attribute of the model you are modifying.
* This model should extend from \Flarum\Database\AbstractModel.
*/
public function __construct(string $modelClass)
{
$this->modelClass = $modelClass;
}
/**
* Add an attribute to be treated as a date.
*
* @param string $attribute
* @return self
*/
public function dateAttribute(string $attribute)
{
Arr::set(
AbstractModel::$dateAttributes,
$this->modelClass,
array_merge(
Arr::get(AbstractModel::$dateAttributes, $this->modelClass, []),
[$attribute]
)
);
return $this;
}
/**
* Add a default value for a given attribute, which can be an explicit value, or a closure.
*
* @param string $attribute
* @param mixed $value
* @return self
*/
public function default(string $attribute, $value)
{
Arr::set(AbstractModel::$defaults, "$this->modelClass.$attribute", $value);
return $this;
}
/**
* Establish a simple belongsTo relationship from this model to another model.
* This represents an inverse one-to-one or inverse one-to-many relationship.
* For more complex relationships, use the ->relationship method.
*
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
* @param string $foreignKey: The foreign key attribute of the parent model.
* @param string $ownerKey: The primary key attribute of the parent model.
* @return self
*/
public function belongsTo(string $name, string $related, string $foreignKey = null, string $ownerKey = null)
{
return $this->relationship($name, function (AbstractModel $model) use ($related, $foreignKey, $ownerKey, $name) {
return $model->belongsTo($related, $foreignKey, $ownerKey, $name);
});
}
/**
* Establish a simple belongsToMany relationship from this model to another model.
* This represents a many-to-many relationship.
* For more complex relationships, use the ->relationship method.
*
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
* @param string $table: The intermediate table for this relation
* @param string $foreignPivotKey: The foreign key attribute of the parent model.
* @param string $relatedPivotKey: The associated key attribute of the relation.
* @param string $parentKey: The key name of the parent model.
* @param string $relatedKey: The key name of the related model.
* @return self
*/
public function belongsToMany(
string $name,
string $related,
string $table = null,
string $foreignPivotKey = null,
string $relatedPivotKey = null,
string $parentKey = null,
string $relatedKey = null
) {
return $this->relationship($name, function (AbstractModel $model) use ($related, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $name) {
return $model->belongsToMany($related, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $name);
});
}
/**
* Establish a simple hasOne relationship from this model to another model.
* This represents a one-to-one relationship.
* For more complex relationships, use the ->relationship method.
*
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
* @param string $foreignKey: The foreign key attribute of the parent model.
* @param string $localKey: The primary key attribute of the parent model.
* @return self
*/
public function hasOne(string $name, string $related, string $foreignKey = null, string $localKey = null)
{
return $this->relationship($name, function (AbstractModel $model) use ($related, $foreignKey, $localKey) {
return $model->hasOne($related, $foreignKey, $localKey);
});
}
/**
* Establish a simple hasMany relationship from this model to another model.
* This represents a one-to-many relationship.
* For more complex relationships, use the ->relationship method.
*
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param string $related: The ::class attribute of the model, which should extend \Flarum\Database\AbstractModel.
* @param string $foreignKey: The foreign key attribute of the parent model.
* @param string $localKey: The primary key attribute of the parent model.
* @return self
*/
public function hasMany(string $name, string $related, string $foreignKey = null, string $localKey = null)
{
return $this->relationship($name, function (AbstractModel $model) use ($related, $foreignKey, $localKey) {
return $model->hasMany($related, $foreignKey, $localKey);
});
}
/**
* Add a relationship from this model to another model.
*
* @param string $name: The name of the relation. This doesn't have to be anything in particular,
* but has to be unique from other relation names for this model, and should
* work as the name of a method.
* @param callable $callable
*
* The callable can be a closure or invokable class, and should accept:
* - $instance: An instance of this model.
*
* The callable should return:
* - $relationship: A Laravel Relationship object. See relevant methods of models
* like \Flarum\User\User for examples of how relationships should be returned.
*
* @return self
*/
public function relationship(string $name, callable $callable)
{
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", $callable);
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
// Nothing needed here.
}
}

View File

@@ -31,6 +31,8 @@ class ExtensionManager
protected $app;
protected $container;
protected $migrator;
/**
@@ -51,12 +53,14 @@ class ExtensionManager
public function __construct(
SettingsRepositoryInterface $config,
Application $app,
Container $container,
Migrator $migrator,
Dispatcher $dispatcher,
Filesystem $filesystem
) {
$this->config = $config;
$this->app = $app;
$this->container = $container;
$this->migrator = $migrator;
$this->dispatcher = $dispatcher;
$this->filesystem = $filesystem;
@@ -73,12 +77,20 @@ class ExtensionManager
// Load all packages installed by composer.
$installed = json_decode($this->filesystem->get($this->app->vendorPath().'/composer/installed.json'), true);
// Composer 2.0 changes the structure of the installed.json manifest
$installed = $installed['packages'] ?? $installed;
foreach ($installed as $package) {
if (Arr::get($package, 'type') != 'flarum-extension' || empty(Arr::get($package, 'name'))) {
continue;
}
$path = isset($package['install-path'])
? $this->getExtensionsDir().'/composer/'.$package['install-path']
: $this->getExtensionsDir().'/'.Arr::get($package, 'name');
// Instantiates an Extension object using the package path and composer.json file.
$extension = new Extension($this->getExtensionsDir().'/'.Arr::get($package, 'name'), $package);
$extension = new Extension($path, $package);
// Per default all extensions are installed if they are registered in composer.
$extension->setInstalled(true);
@@ -130,7 +142,7 @@ class ExtensionManager
$this->setEnabled($enabled);
$extension->enable($this->app);
$extension->enable($this->container);
$this->dispatcher->dispatch(new Enabled($extension));
}
@@ -156,7 +168,7 @@ class ExtensionManager
$this->setEnabled($enabled);
$extension->disable($this->app);
$extension->disable($this->container);
$this->dispatcher->dispatch(new Disabled($extension));
}
@@ -227,7 +239,7 @@ class ExtensionManager
*/
public function migrate(Extension $extension, $direction = 'up')
{
$this->app->bind(Builder::class, function ($container) {
$this->container->bind(Builder::class, function ($container) {
return $container->make(ConnectionInterface::class)->getSchemaBuilder();
});

View File

@@ -23,7 +23,7 @@ class Application extends Container implements ApplicationContract
*
* @var string
*/
const VERSION = '0.1.0-beta.13-dev';
const VERSION = '0.1.0-beta.13';
/**
* The base path for the Flarum installation.

View File

@@ -12,10 +12,12 @@ namespace Flarum\Frontend\Content;
use Flarum\Foundation\Application;
use Flarum\Frontend\Compiler\CompilerInterface;
use Flarum\Frontend\Document;
use Illuminate\Contracts\Container\Container;
use Psr\Http\Message\ServerRequestInterface as Request;
class Assets
{
protected $container;
protected $app;
/**
@@ -23,14 +25,15 @@ class Assets
*/
protected $assets;
public function __construct(Application $app)
public function __construct(Container $container, Application $app)
{
$this->container = $container;
$this->app = $app;
}
public function forFrontend(string $name)
{
$this->assets = $this->app->make('flarum.assets.'.$name);
$this->assets = $this->container->make('flarum.assets.'.$name);
return $this;
}

View File

@@ -54,7 +54,8 @@ class CreateGroupHandler
Arr::get($data, 'attributes.nameSingular'),
Arr::get($data, 'attributes.namePlural'),
Arr::get($data, 'attributes.color'),
Arr::get($data, 'attributes.icon')
Arr::get($data, 'attributes.icon'),
Arr::get($data, 'attributes.isHidden', false)
);
$this->events->dispatch(

View File

@@ -74,6 +74,10 @@ class EditGroupHandler
$group->icon = $attributes['icon'];
}
if (isset($attributes['isHidden'])) {
$group->is_hidden = $attributes['isHidden'];
}
$this->events->dispatch(
new Saving($group, $actor, $data)
);

View File

@@ -23,6 +23,7 @@ use Flarum\User\User;
* @property string $name_plural
* @property string|null $color
* @property string|null $icon
* @property bool $is_hidden
* @property \Illuminate\Database\Eloquent\Collection $users
* @property \Illuminate\Database\Eloquent\Collection $permissions
*/
@@ -72,9 +73,10 @@ class Group extends AbstractModel
* @param string $namePlural
* @param string $color
* @param string $icon
* @param bool $isHidden
* @return static
*/
public static function build($nameSingular, $namePlural, $color, $icon)
public static function build($nameSingular, $namePlural, $color = null, $icon = null, bool $isHidden = false): self
{
$group = new static;
@@ -82,6 +84,7 @@ class Group extends AbstractModel
$group->name_plural = $namePlural;
$group->color = $color;
$group->icon = $icon;
$group->is_hidden = $isHidden;
$group->raise(new Created($group));

View File

@@ -11,6 +11,7 @@ namespace Flarum\Group;
use Flarum\User\AbstractPolicy;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
class GroupPolicy extends AbstractPolicy
{
@@ -30,4 +31,15 @@ class GroupPolicy extends AbstractPolicy
return true;
}
}
/**
* @param User $actor
* @param Builder $query
*/
public function find(User $actor, Builder $query)
{
if ($actor->cannot('viewHiddenGroups')) {
$query->where('is_hidden', false);
}
}
}

View File

@@ -89,14 +89,22 @@ class EnableBundledExtensions implements Step
private function loadExtensions()
{
$json = file_get_contents("$this->vendorPath/composer/installed.json");
$installed = json_decode($json, true);
return (new Collection(json_decode($json, true)))
// Composer 2.0 changes the structure of the installed.json manifest
$installed = $installed['packages'] ?? $installed;
return (new Collection($installed))
->filter(function ($package) {
return Arr::get($package, 'type') == 'flarum-extension';
})->filter(function ($package) {
return ! empty(Arr::get($package, 'name'));
})->map(function ($package) {
$extension = new Extension($this->vendorPath.'/'.Arr::get($package, 'name'), $package);
$path = isset($package['install-path'])
? "$this->vendorPath/composer/".$package['install-path']
: $this->vendorPath.'/'.Arr::get($package, 'name');
$extension = new Extension($path, $package);
$extension->setVersion(Arr::get($package, 'version'));
return $extension;

View File

@@ -51,19 +51,6 @@ class DiscussionRenamedBlueprint implements BlueprintInterface
return ['postNumber' => (int) $this->post->number];
}
/**
* {@inheritdoc}
*/
public function getAttributes(): array
{
return [
'type' => static::getType(),
'from_user_id' => $this->post->user ? $this->post->user->id : null,
'subject_id' => $this->post->discussion ? $this->post->discussion->id : null,
'data' => json_encode(['postNumber' => (int) $this->post->number]),
];
}
/**
* {@inheritdoc}
*/

View File

@@ -35,7 +35,7 @@ abstract class AbstractPolicy
*/
public function getPermission(GetPermission $event)
{
if (! $event->model instanceof $this->model) {
if (! $event->model instanceof $this->model && $event->model !== $this->model) {
return;
}

View File

@@ -606,6 +606,11 @@ class User extends AbstractModel
return $this->belongsToMany(Group::class);
}
public function visibleGroups()
{
return $this->belongsToMany(Group::class)->where('is_hidden', false);
}
/**
* Define the relationship with the user's notifications.
*

View File

@@ -9,15 +9,37 @@
namespace Flarum\Tests\integration\api\groups;
use Flarum\Group\Group;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
use Illuminate\Support\Arr;
class ListTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function setUp(): void
{
parent::setUp();
$this->prepareDatabase([
'users' => [
$this->adminUser(),
$this->normalUser(),
],
'groups' => [
$this->adminGroup(),
$this->hiddenGroup()
],
'group_user' => [
['user_id' => 1, 'group_id' => 1],
],
]);
}
/**
* @test
*/
public function shows_index_for_guest()
public function shows_limited_index_for_guest()
{
$response = $this->send(
$this->request('GET', '/api/groups')
@@ -26,6 +48,35 @@ class ListTest extends TestCase
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals(Group::count(), count($data['data']));
$this->assertEquals(['1'], Arr::pluck($data['data'], 'id'));
}
/**
* @test
*/
public function shows_index_for_admin()
{
$response = $this->send(
$this->request('GET', '/api/groups', [
'authenticatedAs' => 1,
])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
$this->assertEquals(['1', '10'], Arr::pluck($data['data'], 'id'));
}
protected function hiddenGroup(): array
{
return [
'id' => 10,
'name_singular' => 'Hidden',
'name_plural' => 'Ninjas',
'color' => null,
'icon' => 'fas fa-wrench',
'is_hidden' => 1
];
}
}

View File

@@ -0,0 +1,425 @@
<?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 Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Group\Group;
use Flarum\Post\AbstractEventPost;
use Flarum\Post\CommentPost;
use Flarum\Post\DiscussionRenamedPost;
use Flarum\Post\Post;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
use Flarum\User\User;
class ModelTest extends TestCase
{
use RetrievesAuthorizedUsers;
protected function prepDb()
{
$this->prepareDatabase([
'users' => [
$this->adminUser(),
$this->normalUser(),
],
'discussions' => []
]);
}
protected function prepPostsHierarchy()
{
$this->prepareDatabase([
'users' => [
$this->normalUser(),
],
'discussions' => [
['id' => 1, 'title' => 'Discussion with post', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1, 'is_private' => 0],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'discussionRenamed', 'content' => '<t><p>can i haz relationz?</p></t>'],
],
]);
}
/**
* @test
*/
public function custom_relationship_does_not_exist_by_default()
{
$this->prepDB();
$user = User::find(1);
$this->expectException(\BadMethodCallException::class);
$user->customRelation();
}
/**
* @test
*/
public function custom_hasOne_relationship_exists_if_added()
{
$this->extend(
(new Extend\Model(User::class))
->hasOne('customRelation', Discussion::class, 'user_id')
);
$this->prepDB();
$user = User::find(1);
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/**
* @test
*/
public function custom_hasMany_relationship_exists_if_added()
{
$this->extend(
(new Extend\Model(User::class))
->hasMany('customRelation', Discussion::class, 'user_id')
);
$this->prepDB();
$user = User::find(1);
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/**
* @test
*/
public function custom_belongsTo_relationship_exists_if_added()
{
$this->extend(
(new Extend\Model(User::class))
->belongsTo('customRelation', Discussion::class, 'user_id')
);
$this->prepDB();
$user = User::find(1);
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/**
* @test
*/
public function custom_relationship_exists_if_added()
{
$this->extend(
(new Extend\Model(User::class))
->relationship('customRelation', function (User $user) {
return $user->hasMany(Discussion::class, 'user_id');
})
);
$this->prepDB();
$user = User::find(1);
$this->assertEquals([], $user->customRelation()->get()->toArray());
}
/**
* @test
*/
public function custom_relationship_exists_and_can_return_instances_if_added()
{
$this->extend(
(new Extend\Model(User::class))
->relationship('customRelation', function (User $user) {
return $user->hasMany(Discussion::class, 'user_id');
})
);
$this->prepDB();
$this->prepareDatabase([
'discussions' => [
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1]
]
]);
$user = User::find(1);
$this->assertNotEquals([], $user->customRelation()->get()->toArray());
$this->assertContains(json_encode(__CLASS__), json_encode($user->customRelation()->get()));
}
/**
* @test
*/
public function custom_relationship_is_inherited_to_child_classes()
{
$this->extend(
(new Extend\Model(Post::class))
->belongsTo('ancestor', Discussion::class, 'discussion_id')
);
$this->prepPostsHierarchy();
$post = CommentPost::find(1);
$this->assertInstanceOf(Discussion::class, $post->ancestor);
$this->assertEquals(1, $post->ancestor->id);
}
/**
* @test
*/
public function custom_relationship_prioritizes_child_classes_within_2_parent_classes()
{
$this->extend(
(new Extend\Model(Post::class))
->belongsTo('ancestor', User::class, 'user_id'),
(new Extend\Model(AbstractEventPost::class))
->belongsTo('ancestor', Discussion::class, 'discussion_id')
);
$this->prepPostsHierarchy();
$post = DiscussionRenamedPost::find(1);
$this->assertInstanceOf(Discussion::class, $post->ancestor);
$this->assertEquals(1, $post->ancestor->id);
}
/**
* @test
*/
public function custom_relationship_prioritizes_child_classes_within_child_class_and_immediate_parent()
{
$this->extend(
(new Extend\Model(AbstractEventPost::class))
->belongsTo('ancestor', Discussion::class, 'discussion_id'),
(new Extend\Model(DiscussionRenamedPost::class))
->belongsTo('ancestor', User::class, 'user_id')
);
$this->prepPostsHierarchy();
$post = DiscussionRenamedPost::find(1);
$this->assertInstanceOf(User::class, $post->ancestor);
$this->assertEquals(2, $post->ancestor->id);
}
/**
* @test
*/
public function custom_relationship_does_not_exist_if_added_to_unrelated_model()
{
$this->extend(
(new Extend\Model(User::class))
->relationship('customRelation', function (User $user) {
return $user->hasMany(Discussion::class, 'user_id');
})
);
$this->prepDB();
$this->prepareDatabase([
'groups' => [
$this->adminGroup()
]
]);
$group = Group::find(1);
$this->expectException(\BadMethodCallException::class);
$group->customRelation();
}
/**
* @test
*/
public function custom_default_attribute_doesnt_exist_if_not_set()
{
$group = new Group;
$this->app();
$this->assertNotEquals('Custom Default', $group->name_singular);
}
/**
* @test
*/
public function custom_default_attribute_works_if_set()
{
$this->extend(
(new Extend\Model(Group::class))
->default('name_singular', 'Custom Default')
);
$this->app();
$group = new Group;
$this->assertEquals('Custom Default', $group->name_singular);
}
/**
* @test
*/
public function custom_default_attribute_evaluated_at_runtime_if_callable()
{
$this->extend(
(new Extend\Model(Group::class))
->default('counter', function () {
static $counter = 0;
return ++$counter;
})
);
$this->app();
$group1 = new Group;
$group2 = new Group;
$this->assertEquals(1, $group1->counter);
$this->assertEquals(2, $group2->counter);
}
/**
* @test
*/
public function custom_default_attribute_is_inherited_to_child_classes()
{
$this->extend(
(new Extend\Model(Post::class))
->default('answer', 42)
);
$this->app();
$post = new CommentPost;
$this->assertEquals(42, $post->answer);
}
/**
* @test
*/
public function custom_default_attribute_inheritance_prioritizes_child_class()
{
$this->extend(
(new Extend\Model(Post::class))
->default('answer', 'dont do this'),
(new Extend\Model(AbstractEventPost::class))
->default('answer', 42),
(new Extend\Model(DiscussionRenamedPost::class))
->default('answer', 'ni!')
);
$this->app();
$post = new CustomPost;
$this->assertEquals(42, $post->answer);
$commentPost = new DiscussionRenamedPost;
$this->assertEquals('ni!', $commentPost->answer);
}
/**
* @test
*/
public function custom_default_attribute_doesnt_work_if_set_on_unrelated_model()
{
$this->extend(
(new Extend\Model(Group::class))
->default('name_singular', 'Custom Default')
);
$this->app();
$user = new User;
$this->assertNotEquals('Custom Default', $user->name_singular);
}
/**
* @test
*/
public function custom_date_attribute_doesnt_exist_by_default()
{
$post = new Post;
$this->app();
$this->assertNotContains('custom', $post->getDates());
}
/**
* @test
*/
public function custom_date_attribute_can_be_set()
{
$this->extend(
(new Extend\Model(Post::class))
->dateAttribute('custom')
);
$this->app();
$post = new Post;
$this->assertContains('custom', $post->getDates());
}
/**
* @test
*/
public function custom_date_attribute_is_inherited_to_child_classes()
{
$this->extend(
(new Extend\Model(Post::class))
->dateAttribute('custom')
);
$this->app();
$post = new CommentPost;
$this->assertContains('custom', $post->getDates());
}
/**
* @test
*/
public function custom_date_attribute_doesnt_work_if_set_on_unrelated_model()
{
$this->extend(
(new Extend\Model(Post::class))
->dateAttribute('custom')
);
$this->app();
$discussion = new Discussion;
$this->assertNotContains('custom', $discussion->getDates());
}
}
class CustomPost extends AbstractEventPost
{
/**
* {@inheritdoc}
*/
public static $type = 'customPost';
}

View File

@@ -0,0 +1,64 @@
<?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\unit\User;
use Flarum\Event\GetPermission;
use Flarum\Tests\unit\TestCase;
use Flarum\User\AbstractPolicy;
use Flarum\User\User;
use Illuminate\Events\Dispatcher;
use Mockery as m;
class AbstractPolicyTest extends TestCase
{
private $policy;
private $dispatcher;
protected function setUp(): void
{
$this->policy = m::mock(CustomUserPolicy::class)->makePartial();
$this->dispatcher = new Dispatcher();
$this->dispatcher->subscribe($this->policy);
User::setEventDispatcher($this->dispatcher);
}
public function test_policy_can_be_called_with_object()
{
$this->policy->shouldReceive('edit')->andReturn(true);
$allowed = $this->dispatcher->until(new GetPermission(new User(), 'edit', new User()));
$this->assertTrue($allowed);
}
public function test_policy_can_be_called_with_class()
{
$this->policy->shouldReceive('create')->andReturn(true);
$allowed = $this->dispatcher->until(new GetPermission(new User(), 'create', User::class));
$this->assertTrue($allowed);
}
}
class CustomUserPolicy extends AbstractPolicy
{
protected $model = User::class;
public function create(User $actor)
{
return true;
}
public function edit(User $actor, User $user)
{
return true;
}
}