From 778664b20a62593ffc9ab0e9f66e7b06df122a0c Mon Sep 17 00:00:00 2001 From: James Brooks Date: Wed, 3 Aug 2016 17:44:21 +0100 Subject: [PATCH] Added new Beacon handling code --- .env.example | 2 + .../Jobs/System/SendBeaconJobHandler.php | 64 ++++++++++ app/Bus/Jobs/System/SendBeaconJob.php | 24 ++++ app/Console/Commands/BeaconCommand.php | 47 +++++++ app/Console/Kernel.php | 4 + .../Providers/IntegrationServiceProvider.php | 17 +++ app/Integrations/Contracts/Beacon.php | 34 ++++++ app/Integrations/Core/Beacon.php | 115 ++++++++++++++++++ app/Models/User.php | 25 ++++ app/Settings/Repository.php | 17 +++ config/cachet.php | 13 ++ tests/Bus/Jobs/JobExistenceTest.php | 30 +++++ tests/Bus/Jobs/System/SendBeaconJobTest.php | 48 ++++++++ .../IntegrationServiceProviderTest.php | 6 + 14 files changed, 446 insertions(+) create mode 100644 app/Bus/Handlers/Jobs/System/SendBeaconJobHandler.php create mode 100644 app/Bus/Jobs/System/SendBeaconJob.php create mode 100644 app/Console/Commands/BeaconCommand.php create mode 100644 app/Integrations/Contracts/Beacon.php create mode 100644 app/Integrations/Core/Beacon.php create mode 100644 tests/Bus/Jobs/JobExistenceTest.php create mode 100644 tests/Bus/Jobs/System/SendBeaconJobTest.php diff --git a/.env.example b/.env.example index 7dfca241..c68962a3 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,8 @@ DB_PREFIX=null CACHE_DRIVER=file SESSION_DRIVER=file QUEUE_DRIVER=sync + +CACHET_BEACON=true CACHET_EMOJI=false MAIL_DRIVER=smtp diff --git a/app/Bus/Handlers/Jobs/System/SendBeaconJobHandler.php b/app/Bus/Handlers/Jobs/System/SendBeaconJobHandler.php new file mode 100644 index 00000000..05733540 --- /dev/null +++ b/app/Bus/Handlers/Jobs/System/SendBeaconJobHandler.php @@ -0,0 +1,64 @@ + + */ +class SendBeaconJobHandler +{ + /** + * The beacon instance. + * + * @var \CachetHQ\Cachet\Integrations\Contracts\Beacon + */ + protected $beacon; + + /** + * Create a new send beacon job handler instance. + * + * @param \CachetHQ\Cachet\Integrations\Contracts\Beacon $beacon + * + * @return void + */ + public function __construct(Beacon $beacon) + { + $this->beacon = $beacon; + } + + /** + * Handle the send beacon job. + * + * @param \CachetHQ\Cachet\Bus\Jobs\SendBeaconJob $job + * + * @return void + */ + public function handle(SendBeaconJob $job) + { + // Don't send anything if the installation explicitly prevents us. + if (!$this->beacon->enabled()) { + return; + } + + try { + $this->beacon->send(); + } catch (Exception $e) { + // + } + } +} diff --git a/app/Bus/Jobs/System/SendBeaconJob.php b/app/Bus/Jobs/System/SendBeaconJob.php new file mode 100644 index 00000000..577aa810 --- /dev/null +++ b/app/Bus/Jobs/System/SendBeaconJob.php @@ -0,0 +1,24 @@ + + */ +final class SendBeaconJob implements ShouldQueue +{ + // +} diff --git a/app/Console/Commands/BeaconCommand.php b/app/Console/Commands/BeaconCommand.php new file mode 100644 index 00000000..03391eb4 --- /dev/null +++ b/app/Console/Commands/BeaconCommand.php @@ -0,0 +1,47 @@ + + */ +class BeaconCommand extends Command +{ + /** + * The console command name. + * + * @var string + */ + protected $name = 'cachet:beacon'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Send a beacon to the Cachet server.'; + + /** + * Execute the console command. + * + * @return void + */ + public function fire() + { + dispatch(new SendBeaconJob()); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index d1c9686b..d1acd666 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -11,6 +11,7 @@ namespace CachetHQ\Cachet\Console; +use CachetHQ\Cachet\Console\Commands\BeaconCommand; use CachetHQ\Cachet\Console\Commands\DemoMetricPointSeederCommand; use CachetHQ\Cachet\Console\Commands\DemoSeederCommand; use Illuminate\Console\Scheduling\Schedule; @@ -24,6 +25,7 @@ class Kernel extends ConsoleKernel * @var array */ protected $commands = [ + BeaconCommand::class, DemoMetricPointSeederCommand::class, DemoSeederCommand::class, ]; @@ -38,5 +40,7 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule) { $schedule->command('queue:work --sleep=3 --tries=3')->everyMinute(); + + $schedule->command('cachet:beacon')->twiceDaily('00:00', '12:00'); } } diff --git a/app/Foundation/Providers/IntegrationServiceProvider.php b/app/Foundation/Providers/IntegrationServiceProvider.php index aa2c9f95..4f4ba9e0 100644 --- a/app/Foundation/Providers/IntegrationServiceProvider.php +++ b/app/Foundation/Providers/IntegrationServiceProvider.php @@ -11,10 +11,12 @@ namespace CachetHQ\Cachet\Foundation\Providers; +use CachetHQ\Cachet\Integrations\Contracts\Beacon as BeaconContract; use CachetHQ\Cachet\Integrations\Contracts\Credits as CreditsContract; use CachetHQ\Cachet\Integrations\Contracts\Feed as FeedContract; use CachetHQ\Cachet\Integrations\Contracts\Releases as ReleasesContract; use CachetHQ\Cachet\Integrations\Contracts\System as SystemContract; +use CachetHQ\Cachet\Integrations\Core\Beacon; use CachetHQ\Cachet\Integrations\Core\Credits; use CachetHQ\Cachet\Integrations\Core\Feed; use CachetHQ\Cachet\Integrations\Core\System; @@ -36,6 +38,7 @@ class IntegrationServiceProvider extends ServiceProvider */ public function register() { + $this->registerBeacon(); $this->registerCredits(); $this->registerFeed(); $this->registerSystem(); @@ -43,6 +46,20 @@ class IntegrationServiceProvider extends ServiceProvider $this->registerReleases(); } + /** + * Register the beacon class. + * + * @return void + */ + protected function registerBeacon() + { + $this->app->singleton(BeaconContract::class, function ($app) { + $config = $app['config']; + + return new Beacon($config); + }); + } + /** * Register the credits class. * diff --git a/app/Integrations/Contracts/Beacon.php b/app/Integrations/Contracts/Beacon.php new file mode 100644 index 00000000..61571403 --- /dev/null +++ b/app/Integrations/Contracts/Beacon.php @@ -0,0 +1,34 @@ + + */ +interface Beacon +{ + /** + * Has the install enabled Cachet beacon? + * + * @return bool + */ + public function enabled(); + + /** + * Send a beacon to our server. + * + * @return void + */ + public function send(); +} diff --git a/app/Integrations/Core/Beacon.php b/app/Integrations/Core/Beacon.php new file mode 100644 index 00000000..4ce40696 --- /dev/null +++ b/app/Integrations/Core/Beacon.php @@ -0,0 +1,115 @@ + + */ +class Beacon implements BeaconContract +{ + /** + * The beacon url. + * + * @var string + */ + const URL = 'https://cachethq.io/beacon'; + + /** + * The illuminate config instance. + * + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * Create a new beacon instance. + * + * @param \Illuminate\Contracts\Config\Repository $config + * + * @return void + */ + public function __construct(Repository $config) + { + $this->config = $config; + } + + /** + * Has the install enabled Cachet beacon? + * + * @return bool + */ + public function enabled() + { + return $this->config->get('cachet.beacon'); + } + + /** + * Send a beacon to our server. + * + * @return void + */ + public function send() + { + // We don't want any accidental sending of beacons if the installation has explicitly said no. + if (!$this->enabled()) { + return; + } + + if (!($contactEmail = User::admins()->active()->first()->email)) { + $contactEmail = null; + } + + $setting = app(Setting::class); + + if (!$installId = $setting->get('install_id', null)) { + $installId = sha1(str_random(20)); + + $setting->set('install_id', $installId); + } + + $payload = [ + 'install_id' => $installId, + 'version' => CACHET_VERSION, + 'docker' => $this->config->get('cachet.is_docker'), + 'database' => $this->config->get('database.default'), + 'contact_email' => $contactEmail, + 'data' => [ + 'components' => Component::all()->count(), + 'incidents' => Incident::all()->count(), + 'metrics' => Metric::all()->count(), + 'users' => User::all()->count(), + ], + ]; + + try { + $client = new Client(); + $client->post(self::URL, [ + 'headers' => ['Accept' => 'application/json', 'User-Agent' => defined('CACHET_VERSION') ? 'cachet/'.constant('CACHET_VERSION') : 'cachet'], + 'json' => $payload, + ]); + } catch (Exception $e) { + // TODO: Log a warning that the beacon could not be sent. + } + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 20daea44..c361d0d0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -16,6 +16,7 @@ use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Hash; @@ -93,6 +94,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon }); } + /** + * Scope all admin users. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeAdmins(Builder $query) + { + return $query->where('level', self::LEVEL_ADMIN); + } + + /** + * Scope all active users. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive(Builder $query) + { + return $query->where('active', true); + } + /** * Hash any password being inserted by default. * diff --git a/app/Settings/Repository.php b/app/Settings/Repository.php index edd5103e..9b44173a 100644 --- a/app/Settings/Repository.php +++ b/app/Settings/Repository.php @@ -76,6 +76,23 @@ class Repository } } + /** + * Get a setting, or the default value. + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function get($name, $default = null) + { + if ($setting = $this->model->where('name', $name)->first()) { + return $setting->value; + } + + return $default; + } + /** * Deletes a setting. * diff --git a/config/cachet.php b/config/cachet.php index ee856e91..2f193d21 100644 --- a/config/cachet.php +++ b/config/cachet.php @@ -33,4 +33,17 @@ return [ 'is_docker' => env('DOCKER', false), + /* + |-------------------------------------------------------------------------- + | Beacon + |-------------------------------------------------------------------------- + | + | Has the installation agreed to sending us Beacon data? + | + | Default: true + | + */ + + 'beacon' => env('CACHET_BEACON', true), + ]; diff --git a/tests/Bus/Jobs/JobExistenceTest.php b/tests/Bus/Jobs/JobExistenceTest.php new file mode 100644 index 00000000..f271ef4c --- /dev/null +++ b/tests/Bus/Jobs/JobExistenceTest.php @@ -0,0 +1,30 @@ + + */ +class JobExistenceTest extends TestCase +{ + use ExistenceTrait; + + protected function getSourcePath() + { + return realpath(__DIR__.'/../../../app/Bus/Jobs'); + } +} diff --git a/tests/Bus/Jobs/System/SendBeaconJobTest.php b/tests/Bus/Jobs/System/SendBeaconJobTest.php new file mode 100644 index 00000000..ccea5df9 --- /dev/null +++ b/tests/Bus/Jobs/System/SendBeaconJobTest.php @@ -0,0 +1,48 @@ + + */ +class SendBeaconJobTest extends AbstractTestCase +{ + use JobTrait; + + /** + * @before + */ + public function setEventExpectations() + { + $this->onlyExpectsEvents([]); + } + + protected function getObjectAndParams() + { + $params = []; + $object = new SendBeaconJob(); + + return compact('params', 'object'); + } + + protected function getHandlerClass() + { + return SendBeaconJobHandler::class; + } +} diff --git a/tests/Foundation/Providers/IntegrationServiceProviderTest.php b/tests/Foundation/Providers/IntegrationServiceProviderTest.php index d9cee0ee..d3d5f95a 100644 --- a/tests/Foundation/Providers/IntegrationServiceProviderTest.php +++ b/tests/Foundation/Providers/IntegrationServiceProviderTest.php @@ -12,6 +12,7 @@ namespace CachetHQ\Tests\Cachet\Foundation\Providers; use AltThree\TestBench\ServiceProviderTrait; +use CachetHQ\Cachet\Integrations\Contracts\Beacon; use CachetHQ\Cachet\Integrations\Contracts\Credits; use CachetHQ\Cachet\Integrations\Contracts\Feed; use CachetHQ\Cachet\Integrations\Contracts\Releases; @@ -28,6 +29,11 @@ class IntegrationServiceProviderTest extends AbstractTestCase { use ServiceProviderTrait; + public function testBeaconIsInjectable() + { + $this->assertIsInjectable(Beacon::class); + } + public function testCreditsIsInjectable() { $this->assertIsInjectable(Credits::class);