mirror of
https://github.com/flarum/core.git
synced 2025-08-13 20:04:24 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a94bf44072 | ||
|
ce82834a58 | ||
|
397642ab5a | ||
|
79e17b3bde | ||
|
c6ba2cd0d5 | ||
|
5d6795c353 | ||
|
d61c3cf277 | ||
|
a0b9add2d8 | ||
|
88abe63a8f | ||
|
9d00490591 | ||
|
7c1f55d985 | ||
|
345023242b | ||
|
00329eaee0 | ||
|
6c89f8161e | ||
|
8eb56b16cf | ||
|
634132e06e |
4
.github/workflows/REUSABLE_backend.yml
vendored
4
.github/workflows/REUSABLE_backend.yml
vendored
@@ -105,6 +105,8 @@ jobs:
|
||||
php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||
- php: 8.3
|
||||
php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||
# - php: 8.4
|
||||
# php_ini_values: error_reporting=E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||
|
||||
# To reduce number of actions, we exclude some PHP versions from running with some DB versions.
|
||||
exclude:
|
||||
@@ -177,6 +179,8 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
php: ${{ fromJSON(inputs.php_versions) }}
|
||||
# exclude:
|
||||
# - php: 8.4
|
||||
|
||||
name: 'PHPStan PHP ${{ matrix.php }}'
|
||||
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## [v1.8.9](https://github.com/flarum/framework/compare/v1.8.8...v1.8.9)
|
||||
### Fixed
|
||||
* change condition when unread label is shown in Scrubber (https://github.com/flarum/framework/pull/4116)
|
||||
* resolve a11y warnings in Admin Frontend (https://github.com/flarum/framework/pull/4114)
|
||||
* return empty object if selected mail driver is unavailable (https://github.com/flarum/framework/pull/4113)
|
||||
### Changed
|
||||
* change private to protected, allowing extensibility (https://github.com/flarum/framework/pull/4119)
|
||||
* change length of email field (https://github.com/flarum/framework/pull/4117)
|
||||
### Added
|
||||
* Implement Support for Translatable Validation Attribute Errors (https://github.com/flarum/framework/pull/4070)
|
||||
* PHP 8.4 support (https://github.com/flarum/framework/pull/4105)
|
||||
* conditional extend whenExtensionDisabled (https://github.com/flarum/framework/pull/4107)
|
||||
|
||||
## [v1.8.8](https://github.com/flarum/framework/compare/v1.8.7...v1.8.8)
|
||||
### Fixed
|
||||
* previously suspended admin users cannot remove their avatar after suspension (https://github.com/flarum/framework/pull/4071)
|
||||
* new search term not being passed (https://github.com/flarum/framework/pull/4083)
|
||||
* postfooter did not apply the empty subclass (https://github.com/flarum/framework/pull/4085)
|
||||
|
||||
## [v1.8.7](https://github.com/flarum/framework/compare/v1.8.6...v1.8.7)
|
||||
### Fixed
|
||||
* BasicsPage not viewable if only one language pack enabled, and/or `flarum/nicknames` not enabled (https://github.com/flarum/framework/pull/4062)
|
||||
|
@@ -112,9 +112,9 @@
|
||||
"illuminate/view": "^8.0",
|
||||
"intervention/image": "2.5.* || ^2.6.1",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laminas/laminas-diactoros": "^2.4.1",
|
||||
"laminas/laminas-httphandlerrunner": "^1.2.0 || ^2.3.0",
|
||||
"laminas/laminas-stratigility": "^3.2.2",
|
||||
"laminas/laminas-diactoros": "^2.4.1 || ^3.0.0",
|
||||
"laminas/laminas-httphandlerrunner": "^1.2.0 || ^2.3.0 || ^3.0.0",
|
||||
"laminas/laminas-stratigility": "^3.2.2 || ^4.0.0",
|
||||
"league/flysystem": "^1.0.11",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"middlewares/base-path": "^2.0.1",
|
||||
|
@@ -51,8 +51,7 @@ return [
|
||||
->belongsToMany('mentionedBy', Post::class, 'post_mentions_post', 'mentions_post_id', 'post_id')
|
||||
->belongsToMany('mentionsPosts', Post::class, 'post_mentions_post', 'post_id', 'mentions_post_id')
|
||||
->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id')
|
||||
->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id')
|
||||
->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id'),
|
||||
->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id'),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
|
@@ -161,3 +161,23 @@ flarum-extension-manager:
|
||||
|
||||
why_not_modal:
|
||||
title: Why Won't it Update
|
||||
|
||||
validation:
|
||||
attributes:
|
||||
minimum_stability: minimum stability
|
||||
repositories: repositories
|
||||
repositories.*: repositories
|
||||
repositories.*.type: repository type
|
||||
repositories.*.url: repository URL
|
||||
extension_id: extension ID
|
||||
update_mode: update mode
|
||||
package: package
|
||||
version: version
|
||||
github_oauth: GitHub OAuth
|
||||
github_oauth.*: GitHub OAuth
|
||||
gitlab_oauth: GitLab OAuth
|
||||
gitlab_oauth.*: GitLab OAuth
|
||||
gitlab_token: GitLab Token
|
||||
gitlab_token.*: GitLab Token
|
||||
bearer: HTTP Bearer
|
||||
bearer.*: HTTP Bearer
|
||||
|
@@ -69,3 +69,7 @@ flarum-suspend:
|
||||
You have been unsuspended. You can head back to the forum by clicking on the following link:
|
||||
|
||||
{forum_url}
|
||||
|
||||
validation:
|
||||
attributes:
|
||||
suspendedUntil: suspended until
|
||||
|
2
extensions/tags/js/dist/admin.js
generated
vendored
2
extensions/tags/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/tags/js/dist/admin.js.map
generated
vendored
2
extensions/tags/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -16,7 +16,12 @@ function tagItem(tag) {
|
||||
<div className="TagListItem-info">
|
||||
{tagIcon(tag)}
|
||||
<span className="TagListItem-name">{tag.name()}</span>
|
||||
<Button className="Button Button--link" icon="fas fa-pencil-alt" onclick={() => app.modal.show(EditTagModal, { model: tag })} />
|
||||
<Button
|
||||
className="Button Button--link"
|
||||
icon="fas fa-pencil-alt"
|
||||
aria-label={app.translator.trans('flarum-tags.admin.tags.edit_tag_label', { tag: tag.name() })}
|
||||
onclick={() => app.modal.show(EditTagModal, { model: tag })}
|
||||
/>
|
||||
</div>
|
||||
{!tag.isChild() && tag.position() !== null && (
|
||||
<ol className="TagListItem-children TagList">
|
||||
|
@@ -56,6 +56,7 @@ flarum-tags:
|
||||
about_tags_text: "Tags are used to categorize discussions. Primary tags are like traditional forum categories: they can be arranged in a two-level hierarchy. Secondary tags do not have hierarchy or order, and are useful for micro-categorization."
|
||||
create_primary_tag_button: Create Primary Tag
|
||||
create_secondary_tag_button: Create Secondary Tag
|
||||
edit_tag_label: Edit Tag {tag}
|
||||
primary_heading: Primary Tags
|
||||
secondary_heading: Secondary Tags
|
||||
settings_heading: Settings
|
||||
@@ -126,3 +127,13 @@ flarum-tags:
|
||||
choose_tags_placeholder: "{count, plural, one {Choose 1 more tag} other {Choose # more tags}}"
|
||||
name: Name
|
||||
tags: Tags
|
||||
|
||||
validation:
|
||||
attributes:
|
||||
name: name
|
||||
slug: slug
|
||||
is_hidden: hidden
|
||||
description: description
|
||||
color: color
|
||||
tag_count_primary: => validation.attributes.tag_count_primary
|
||||
tag_count_secondary: => validation.attributes.tag_count_secondary
|
||||
|
@@ -61,9 +61,9 @@
|
||||
"illuminate/view": "^8.0",
|
||||
"intervention/image": "2.5.* || ^2.6.1",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laminas/laminas-diactoros": "^2.4.1",
|
||||
"laminas/laminas-httphandlerrunner": "^1.2.0 || ^2.3.0",
|
||||
"laminas/laminas-stratigility": "^3.2.2",
|
||||
"laminas/laminas-diactoros": "^2.4.1 || ^3.0.0",
|
||||
"laminas/laminas-httphandlerrunner": "^1.2.0 || ^2.3.0 || ^3.0.0",
|
||||
"laminas/laminas-stratigility": "^3.2.2 || ^4.0.0",
|
||||
"league/flysystem": "^1.0.11",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"middlewares/base-path": "^2.0.1",
|
||||
|
2
framework/core/js/dist/admin.js
generated
vendored
2
framework/core/js/dist/admin.js
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/admin.js.map
generated
vendored
2
framework/core/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/forum.js
generated
vendored
2
framework/core/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
framework/core/js/dist/forum.js.map
generated
vendored
2
framework/core/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -98,7 +98,7 @@ export default class MailPage<CustomAttrs extends IPageAttrs = IPageAttrs> exten
|
||||
|
||||
contentItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const fields = this.driverFields![this.setting('mail_driver')()];
|
||||
const fields = this.driverFields![this.setting('mail_driver')()] || {};
|
||||
const fieldKeys = Object.keys(fields);
|
||||
|
||||
items.add(
|
||||
|
@@ -57,7 +57,12 @@ export default class PermissionGrid<CustomAttrs extends IPermissionGridAttrs = I
|
||||
<th>
|
||||
{scope.label}{' '}
|
||||
{!!scope.onremove && (
|
||||
<Button icon="fas fa-times" className="Button Button--text PermissionGrid-removeScope" onclick={scope.onremove} />
|
||||
<Button
|
||||
icon="fas fa-times"
|
||||
className="Button Button--text PermissionGrid-removeScope"
|
||||
aria-label={app.translator.trans('core.admin.permissions.remove_scope_label', { scope: scope.label })}
|
||||
onclick={scope.onremove}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
|
@@ -41,7 +41,7 @@ export default class PostStreamScrubber extends Component {
|
||||
const newStyle = {
|
||||
top: 100 - unreadPercent * 100 + '%',
|
||||
height: unreadPercent * 100 + '%',
|
||||
opacity: unreadPercent ? 1 : 0,
|
||||
opacity: unreadPercent > 0 ? 1 : 0,
|
||||
};
|
||||
|
||||
if (vnode.state.oldStyle) {
|
||||
|
@@ -226,6 +226,7 @@ core:
|
||||
participate_heading: Participate
|
||||
post_without_throttle_label: Reply multiple times without waiting
|
||||
read_heading: Read
|
||||
remove_scope_label: Remove scope of {scope}
|
||||
rename_discussions_label: Rename discussions
|
||||
reply_to_discussions_label: Reply to discussions
|
||||
search_users_label: => core.ref.search_users
|
||||
|
@@ -79,6 +79,7 @@ validation:
|
||||
present: "The :attribute field must be present."
|
||||
regex: "The :attribute format is invalid."
|
||||
required: "The :attribute field is required."
|
||||
required_array_keys: "The :attribute array must contain entries for: :values."
|
||||
required_if: "The :attribute field is required when :other is :value."
|
||||
required_unless: "The :attribute field is required unless :other is in :values."
|
||||
required_with: "The :attribute field is required when :values is present."
|
||||
|
@@ -0,0 +1,25 @@
|
||||
<?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 Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('users', function (Blueprint $table) {
|
||||
$table->string('email', 254)->change();
|
||||
});
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('users', function (Blueprint $table) {
|
||||
$table->string('email', 150)->change();
|
||||
});
|
||||
}
|
||||
];
|
@@ -47,6 +47,20 @@ class Conditional implements ExtenderInterface
|
||||
}, $extenders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply extenders only if a specific extension is disabled/not installed.
|
||||
*
|
||||
* @param string $extensionId The ID of the extension.
|
||||
* @param ExtenderInterface[]|callable|string $extenders A callable returning an array of extenders, or an invokable class string.
|
||||
* @return self
|
||||
*/
|
||||
public function whenExtensionDisabled(string $extensionId, $extenders): self
|
||||
{
|
||||
return $this->when(function (ExtensionManager $extensions) use ($extensionId) {
|
||||
return ! $extensions->isEnabled($extensionId);
|
||||
}, $extenders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply extenders based on a condition.
|
||||
*
|
||||
|
@@ -333,6 +333,19 @@ class Extension implements Arrayable
|
||||
return $this->composerJsonAttribute('extra.flarum-extension.title');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getNamespace(): ?string
|
||||
{
|
||||
return Collection::make($this->composerJsonAttribute('autoload.psr-4'))
|
||||
->filter(function ($source) {
|
||||
return $source === 'src/';
|
||||
})
|
||||
->keys()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
@@ -62,7 +62,7 @@ class ResponseFactory
|
||||
));
|
||||
}
|
||||
|
||||
private function makeResponse(array $payload): HtmlResponse
|
||||
protected function makeResponse(array $payload): ResponseInterface
|
||||
{
|
||||
$content = sprintf(
|
||||
'<script>window.close(); window.opener.app.authenticationComplete(%s);</script>',
|
||||
@@ -72,7 +72,7 @@ class ResponseFactory
|
||||
return new HtmlResponse($content);
|
||||
}
|
||||
|
||||
private function makeLoggedInResponse(User $user)
|
||||
protected function makeLoggedInResponse(User $user)
|
||||
{
|
||||
$response = $this->makeResponse(['loggedIn' => true]);
|
||||
|
||||
|
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Foundation;
|
||||
|
||||
use Illuminate\Contracts\Cache\Store as Cache;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\Factory;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -16,6 +17,13 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
abstract class AbstractValidator
|
||||
{
|
||||
use ExtensionIdTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $CORE_VALIDATION_CACHE_KEY = 'core.validation.extension_id_class_names';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@@ -81,6 +89,30 @@ abstract class AbstractValidator
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getAttributeNames()
|
||||
{
|
||||
$cache = resolve(Cache::class);
|
||||
|
||||
if ($cache->get(self::$CORE_VALIDATION_CACHE_KEY) !== null) {
|
||||
return $cache->get(self::$CORE_VALIDATION_CACHE_KEY);
|
||||
}
|
||||
|
||||
$extId = $this->getClassExtensionId();
|
||||
$attributeNames = [];
|
||||
|
||||
foreach (array_keys($this->rules) as $attribute) {
|
||||
$key = $extId ? "$extId.validation.attributes.$attribute" : "validation.attributes.$attribute";
|
||||
$attributeNames[$attribute] = $this->translator->trans($key);
|
||||
}
|
||||
|
||||
$cache->forever(self::$CORE_VALIDATION_CACHE_KEY, $attributeNames);
|
||||
|
||||
return $attributeNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new validator instance for this model.
|
||||
*
|
||||
@@ -92,6 +124,7 @@ abstract class AbstractValidator
|
||||
$rules = Arr::only($this->getRules(), array_keys($attributes));
|
||||
|
||||
$validator = $this->validator->make($attributes, $rules, $this->getMessages());
|
||||
$validator->setAttributeNames($this->getAttributeNames());
|
||||
|
||||
foreach ($this->configuration as $callable) {
|
||||
$callable($this, $validator);
|
||||
|
@@ -21,7 +21,7 @@ class Application
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '1.8.8';
|
||||
const VERSION = '1.8.9';
|
||||
|
||||
/**
|
||||
* The IoC container for the Flarum application.
|
||||
|
31
framework/core/src/Foundation/ExtensionIdTrait.php
Normal file
31
framework/core/src/Foundation/ExtensionIdTrait.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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\Foundation;
|
||||
|
||||
use Flarum\Extension\Extension;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
|
||||
trait ExtensionIdTrait
|
||||
{
|
||||
protected function getClassExtensionId(): ?string
|
||||
{
|
||||
$extensions = resolve(ExtensionManager::class);
|
||||
|
||||
return $extensions->getExtensions()
|
||||
->mapWithKeys(function (Extension $extension) {
|
||||
return [$extension->getId() => $extension->getNamespace()];
|
||||
})
|
||||
->filter(function ($namespace) {
|
||||
return $namespace && str_starts_with(static::class, $namespace);
|
||||
})
|
||||
->keys()
|
||||
->first();
|
||||
}
|
||||
}
|
@@ -139,6 +139,71 @@ class CreateTest extends TestCase
|
||||
$this->assertEquals(1, $user->is_email_confirmed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_can_create_user_with_longest_valid_email()
|
||||
{
|
||||
$localPart = str_repeat('a', 64);
|
||||
$domain = str_repeat('a', 61).'.'.str_repeat('a', 60).'.'.str_repeat('a', 60).'.local';
|
||||
$email = $localPart.'@'.$domain;
|
||||
|
||||
$response = $this->send(
|
||||
$this->request(
|
||||
'POST',
|
||||
'/api/users',
|
||||
[
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'username' => 'test',
|
||||
'password' => 'too-obscure',
|
||||
'email' => $email,
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::where('username', 'test')->firstOrFail();
|
||||
|
||||
$this->assertEquals($email, $user->email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function admin_cannot_create_user_with_invalid_email_length()
|
||||
{
|
||||
$email = str_repeat('a', 65).'@'.str_repeat('a', 256).'.local';
|
||||
|
||||
$response = $this->send(
|
||||
$this->request(
|
||||
'POST',
|
||||
'/api/users',
|
||||
[
|
||||
'authenticatedAs' => 1,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'username' => 'test',
|
||||
'password' => 'too-obscure',
|
||||
'email' => $email,
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals(422, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
@@ -160,6 +160,27 @@ class ConditionalTest extends TestCase
|
||||
$this->app();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function conditional_disabled_extension_not_enabled_applies_extender()
|
||||
{
|
||||
$this->extend(
|
||||
(new Extend\Conditional())
|
||||
->whenExtensionDisabled('flarum-dummy-extension', TestExtender::class)
|
||||
);
|
||||
|
||||
$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_instantiate_extender_if_condition_is_false_using_callable()
|
||||
{
|
||||
|
Reference in New Issue
Block a user