diff --git a/composer.json b/composer.json index 7dce882..55dd4d9 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ "prefer-stable": true, "autoload-dev": { "psr-4": { - "FoskyM\\OAuthCenter\\Tests\\": "tests/" + "RhodesIsland\\OAuthCenter\\Tests\\": "tests/" } }, "archive": { diff --git a/extend.php b/extend.php index 0c6c625..7faf3bd 100644 --- a/extend.php +++ b/extend.php @@ -31,6 +31,7 @@ return [ (new Extend\Routes('forum')) ->post('/oauth/authorize', 'oauth.authorize.post', Controllers\AuthorizeController::class) + ->post('/oauth/authorize/redirect', 'oauth.authorize.redirect', Controllers\AuthorizeRedirectController::class) ->post('/oauth/token', 'oauth.token', Controllers\TokenController::class), (new Extend\Routes('api')) @@ -45,12 +46,16 @@ return [ ->patch('/oauth-scopes/{id}', 'oauth.scopes.update', Api\Controller\UpdateScopeController::class) ->delete('/oauth-scopes/{id}', 'oauth.scopes.delete', Api\Controller\DeleteScopeController::class) + ->get('/oauth-records', 'oauth.records.list', Api\Controller\ListRecordController::class) + ->get('/user', 'user.show', Controllers\ApiUserController::class), (new Extend\Settings) + ->serializeToForum('rhodes-island-oauth-center.display_mode', 'rhodes-island-oauth-center.display_mode') ->serializeToForum('rhodes-island-oauth-center.allow_implicit', 'rhodes-island-oauth-center.allow_implicit', 'boolval') ->serializeToForum('rhodes-island-oauth-center.enforce_state', 'rhodes-island-oauth-center.enforce_state', 'boolval') - ->serializeToForum('rhodes-island-oauth-center.require_exact_redirect_uri', 'rhodes-island-oauth-center.require_exact_redirect_uri', 'boolval'), + ->serializeToForum('rhodes-island-oauth-center.require_exact_redirect_uri', 'rhodes-island-oauth-center.require_exact_redirect_uri', 'boolval') + ->serializeToForum('rhodes-island-oauth-center.use_redirect_authorize', 'rhodes-island-oauth-center.use_redirect_authorize', 'boolval'), (new Extend\Middleware('api')) ->insertAfter(AuthenticateWithHeader::class, ResourceScopeMiddleware::class), diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts index 6ac3256..053a619 100644 --- a/js/src/admin/index.ts +++ b/js/src/admin/index.ts @@ -28,6 +28,11 @@ app.initializers.add('foskym/flarum-oauth-center', () => { setting: "rhodes-island-oauth-center.require_exact_redirect_uri", type: "boolean" }) + .registerSetting({ + label: app.translator.trans('rhodes-island-oauth-center.admin.settings.use_redirect_authorize'), + setting: "rhodes-island-oauth-center.use_redirect_authorize", + type: "boolean" + }) .registerSetting({ label: app.translator.trans('rhodes-island-oauth-center.admin.settings.access_lifetime'), setting: "rhodes-island-oauth-center.access_lifetime", diff --git a/js/src/common/extend.ts b/js/src/common/extend.ts index 8e49d35..d1529e7 100644 --- a/js/src/common/extend.ts +++ b/js/src/common/extend.ts @@ -1,9 +1,11 @@ import Extend from 'flarum/common/extenders'; import Client from "./models/Client"; import Scope from "./models/Scope"; +import Record from './models/Record'; export default [ new Extend.Store() .add('oauth-clients', Client) - .add('oauth-scopes', Scope), + .add('oauth-scopes', Scope) + .add('oauth-records', Record), ]; diff --git a/js/src/common/models/Client.ts b/js/src/common/models/Client.ts index 5c79ca7..d1d250b 100644 --- a/js/src/common/models/Client.ts +++ b/js/src/common/models/Client.ts @@ -4,11 +4,11 @@ export default class Client extends Model { client_id = Model.attribute('client_id'); client_secret = Model.attribute('client_secret'); redirect_uri = Model.attribute('redirect_uri'); - grant_types = Model.attribute('grant_types'); - scope = Model.attribute('scope'); + grant_types = Model.attribute('grant_types'); + scope = Model.attribute('scope'); user_id = Model.attribute('user_id'); client_name = Model.attribute('client_name'); - client_icon = Model.attribute('client_icon'); - client_desc = Model.attribute('client_desc'); - client_home = Model.attribute('client_home'); + client_icon = Model.attribute('client_icon'); + client_desc = Model.attribute('client_desc'); + client_home = Model.attribute('client_home'); } diff --git a/js/src/common/models/Record.ts b/js/src/common/models/Record.ts new file mode 100644 index 0000000..514670d --- /dev/null +++ b/js/src/common/models/Record.ts @@ -0,0 +1,8 @@ +import Model from 'flarum/common/Model'; +import type Client from './Client'; + +export default class Record extends Model { + client = Model.hasOne('client'); + user_id = Model.attribute('user_id'); + authorized_at = Model.attribute('authorized_at', Model.transformDate); +} diff --git a/js/src/forum/components/oauth/AuthorizePage.tsx b/js/src/forum/components/oauth/AuthorizePage.tsx index a0158e3..a6f6e48 100644 --- a/js/src/forum/components/oauth/AuthorizePage.tsx +++ b/js/src/forum/components/oauth/AuthorizePage.tsx @@ -86,7 +86,6 @@ export default class AuthorizePage extends IndexPage { finalScopes.push(definedScope); } } - console.log(wantedScopes, definedScopes, finalScopes); this.clientScopes = finalScopes; this.loading = false; @@ -103,7 +102,7 @@ export default class AuthorizePage extends IndexPage { super.oncreate(vnode); if (!app.session.user) { - app.modal.show(LogInModal); + setTimeout(() => app.modal.show(LogInModal)); // make sure mithril wont get double redraw } } @@ -136,14 +135,14 @@ export default class AuthorizePage extends IndexPage {

{app.translator.trans('rhodes-island-oauth-center.forum.authorize.app_text')}

- {(clientHome && clientHome != "" ? + {(clientHome ? - {clientIcon && clientIcon != "" ? : } + {clientIcon ? : }

{this.client!!.client_name()}

{this.client!!.client_desc()}
:
- {clientIcon && clientIcon != "" ? : } + {clientIcon ? : }

{this.client!!.client_name()}

{this.client!!.client_desc()}
)} @@ -176,16 +175,16 @@ export default class AuthorizePage extends IndexPage { }
-
- - -
+ {this.submitting ? : +
+ + +
+ } @@ -194,19 +193,37 @@ export default class AuthorizePage extends IndexPage { submit(authorized: boolean) { this.submitting = true; - app.request({ - method: 'POST', - url: '/oauth/authorize', - body: { - response_type: this.params!!.response_type, - client_id: this.params!!.client_id, - redirect_uri: this.params!!.redirect_uri, - state: this.params!!.state, - scope: this.params!!.scope, - is_authorized: authorized, + const form = { + response_type: this.params!!.response_type, + client_id: this.params!!.client_id, + redirect_uri: this.params!!.redirect_uri, + state: this.params!!.state, + scope: this.params!!.scope, + is_authorized: authorized, + }; + if (app.forum.attribute('rhodes-island-oauth-center.use_redirect_authorize')) { + const formEl = document.createElement("form"); + formEl.style.display = "none"; + formEl.action = "/oauth/authorize/redirect"; + formEl.method = "POST"; + for (const k in form) { + const el = document.createElement("input"); + el.name = k; + el.value = (form as any)[k]; + formEl.appendChild(el); } - }).then((params: any) => { - window.location.href = params.location; - }); + document.body.appendChild(formEl); + formEl.submit(); + } else { + app.request({ + method: 'POST', + url: '/oauth/authorize', + body: form + }).then((params: any) => { + window.location.href = params.redirect; + }).catch(() => { + // TODO: error handling + }); + } } } diff --git a/js/src/forum/components/user/AuthorizedPage.tsx b/js/src/forum/components/user/AuthorizedPage.tsx index daeacc8..8b82693 100644 --- a/js/src/forum/components/user/AuthorizedPage.tsx +++ b/js/src/forum/components/user/AuthorizedPage.tsx @@ -1,14 +1,65 @@ +import app from "flarum/common/app"; +import LoadingIndicator from "flarum/common/components/LoadingIndicator"; +import Placeholder from "flarum/common/components/Placeholder"; import UserPage from 'flarum/forum/components/UserPage'; +import type Client from "src/common/models/Client"; +import type Record from "src/common/models/Record"; + export default class AuthorizedPage extends UserPage { + records: Record[] = []; + loading = true; + nomore = false; + page = 0; + oninit(vnode: any) { super.oninit(vnode); this.loadUser(m.route.param('username')); + this.loadRecords(); } + + loadRecords() { + app.store.find('oauth-records', { page: this.page } as any).then(records => { + this.records = this.records.concat(records as unknown as Record[]); + this.loading = false; + if (this.records.length < 10) { + this.nomore = true; + } + m.redraw(); + }); + } + + loadMore() { + this.loadRecords(); + this.page += 1 + } + content() { + if (this.records.length == 0) { + return {app.translator.trans('rhodes-island-oauth-center.forum.authorized.no_records')}; + } + return (
+
+ {this.records.map(record => + { + const client = record.attribute("client") as Client; + return
  • +

    + {client.client_name} + +

    +

    {client.client_desc}

    +
    +
  • + } + )} +
    + {this.loading && }
    ); } diff --git a/js/src/forum/index.ts b/js/src/forum/index.ts deleted file mode 100644 index 7260327..0000000 --- a/js/src/forum/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import app from 'flarum/forum/app'; -import AuthorizePage from "./components/oauth/AuthorizePage"; -import AuthorizedPage from "./components/user/AuthorizedPage"; -app.initializers.add('foskym/flarum-oauth-center', () => { - app.routes['oauth.authorize'] = { - path: '/oauth/authorize', - component: AuthorizePage - }; - - app.routes['user.authorized'] = { - path: '/u/:username/authorized', - component: AuthorizedPage - }; -}); diff --git a/js/src/forum/index.tsx b/js/src/forum/index.tsx new file mode 100644 index 0000000..ae835ec --- /dev/null +++ b/js/src/forum/index.tsx @@ -0,0 +1,28 @@ +import app from 'flarum/forum/app'; +import { extend } from 'flarum/common/extend'; +import AuthorizePage from "./components/oauth/AuthorizePage"; +import AuthorizedPage from "./components/user/AuthorizedPage"; +import UserPage from 'flarum/forum/components/UserPage'; +import LinkButton from "flarum/common/components/LinkButton"; + +app.initializers.add('foskym/flarum-oauth-center', () => { + app.routes['oauth.authorize'] = { + path: '/oauth/authorize', + component: AuthorizePage + }; + + app.routes['user.authorized'] = { + path: '/u/:username/authorized', + component: AuthorizedPage + }; + + /*extend(UserPage.prototype, 'navItems', function (items) { + if (app.session.user?.id() === this.user?.id()) { + items.add( + 'authorized', + {app.translator.trans('rhodes-island-oauth-center.forum.page.label.authorized')}, + -110 + ); + } + });*/ // TODO: finish this +}); diff --git a/locale/en.yml b/locale/en.yml index e08603c..80dae89 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -42,7 +42,7 @@ rhodes-island-oauth-center: title: authorize: Authorize label: - authorized: Authorized Logs + authorized: Authorization Logs authorize: title: Third party application authorization description: Authorize third party application to use your identity diff --git a/locale/zh-Hans.yml b/locale/zh-Hans.yml index 01b2926..530a009 100644 --- a/locale/zh-Hans.yml +++ b/locale/zh-Hans.yml @@ -11,6 +11,7 @@ rhodes-island-oauth-center: allow_implicit: 允许隐式授权(response_type=token) enforce_state: 强制状态验证(state 参数) require_exact_redirect_uri: 需要精确的重定向 URI + use_redirect_authorize: 授权时直接进行跳转 clients: client_id: 应用 ID client_secret: 应用密钥 @@ -52,3 +53,5 @@ rhodes-island-oauth-center: agree: 授权 deny: 拒绝 not_logged_in: 请登录后再继续操作 + authorized: + no_records: 无授权记录 \ No newline at end of file diff --git a/migrations/2024_02_25_create_oauth_records_table.php b/migrations/2024_02_25_create_oauth_records_table.php new file mode 100644 index 0000000..6e1d809 --- /dev/null +++ b/migrations/2024_02_25_create_oauth_records_table.php @@ -0,0 +1,29 @@ + function (Builder $schema) { + if ($schema->hasTable('oauth_records')) { + return; + } + $schema->create('oauth_records', function (Blueprint $table) { + $table->increments('id'); + $table->string('client_id', 80); + $table->string('user_id', 80)->nullable(); + $table->timestamp('authorized_at'); + }); + }, + 'down' => function (Builder $schema) { + $schema->dropIfExists('oauth_records'); + }, +]; diff --git a/src/Api/Controller/CreateClientController.php b/src/Api/Controller/CreateClientController.php index 2f8eaea..dd4295c 100644 --- a/src/Api/Controller/CreateClientController.php +++ b/src/Api/Controller/CreateClientController.php @@ -6,6 +6,7 @@ use Flarum\Api\Controller\AbstractCreateController; use Flarum\Http\RequestUtil; use Illuminate\Support\Arr; use Psr\Http\Message\ServerRequestInterface; +use RhodesIsland\OAuthCenter\Utils; use Tobscure\JsonApi\Document; use RhodesIsland\OAuthCenter\Models\Client; use RhodesIsland\OAuthCenter\Api\Serializer\ClientSerializer; @@ -20,17 +21,9 @@ class CreateClientController extends AbstractCreateController $attributes = Arr::get($request->getParsedBody(), 'data.attributes'); - $validAttrs = [ - 'user_id' => $actor->id - ]; + $attrs = Utils::processAttributes($attributes, Utils::CLIENT_ATTRIBUTES, Utils::CLIENT_NULLABLE_ATTRIBUTES); + $attrs["user_id"] = $actor->id; - collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home']) - ->each(function (string $attribute) use (&$validAttrs, $attributes) { - if (($val = Arr::get($attributes, $attribute)) !== null) { - $validAttrs[$attribute] = $val; - } - }); - - return Client::create($validAttrs); + return Client::create($attrs); } } diff --git a/src/Api/Controller/ListRecordController.php b/src/Api/Controller/ListRecordController.php new file mode 100644 index 0000000..a8cf8c3 --- /dev/null +++ b/src/Api/Controller/ListRecordController.php @@ -0,0 +1,33 @@ +getQueryParams(), 'page', 0); + + $actor = RequestUtil::getActor($request); + $actor->assertRegistered(); + + $pageSize = 10; + $skip = $page * $pageSize; + $records = Record::where('user_id', $actor->id) + ->orderBy('authorized_at', 'desc') + ->skip($skip) + ->take($pageSize) + ->get(); + + return $records; + } +} diff --git a/src/Api/Controller/UpdateClientController.php b/src/Api/Controller/UpdateClientController.php index 00f0845..fe3a5a1 100644 --- a/src/Api/Controller/UpdateClientController.php +++ b/src/Api/Controller/UpdateClientController.php @@ -6,6 +6,7 @@ use Flarum\Api\Controller\AbstractListController; use Flarum\Http\RequestUtil; use Illuminate\Support\Arr; use Psr\Http\Message\ServerRequestInterface; +use RhodesIsland\OAuthCenter\Utils; use Tobscure\JsonApi\Document; use RhodesIsland\OAuthCenter\Models\Client; use RhodesIsland\OAuthCenter\Api\Serializer\ClientSerializer; @@ -23,12 +24,11 @@ class UpdateClientController extends AbstractListController $attributes = Arr::get($request->getParsedBody(), 'data.attributes', []); - collect(['client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'client_name', 'client_desc', 'client_icon', 'client_home']) - ->each(function (string $attribute) use ($client, $attributes) { - if (($val = Arr::get($attributes, $attribute)) !== null) { - $client->$attribute = $val; - } - }); + $attrs = Utils::processAttributes($attributes, Utils::CLIENT_ATTRIBUTES, Utils::CLIENT_NULLABLE_ATTRIBUTES); + + foreach ($attrs as $k => $v) { + $client[$k] = $v; + } $client->save(); diff --git a/src/Api/Serializer/RecordSerializer.php b/src/Api/Serializer/RecordSerializer.php new file mode 100644 index 0000000..0c0d731 --- /dev/null +++ b/src/Api/Serializer/RecordSerializer.php @@ -0,0 +1,30 @@ + $model->id, + "client" => $model->client, + "user_id" => $model->user_id, + "authorized_at" => $model->authorized_at + ]; + } +} diff --git a/src/Controllers/ApiUserController.php b/src/Controllers/ApiUserController.php index 5c7f8d4..c51c8fc 100644 --- a/src/Controllers/ApiUserController.php +++ b/src/Controllers/ApiUserController.php @@ -9,16 +9,12 @@ * file that was distributed with this source code. */ namespace RhodesIsland\OAuthCenter\Controllers; -use Flarum\User\User; use Flarum\Http\RequestUtil; -use RhodesIsland\OAuthCenter\OAuth; -use Illuminate\Support\Arr; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Laminas\Diactoros\Response\JsonResponse; use Flarum\Settings\SettingsRepositoryInterface; -use Flarum\Group\Group; class ApiUserController implements RequestHandlerInterface { diff --git a/src/Controllers/AuthorizeController.php b/src/Controllers/AuthorizeController.php index 9b29a57..05bc9bc 100644 --- a/src/Controllers/AuthorizeController.php +++ b/src/Controllers/AuthorizeController.php @@ -9,8 +9,8 @@ * file that was distributed with this source code. */ namespace RhodesIsland\OAuthCenter\Controllers; -use Flarum\User\User; use Flarum\Http\RequestUtil; +use RhodesIsland\OAuthCenter\Models\Record; use RhodesIsland\OAuthCenter\OAuth; use Illuminate\Support\Arr; use Psr\Http\Message\ResponseInterface; @@ -18,7 +18,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Laminas\Diactoros\Response\JsonResponse; use Flarum\Settings\SettingsRepositoryInterface; -use Flarum\Group\Group; class AuthorizeController implements RequestHandlerInterface { @@ -34,6 +33,7 @@ class AuthorizeController implements RequestHandlerInterface $actor->assertRegistered(); if (!$actor->hasPermission('rhodes-island-oauth-center.use-oauth')) { + // TODO: i18n description return new JsonResponse([ 'error' => 'no_permission', 'error_description' => 'Don\'t have the permissions of oauth' ]); } @@ -45,18 +45,23 @@ class AuthorizeController implements RequestHandlerInterface $response = $oauth->response(); if (!$server->validateAuthorizeRequest($request, $response)) { - return new JsonResponse(json_decode($response->getResponseBody(), true)); + return new JsonResponse($response->getParameters(), $response->getStatusCode(), $response->getHttpHeaders()); } $is_authorized = Arr::get($params, 'is_authorized', 0); $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id); + if ($is_authorized) { -// $code = substr($response->getHttpHeader('Location'), strpos($response->getHttpHeader('Location'), 'code=') + 5, 40); + /*Record::create([ + 'client_id' => Arr::get($params, 'client_id'), + 'user_id' => $actor->id, + 'authorized_at' => date('Y-m-d H:i:s') + ]);*/ return new JsonResponse([ - 'location' => $response->getHttpHeader('Location') + "redirect" => $response->getHttpHeader("Location") ]); } - return new JsonResponse(json_decode($response->getResponseBody(), true)); + return new JsonResponse($response->getParameters(), $response->getStatusCode(), $response->getHttpHeaders()); } } diff --git a/src/Controllers/AuthorizeRedirectController.php b/src/Controllers/AuthorizeRedirectController.php new file mode 100644 index 0000000..1565433 --- /dev/null +++ b/src/Controllers/AuthorizeRedirectController.php @@ -0,0 +1,65 @@ +settings = $settings; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $actor = RequestUtil::getActor($request); + $actor->assertRegistered(); + + if (!$actor->hasPermission('rhodes-island-oauth-center.use-oauth')) { + // TODO: better error response + return new HtmlResponse('Don\'t have the permissions of oauth'); + } + + $params = $request->getParsedBody(); + + $oauth = new OAuth($this->settings); + $server = $oauth->server(); + $request = $oauth->request()::createFromGlobals(); + $response = $oauth->response(); + + if (!$server->validateAuthorizeRequest($request, $response)) { + return new JsonResponse($response->getParameters(), $response->getStatusCode(), $response->getHttpHeaders()); + } + + $is_authorized = (bool) Arr::get($params, 'is_authorized', 0); + $server->handleAuthorizeRequest($request, $response, $is_authorized, $actor->id); + + if ($is_authorized) { + /*Record::create([ + 'client_id' => Arr::get($params, 'client_id'), + 'user_id' => $actor->id, + 'authorized_at' => date('Y-m-d H:i:s') + ]);*/ + } + + return new JsonResponse($response->getParameters(), $response->getStatusCode(), $response->getHttpHeaders()); + } +} diff --git a/src/Middlewares/UnsetCsrfMiddleware.php b/src/Middlewares/UnsetCsrfMiddleware.php index a5fad9c..79e396a 100644 --- a/src/Middlewares/UnsetCsrfMiddleware.php +++ b/src/Middlewares/UnsetCsrfMiddleware.php @@ -2,28 +2,18 @@ namespace RhodesIsland\OAuthCenter\Middlewares; -use Flarum\Foundation\ErrorHandling\ExceptionHandler\IlluminateValidationExceptionHandler; -use Flarum\Foundation\ErrorHandling\JsonApiFormatter; -use RhodesIsland\OAuthCenter\OAuth; -use RhodesIsland\OAuthCenter\Storage; -use Illuminate\Support\Arr; -use Illuminate\Validation\ValidationException; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Flarum\Http\RequestUtil; -use Flarum\Api\JsonApiResponse; -use Tobscure\JsonApi\Document; -use Tobscure\JsonApi\Exception\Handler\ResponseBag; -use RhodesIsland\OAuthCenter\Models\Scope; class UnsetCsrfMiddleware implements MiddlewareInterface { public function process(Request $request, RequestHandlerInterface $handler): Response { $uri = [ '/oauth/token', + '/oauth/authorize/redirect' ]; $path = $request->getUri()->getPath(); if (in_array($path, $uri)) { diff --git a/src/Models/Client.php b/src/Models/Client.php index 9e197d2..d648b5b 100644 --- a/src/Models/Client.php +++ b/src/Models/Client.php @@ -27,4 +27,9 @@ class Client extends AbstractModel return $client; } + + public function record() + { + return $this->hasMany(Record::class, 'client_id', 'client_id'); + } } diff --git a/src/Models/Record.php b/src/Models/Record.php new file mode 100644 index 0000000..8c2291f --- /dev/null +++ b/src/Models/Record.php @@ -0,0 +1,24 @@ +belongsTo(Client::class, 'client_id', 'client_id'); + } +} diff --git a/src/Storage.php b/src/Storage.php index e9423db..b33fcc8 100644 --- a/src/Storage.php +++ b/src/Storage.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ namespace RhodesIsland\OAuthCenter; -use Flarum\Extend\Model; + use Flarum\User\User; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; use OAuth2\OpenID\Storage\UserClaimsInterface; diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..296a292 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,40 @@ +each(function (string $attribute) use (&$result, $attributes) { + if (($val = Arr::get($attributes, $attribute)) !== null) { + $result[$attribute] = $val; + } + }); + collect($nullableAttributes)->each(function (string $attribute) use (&$result, $attributes) { + if (($val = Arr::get($attributes, $attribute)) !== null) { + $result[$attribute] = Utils::nullIfEmpty($val); + } + }); + return $result; + } +} \ No newline at end of file