diff --git a/app/Bus/Events/System/SystemCheckedForUpdatesEvent.php b/app/Bus/Events/System/SystemCheckedForUpdatesEvent.php new file mode 100644 index 00000000..e8da9d00 --- /dev/null +++ b/app/Bus/Events/System/SystemCheckedForUpdatesEvent.php @@ -0,0 +1,22 @@ + + */ +final class SystemCheckedForUpdatesEvent implements SystemEventInterface +{ + // +} diff --git a/app/Bus/Events/System/SystemEventInterface.php b/app/Bus/Events/System/SystemEventInterface.php new file mode 100644 index 00000000..d195fa58 --- /dev/null +++ b/app/Bus/Events/System/SystemEventInterface.php @@ -0,0 +1,24 @@ + + */ +interface SystemEventInterface extends EventInterface +{ + // +} diff --git a/app/Bus/Events/System/SystemWasInstalledEvent.php b/app/Bus/Events/System/SystemWasInstalledEvent.php new file mode 100644 index 00000000..6f333f7c --- /dev/null +++ b/app/Bus/Events/System/SystemWasInstalledEvent.php @@ -0,0 +1,22 @@ + + */ +final class SystemWasInstalledEvent implements SystemEventInterface +{ + // +} diff --git a/app/Bus/Events/System/SystemWasResetEvent.php b/app/Bus/Events/System/SystemWasResetEvent.php new file mode 100644 index 00000000..a2146c88 --- /dev/null +++ b/app/Bus/Events/System/SystemWasResetEvent.php @@ -0,0 +1,22 @@ + + */ +final class SystemWasResetEvent implements SystemEventInterface +{ + // +} diff --git a/app/Bus/Events/System/SystemWasUpdatedEvent.php b/app/Bus/Events/System/SystemWasUpdatedEvent.php new file mode 100644 index 00000000..51572888 --- /dev/null +++ b/app/Bus/Events/System/SystemWasUpdatedEvent.php @@ -0,0 +1,22 @@ + + */ +final class SystemWasUpdatedEvent implements SystemEventInterface +{ + // +} diff --git a/app/Bus/Events/User/UserDisabledTwoAuthEvent.php b/app/Bus/Events/User/UserDisabledTwoAuthEvent.php new file mode 100644 index 00000000..22316bda --- /dev/null +++ b/app/Bus/Events/User/UserDisabledTwoAuthEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserDisabledTwoAuthEvent implements UserEventInterface +{ + /** + * The user that disabled two auth. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user disabled two auth event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Bus/Events/User/UserEnabledTwoAuthEvent.php b/app/Bus/Events/User/UserEnabledTwoAuthEvent.php new file mode 100644 index 00000000..ad3a56b7 --- /dev/null +++ b/app/Bus/Events/User/UserEnabledTwoAuthEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserEnabledTwoAuthEvent implements UserEventInterface +{ + /** + * The user that enabled two auth. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user enabled two auth event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Bus/Events/User/UserFailedTwoAuthEvent.php b/app/Bus/Events/User/UserFailedTwoAuthEvent.php new file mode 100644 index 00000000..c416e317 --- /dev/null +++ b/app/Bus/Events/User/UserFailedTwoAuthEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserFailedTwoAuthEvent implements UserEventInterface +{ + /** + * The user that failed two auth. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user failed two auth event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Bus/Events/User/UserLoggedInEvent.php b/app/Bus/Events/User/UserLoggedInEvent.php new file mode 100644 index 00000000..a10d1547 --- /dev/null +++ b/app/Bus/Events/User/UserLoggedInEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserLoggedInEvent implements UserEventInterface +{ + /** + * The user that logged in. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user logged in event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Bus/Events/User/UserLoggedOutEvent.php b/app/Bus/Events/User/UserLoggedOutEvent.php new file mode 100644 index 00000000..9c51c6e3 --- /dev/null +++ b/app/Bus/Events/User/UserLoggedOutEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserLoggedOutEvent implements UserEventInterface +{ + /** + * The user that logged out. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user logged out event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Bus/Events/User/UserPassedTwoAuthEvent.php b/app/Bus/Events/User/UserPassedTwoAuthEvent.php new file mode 100644 index 00000000..be775510 --- /dev/null +++ b/app/Bus/Events/User/UserPassedTwoAuthEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserPassedTwoAuthEvent implements UserEventInterface +{ + /** + * The user that passed two auth. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user passed two auth event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Bus/Events/User/UserRegeneratedApiTokenEvent.php b/app/Bus/Events/User/UserRegeneratedApiTokenEvent.php new file mode 100644 index 00000000..4797608f --- /dev/null +++ b/app/Bus/Events/User/UserRegeneratedApiTokenEvent.php @@ -0,0 +1,41 @@ + + */ +final class UserRegeneratedApiTokenEvent implements UserEventInterface +{ + /** + * The user that regenerated their api token. + * + * @var \CachetHQ\Cachet\Models\User + */ + public $user; + + /** + * Create a new user regenerated api token event instance. + * + * @param \CachetHQ\Cachet\Models\User $user + * + * @return void + */ + public function __construct(User $user) + { + $this->user = $user; + } +} diff --git a/app/Foundation/Providers/EventServiceProvider.php b/app/Foundation/Providers/EventServiceProvider.php index aaff317e..9b15570f 100644 --- a/app/Foundation/Providers/EventServiceProvider.php +++ b/app/Foundation/Providers/EventServiceProvider.php @@ -87,6 +87,39 @@ class EventServiceProvider extends ServiceProvider 'CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasVerifiedEvent' => [ // ], + 'CachetHQ\Cachet\Bus\Events\System\SystemCheckedForUpdatesEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\System\SystemWasInstalledEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\System\SystemWasResetEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\System\SystemWasUpdatedEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserDisabledTwoAuthEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserEnabledTwoAuthEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserFailedTwoAuthEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserLoggedInEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserLoggedOutEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserPassedTwoAuthEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\User\UserRegeneratedApiTokenEvent' => [ + // + ], 'CachetHQ\Cachet\Bus\Events\User\UserWasAddedEvent' => [ // ], diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index b6d63570..c267ccb4 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -11,6 +11,10 @@ namespace CachetHQ\Cachet\Http\Controllers; +use CachetHQ\Cachet\Bus\Events\User\UserFailedTwoAuthEvent; +use CachetHQ\Cachet\Bus\Events\User\UserLoggedInEvent; +use CachetHQ\Cachet\Bus\Events\User\UserLoggedOutEvent; +use CachetHQ\Cachet\Bus\Events\User\UserPassedTwoAuthEvent; use GrahamCampbell\Binput\Facades\Binput; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Auth; @@ -62,6 +66,8 @@ class AuthController extends Controller // We probably want to add support for "Remember me" here. Auth::attempt($loginData); + event(new UserLoggedInEvent(Auth::user())); + return Redirect::intended('dashboard'); } @@ -99,8 +105,14 @@ class AuthController extends Controller $valid = Google2FA::verifyKey(Auth::user()->google_2fa_secret, $code); if ($valid) { + event(new UserPassedTwoAuthEvent(Auth::user())); + + event(new UserLoggedInEvent(Auth::user())); + return Redirect::intended('dashboard'); } else { + event(new UserFailedTwoAuthEvent(Auth::user())); + // Failed login, log back out. Auth::logout(); @@ -118,6 +130,8 @@ class AuthController extends Controller */ public function logoutAction() { + event(new UserLoggedOutEvent(Auth::user())); + Auth::logout(); return Redirect::to('/'); diff --git a/app/Http/Controllers/Dashboard/UserController.php b/app/Http/Controllers/Dashboard/UserController.php index 882551b7..d665346e 100644 --- a/app/Http/Controllers/Dashboard/UserController.php +++ b/app/Http/Controllers/Dashboard/UserController.php @@ -12,6 +12,9 @@ namespace CachetHQ\Cachet\Http\Controllers\Dashboard; use AltThree\Validator\ValidationException; +use CachetHQ\Cachet\Bus\Events\User\UserDisabledTwoAuthEvent; +use CachetHQ\Cachet\Bus\Events\User\UserEnabledTwoAuthEvent; +use CachetHQ\Cachet\Bus\Events\User\UserRegeneratedApiTokenEvent; use CachetHQ\Cachet\Models\User; use GrahamCampbell\Binput\Facades\Binput; use Illuminate\Routing\Controller; @@ -46,8 +49,10 @@ class UserController extends Controller // Let's enable/disable auth if ($enable2FA && !Auth::user()->hasTwoFactor) { + event(new UserEnabledTwoAuthEvent(Auth::user())); $userData['google_2fa_secret'] = Google2FA::generateSecretKey(); } elseif (!$enable2FA) { + event(new UserDisabledTwoAuthEvent(Auth::user())); $userData['google_2fa_secret'] = ''; } @@ -76,6 +81,8 @@ class UserController extends Controller $user->api_key = User::generateApiKey(); $user->save(); + event(new UserRegeneratedApiTokenEvent($user)); + return Redirect::route('dashboard.user'); } } diff --git a/app/Integrations/Contracts/System.php b/app/Integrations/Contracts/System.php index f427b8e4..e64f4262 100644 --- a/app/Integrations/Contracts/System.php +++ b/app/Integrations/Contracts/System.php @@ -24,4 +24,11 @@ interface System * @return array */ public function getStatus(); + + /** + * Get the cachet version. + * + * @return string + */ + public function getVersion(); } diff --git a/app/Integrations/Core/System.php b/app/Integrations/Core/System.php index a44af6e8..77c55e57 100644 --- a/app/Integrations/Core/System.php +++ b/app/Integrations/Core/System.php @@ -30,8 +30,8 @@ class System implements SystemContract public function getStatus() { $enabledScope = Component::enabled(); - $totalComponents = Component::enabled()->count(); - $majorOutages = Component::enabled()->status(4)->count(); + $totalComponents = $enabledScope->count(); + $majorOutages = $enabledScope->status(4)->count(); $isMajorOutage = $totalComponents ? ($majorOutages / $totalComponents) >= 0.5 : false; // Default data @@ -47,7 +47,7 @@ class System implements SystemContract 'system_message' => trans_choice('cachet.service.major', $totalComponents), 'favicon' => 'favicon-high-alert', ]; - } elseif (Component::enabled()->notStatus(1)->count() === 0) { + } elseif ($enabledScope->notStatus(1)->count() === 0) { // If all our components are ok, do we have any non-fixed incidents? $incidents = Incident::notScheduled()->orderBy('created_at', 'desc')->get()->filter(function ($incident) { return $incident->status > 0; @@ -61,10 +61,20 @@ class System implements SystemContract 'favicon' => 'favicon', ]; } - } elseif (Component::enabled()->whereIn('status', [2, 3])->count() > 0) { + } elseif ($enabledScope->whereIn('status', [2, 3])->count() > 0) { $status['favicon'] = 'favicon-medium-alert'; } return $status; } + + /** + * Get the cachet version. + * + * @return string + */ + public function getVersion() + { + return CACHET_VERSION; + } } diff --git a/app/Integrations/GitHub/Releases.php b/app/Integrations/GitHub/Releases.php index b1192723..8d7935ad 100644 --- a/app/Integrations/GitHub/Releases.php +++ b/app/Integrations/GitHub/Releases.php @@ -11,6 +11,7 @@ namespace CachetHQ\Cachet\Integrations\GitHub; +use CachetHQ\Cachet\Bus\Events\System\SystemCheckedForUpdatesEvent; use CachetHQ\Cachet\Integrations\Contracts\Releases as ReleasesContract; use GuzzleHttp\Client; use Illuminate\Contracts\Cache\Repository; @@ -82,6 +83,8 @@ class Releases implements ReleasesContract $headers['OAUTH-TOKEN'] = $this->token; } + event(new SystemCheckedForUpdatesEvent()); + return json_decode((new Client())->get($this->url, [ 'headers' => $headers, ])->getBody(), true); diff --git a/app/Subscribers/CommandSubscriber.php b/app/Subscribers/CommandSubscriber.php index accddefe..2aaa320d 100644 --- a/app/Subscribers/CommandSubscriber.php +++ b/app/Subscribers/CommandSubscriber.php @@ -11,6 +11,9 @@ namespace CachetHQ\Cachet\Subscribers; +use CachetHQ\Cachet\Bus\Events\System\SystemWasInstalledEvent; +use CachetHQ\Cachet\Bus\Events\System\SystemWasResetEvent; +use CachetHQ\Cachet\Bus\Events\System\SystemWasUpdatedEvent; use CachetHQ\Cachet\Settings\Cache; use Carbon\Carbon; use Exception; @@ -63,19 +66,67 @@ class CommandSubscriber */ public function subscribe(Dispatcher $events) { - $events->listen('command.installing', __CLASS__.'@fire', 5); - $events->listen('command.updating', __CLASS__.'@fire', 5); - $events->listen('command.resetting', __CLASS__.'@fire', 5); + $events->listen('command.installing', __CLASS__.'@fireInstallingCommand', 5); + $events->listen('command.updating', __CLASS__.'@fireUpdatingCommand', 5); + $events->listen('command.resetting', __CLASS__.'@fireResettingCommand', 5); } /** - * Clear the settings cache, and backup the databases. + * Fire the installing command. * * @param \Illuminate\Console\Command $command * * @return void */ - public function fire(Command $command) + public function fireInstallingCommand(Command $command) + { + $this->handleMainCommand($command); + + event(new SystemWasInstalledEvent()); + + $command->success('System was installed!'); + } + + /** + * Fire the updating command. + * + * @param \Illuminate\Console\Command $command + * + * @return void + */ + public function fireUpdatingCommand(Command $command) + { + $this->handleMainCommand($command); + + event(new SystemWasUpdatedEvent()); + + $command->success('System was updated!'); + } + + /** + * Fire the resetting command. + * + * @param \Illuminate\Console\Command $command + * + * @return void + */ + public function fireResettingCommand(Command $command) + { + $this->handleMainCommand($command); + + event(new SystemWasResetEvent()); + + $command->success('System was reset!'); + } + + /** + * Handle the main bulk of the command, clear the settings and backup the database. + * + * @param \Illuminate\Console\Command $command + * + * @return void + */ + protected function handleMainCommand(Command $command) { $command->line('Clearing settings cache...'); diff --git a/tests/Bus/Events/System/AbstractSystemEventTestCase.php b/tests/Bus/Events/System/AbstractSystemEventTestCase.php new file mode 100644 index 00000000..b500edcc --- /dev/null +++ b/tests/Bus/Events/System/AbstractSystemEventTestCase.php @@ -0,0 +1,31 @@ + + */ +abstract class AbstractSystemEventTestCase extends AbstractTestCase +{ + use EventTrait; + + protected function getEventInterfaces() + { + return [SystemEventInterface::class]; + } +} diff --git a/tests/Bus/Events/System/SystemCheckedForUpdatesEventTest.php b/tests/Bus/Events/System/SystemCheckedForUpdatesEventTest.php new file mode 100644 index 00000000..45db84cd --- /dev/null +++ b/tests/Bus/Events/System/SystemCheckedForUpdatesEventTest.php @@ -0,0 +1,35 @@ + + */ +class SystemCheckedForUpdatesEventTest extends AbstractSystemEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = []; + $object = new SystemCheckedForUpdatesEvent(); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/System/SystemWasInstalledEventTest.php b/tests/Bus/Events/System/SystemWasInstalledEventTest.php new file mode 100644 index 00000000..1cdd0841 --- /dev/null +++ b/tests/Bus/Events/System/SystemWasInstalledEventTest.php @@ -0,0 +1,35 @@ + + */ +class SystemWasInstalledEventTest extends AbstractSystemEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = []; + $object = new SystemWasInstalledEvent(); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/System/SystemWasResetEventTest.php b/tests/Bus/Events/System/SystemWasResetEventTest.php new file mode 100644 index 00000000..35e593af --- /dev/null +++ b/tests/Bus/Events/System/SystemWasResetEventTest.php @@ -0,0 +1,35 @@ + + */ +class SystemWasResetEventTest extends AbstractSystemEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = []; + $object = new SystemWasResetEvent(); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/System/SystemWasUpdatedEventTest.php b/tests/Bus/Events/System/SystemWasUpdatedEventTest.php new file mode 100644 index 00000000..2d89a4ed --- /dev/null +++ b/tests/Bus/Events/System/SystemWasUpdatedEventTest.php @@ -0,0 +1,35 @@ + + */ +class SystemWasUpdatedEventTest extends AbstractSystemEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = []; + $object = new SystemWasUpdatedEvent(); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserDisabledTwoAuthEventTest.php b/tests/Bus/Events/User/UserDisabledTwoAuthEventTest.php new file mode 100644 index 00000000..464c4181 --- /dev/null +++ b/tests/Bus/Events/User/UserDisabledTwoAuthEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserDisabledTwoAuthEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserDisabledTwoAuthEvent($params['user']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserEnabledTwoAuthEventTest.php b/tests/Bus/Events/User/UserEnabledTwoAuthEventTest.php new file mode 100644 index 00000000..0583ff74 --- /dev/null +++ b/tests/Bus/Events/User/UserEnabledTwoAuthEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserEnabledTwoAuthEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserEnabledTwoAuthEvent($params['user']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserFailedTwoAuthEventTest.php b/tests/Bus/Events/User/UserFailedTwoAuthEventTest.php new file mode 100644 index 00000000..4f892a24 --- /dev/null +++ b/tests/Bus/Events/User/UserFailedTwoAuthEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserFailedTwoAuthEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserFailedTwoAuthEvent($params['user']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserLoggedInEventTest.php b/tests/Bus/Events/User/UserLoggedInEventTest.php new file mode 100644 index 00000000..a8f9f84a --- /dev/null +++ b/tests/Bus/Events/User/UserLoggedInEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserLoggedInEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserLoggedInEvent($params['user']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserLoggedOutEventTest.php b/tests/Bus/Events/User/UserLoggedOutEventTest.php new file mode 100644 index 00000000..34c16980 --- /dev/null +++ b/tests/Bus/Events/User/UserLoggedOutEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserLoggedOutEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserLoggedOutEvent($params['user']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserPassedTwoAuthEventTest.php b/tests/Bus/Events/User/UserPassedTwoAuthEventTest.php new file mode 100644 index 00000000..d03dca4b --- /dev/null +++ b/tests/Bus/Events/User/UserPassedTwoAuthEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserPassedTwoAuthEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserPassedTwoAuthEvent($params['user']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/User/UserRegeneratedApiTokenEventTest.php b/tests/Bus/Events/User/UserRegeneratedApiTokenEventTest.php new file mode 100644 index 00000000..98a3b72d --- /dev/null +++ b/tests/Bus/Events/User/UserRegeneratedApiTokenEventTest.php @@ -0,0 +1,36 @@ + + */ +class UserRegeneratedApiTokenEventTest extends AbstractUserEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['user' => new User()]; + $object = new UserRegeneratedApiTokenEvent($params['user']); + + return compact('params', 'object'); + } +}