From 62dbe2a3ff7ab71c7a1d8d4a06b6c55fac8c581b Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 16 Dec 2016 19:22:30 -0500 Subject: [PATCH 1/6] Begin work on proper MyBB Application class and add base Extension class --- app/Extension/Extension.php | 269 +++++++++ app/Kernel/AbstractServer.php | 268 +++++++++ app/Kernel/AbstractServiceProvider.php | 35 ++ app/Kernel/Application.php | 738 +++++++++++++++++++++++++ composer.json | 3 +- composer.lock | 4 +- 6 files changed, 1314 insertions(+), 3 deletions(-) create mode 100644 app/Extension/Extension.php create mode 100644 app/Kernel/AbstractServer.php create mode 100644 app/Kernel/AbstractServiceProvider.php create mode 100644 app/Kernel/Application.php diff --git a/app/Extension/Extension.php b/app/Extension/Extension.php new file mode 100644 index 00000000..646b173d --- /dev/null +++ b/app/Extension/Extension.php @@ -0,0 +1,269 @@ +path = $path; + $this->composerJson = $composerJson; + $this->assignId(); + } + + /** + * Assigns the id for the extension used globally. + */ + protected function assignId() + { + list($vendor, $package) = explode('/', $this->name); + $package = str_replace(['mybb-ext-', 'mybb-'], '', $package); + $this->id = "$vendor-$package"; + } + + /** + * {@inheritdoc} + */ + public function __get($name) + { + return $this->composerJsonAttribute(Str::snake($name, '-')); + } + + /** + * {@inheritdoc} + */ + public function __isset($name) + { + return isset($this->{$name}) || $this->composerJsonAttribute(Str::snake($name, '-')); + } + + /** + * Dot notation getter for composer.json attributes. + * + * @see https://laravel.com/docs/5.1/helpers#arrays + * + * @param $name + * @return mixed + */ + public function composerJsonAttribute($name) + { + return Arr::get($this->composerJson, $name); + } + + /** + * @param bool $installed + * @return Extension + */ + public function setInstalled($installed) + { + $this->installed = $installed; + + return $this; + } + + /** + * @return bool + */ + public function isInstalled() + { + return $this->installed; + } + + /** + * @param string $version + * @return Extension + */ + public function setVersion($version) + { + $this->version = $version; + + return $this; + } + + /** + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Loads the icon information from the composer.json. + * + * @return array|null + */ + public function getIcon() + { + if (($icon = $this->composerJsonAttribute('extra.mybb-extension.icon'))) { + if ($file = Arr::get($icon, 'image')) { + $file = $this->path.'/'.$file; + + if (file_exists($file)) { + $mimetype = pathinfo($file, PATHINFO_EXTENSION) === 'svg' + ? 'image/svg+xml' + : finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file); + $data = file_get_contents($file); + + $icon['backgroundImage'] = 'url(\'data:'.$mimetype.';base64,'.base64_encode($data).'\')'; + } + } + + return $icon; + } + } + + /** + * @param bool $enabled + * @return Extension + */ + public function setEnabled($enabled) + { + $this->enabled = $enabled; + + return $this; + } + + /** + * @return bool + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * The raw path of the directory under extensions. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Tests whether the extension has assets. + * + * @return bool + */ + public function hasAssets() + { + return realpath($this->path.'/assets/') !== false; + } + + /** + * Tests whether the extension has migrations. + * + * @return bool + */ + public function hasMigrations() + { + return realpath($this->path.'/migrations/') !== false; + } + + /** + * Generates an array result for the object. + * + * @return array + */ + public function toArray() + { + return (array) array_merge([ + 'id' => $this->getId(), + 'version' => $this->getVersion(), + 'path' => $this->path, + 'icon' => $this->getIcon(), + 'hasAssets' => $this->hasAssets(), + 'hasMigrations' => $this->hasMigrations(), + ], $this->composerJson); + } +} diff --git a/app/Kernel/AbstractServer.php b/app/Kernel/AbstractServer.php new file mode 100644 index 00000000..7ddf8664 --- /dev/null +++ b/app/Kernel/AbstractServer.php @@ -0,0 +1,268 @@ +basePath = $basePath; + $this->publicPath = $publicPath; + + if (file_exists($file = $this->basePath.'/config.php')) { + $this->config = include $file; + } + } + + /** + * @return string + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * @return string + */ + public function getPublicPath() + { + return $this->publicPath; + } + + /** + * @return string + */ + public function getStoragePath() + { + return $this->storagePath; + } + + /** + * @param $basePath + */ + public function setBasePath($basePath) + { + $this->basePath = $basePath; + } + + /** + * @param $publicPath + */ + public function setPublicPath($publicPath) + { + $this->publicPath = $publicPath; + } + + /** + * @param $storagePath + */ + public function setStoragePath($storagePath) + { + $this->storagePath = $storagePath; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param array $config + */ + public function setConfig(array $config) + { + $this->config = $config; + } + + /** + * @param callable $callback + */ + public function extend(callable $callback) + { + $this->extendCallbacks[] = $callback; + } + + /** + * @return Application + */ + public function getApp() + { + if ($this->app !== null) { + return $this->app; + } + + date_default_timezone_set('UTC'); + + $app = new Application($this->basePath, $this->publicPath); + + if ($this->storagePath) { + $app->useStoragePath($this->storagePath); + } + + $app->instance('env', 'production'); + $app->instance('mybb.config', $this->config); + $app->instance('config', $config = $this->getIlluminateConfig($app)); + + $this->registerLogger($app); + + $this->registerCache($app); + + //todo: Register Settings, Local, and Database providors here + $app->register('Illuminate\Bus\BusServiceProvider'); + $app->register('Illuminate\Filesystem\FilesystemServiceProvider'); + $app->register('Illuminate\Hashing\HashServiceProvider'); + $app->register('Illuminate\Mail\MailServiceProvider'); + $app->register('Illuminate\View\ViewServiceProvider'); + $app->register('Illuminate\Validation\ValidationServiceProvider'); + + if ($app->isInstalled() && $app->isUpToDate()) { + //todo: Implement new settings repository interface + //$settings = $app->make('MyBB\Settings\SettingsRepositoryInterface'); + + $config->set('mail.driver', $settings->get('mail_driver')); + $config->set('mail.host', $settings->get('mail_host')); + $config->set('mail.port', $settings->get('mail_port')); + $config->set('mail.from.address', $settings->get('mail_from')); + $config->set('mail.from.name', $settings->get('forum_title')); + $config->set('mail.encryption', $settings->get('mail_encryption')); + $config->set('mail.username', $settings->get('mail_username')); + $config->set('mail.password', $settings->get('mail_password')); + + //todo: Register Core, API, Forum, and Admin ServiceProviders here + + foreach ($this->extendCallbacks as $callback) { + $app->call($callback); + } + + //todo: Register ExtensionServiceProvider here + } + + $app->boot(); + + $this->app = $app; + + return $app; + } + + /** + * @param Application $app + * @return ConfigRepository + */ + protected function getIlluminateConfig(Application $app) + { + return new ConfigRepository([ + 'view' => [ + 'paths' => [], + 'compiled' => $app->storagePath().'/views', + ], + 'mail' => [ + 'driver' => 'mail', + ], + 'filesystems' => [ + 'default' => 'local', + 'cloud' => 's3', + 'disks' => [ + 'mybb-avatars' => [ + 'driver' => 'local', + 'root' => $app->publicPath().'/assets/avatars' + ] + ] + ] + ]); + } + + /** + * @param Application $app + */ + protected function registerLogger(Application $app) + { + $logger = new Logger($app->environment()); + $logPath = $app->storagePath().'/logs/mybb.log'; + + $handler = new StreamHandler($logPath, Logger::DEBUG); + $handler->setFormatter(new LineFormatter(null, null, true, true)); + + $logger->pushHandler($handler); + + $app->instance('log', $logger); + $app->alias('log', 'Psr\Log\LoggerInterface'); + } + + /** + * @param Application $app + */ + protected function registerCache(Application $app) + { + $app->singleton('cache.store', function ($app) { + return new \Illuminate\Cache\Repository($app->make('cache.filestore')); + }); + + $app->singleton('cache.filestore', function ($app) { + return new \Illuminate\Cache\FileStore( + new \Illuminate\Filesystem\Filesystem(), + $app->storagePath().'/cache' + ); + }); + + $app->alias('cache.filestore', 'Illuminate\Contracts\Cache\Store'); + $app->alias('cache.store', 'Illuminate\Contracts\Cache\Repository'); + } +} diff --git a/app/Kernel/AbstractServiceProvider.php b/app/Kernel/AbstractServiceProvider.php new file mode 100644 index 00000000..210a6982 --- /dev/null +++ b/app/Kernel/AbstractServiceProvider.php @@ -0,0 +1,35 @@ +registerBaseBindings(); + + $this->registerBaseServiceProviders(); + + $this->registerCoreContainerAliases(); + + if ($basePath) { + $this->setBasePath($basePath); + } + + if ($publicPath) { + $this->setPublicPath($publicPath); + } + } + + /** + * Determine if MyBB has been installed. + * + * @return bool + */ + public function isInstalled() + { + return $this->bound('mybb.config'); + } + + public function isUpToDate() + { + //todo: Rewrite settings repository + $settings = $this->make('MyBB\Settings\SettingsRepositoryInterface'); + + try { + $version = $settings->get('version'); + } finally { + $isUpToDate = isset($version) && $version === $this->version(); + } + + return $isUpToDate; + } + + /** + * @param string $key + * @param mixed $default + * @return mixed + */ + public function config($key, $default = null) + { + return array_get($this->make('mybb.config'), $key, $default); + } + + /** + * Check if MyBB is in debug mode. + * + * @return bool + */ + public function inDebugMode() + { + return ! $this->isInstalled() || $this->config('debug'); + } + + /** + * Get the URL to the MyBB installation. + * + * @param string $path + * @return string + */ + public function url($path = null) + { + $config = $this->isInstalled() ? $this->make('mybb.config') : []; + $url = array_get($config, 'url', array_get($_SERVER, 'REQUEST_URI')); + + if (is_array($url)) { + if (isset($url[$path])) { + return $url[$path]; + } + + $url = $url['base']; + } + + if ($path) { + $url .= '/'.array_get($config, "paths.$path", $path); + } + + return $url; + } + + /** + * Get the version number of the application. + * + * @return string + */ + public function version() + { + return static::VERSION; + } + + /** + * Register the basic bindings into the container. + */ + protected function registerBaseBindings() + { + static::setInstance($this); + + $this->instance('app', $this); + + $this->instance('Illuminate\Container\Container', $this); + } + + /** + * Register all of the base service providers. + */ + protected function registerBaseServiceProviders() + { + $this->register(new EventServiceProvider($this)); + } + + /** + * Set the base path for the application. + * + * @param string $basePath + * @return $this + */ + public function setBasePath($basePath) + { + $this->basePath = rtrim($basePath, '\/'); + + $this->bindPathsInContainer(); + + return $this; + } + + /** + * Set the public path for the application. + * + * @param string $publicPath + * @return $this + */ + public function setPublicPath($publicPath) + { + $this->publicPath = rtrim($publicPath, '\/'); + + $this->bindPathsInContainer(); + + return $this; + } + + /** + * Bind all of the application paths in the container. + * + * @return void + */ + protected function bindPathsInContainer() + { + foreach (['base', 'public', 'storage'] as $path) { + $this->instance('path.'.$path, $this->{$path.'Path'}()); + } + } + + /** + * Get the base path of the Laravel installation. + * + * @return string + */ + public function basePath() + { + return $this->basePath; + } + + /** + * Get the path to the public / web directory. + * + * @return string + */ + public function publicPath() + { + return $this->publicPath; + } + + /** + * Get the path to the storage directory. + * + * @return string + */ + public function storagePath() + { + return $this->storagePath ?: $this->basePath.DIRECTORY_SEPARATOR.'storage'; + } + + /** + * Set the storage directory. + * + * @param string $path + * @return $this + */ + public function useStoragePath($path) + { + $this->storagePath = $path; + + $this->instance('path.storage', $path); + + return $this; + } + + /** + * Get or check the current application environment. + * + * @param mixed + * @return string + */ + public function environment() + { + if (func_num_args() > 0) { + $patterns = is_array(func_get_arg(0)) ? func_get_arg(0) : func_get_args(); + + foreach ($patterns as $pattern) { + if (Str::is($pattern, $this['env'])) { + return true; + } + } + + return false; + } + + return $this['env']; + } + + /** + * Determine if we are running in the console. + * + * @return bool + */ + public function runningInConsole() + { + return php_sapi_name() == 'cli'; + } + + /** + * Determine if we are running unit tests. + * + * @return bool + */ + public function runningUnitTests() + { + return $this['env'] == 'testing'; + } + + /** + * Register all of the configured providers. + * + * @return void + */ + public function registerConfiguredProviders() + { + } + + /** + * Register a service provider with the application. + * + * @param ServiceProvider|string $provider + * @param array $options + * @param bool $force + * @return ServiceProvider + */ + public function register($provider, $options = [], $force = false) + { + if ($registered = $this->getProvider($provider) && ! $force) { + return $registered; + } + + // If the given "provider" is a string, we will resolve it, passing in the + // application instance automatically for the developer. This is simply + // a more convenient way of specifying your service provider classes. + if (is_string($provider)) { + $provider = $this->resolveProviderClass($provider); + } + + $provider->register(); + + // Once we have registered the service we will iterate through the options + // and set each of them on the application so they will be available on + // the actual loading of the service objects and for developer usage. + foreach ($options as $key => $value) { + $this[$key] = $value; + } + + $this->markAsRegistered($provider); + + // If the application has already booted, we will call this boot method on + // the provider class so it has an opportunity to do its boot logic and + // will be ready for any usage by the developer's application logics. + if ($this->booted) { + $this->bootProvider($provider); + } + + return $provider; + } + + /** + * Get the registered service provider instance if it exists. + * + * @param ServiceProvider|string $provider + * @return ServiceProvider|null + */ + public function getProvider($provider) + { + $name = is_string($provider) ? $provider : get_class($provider); + + return Arr::first($this->serviceProviders, function ($key, $value) use ($name) { + return $value instanceof $name; + }); + } + + /** + * Resolve a service provider instance from the class name. + * + * @param string $provider + * @return ServiceProvider + */ + public function resolveProviderClass($provider) + { + return new $provider($this); + } + + /** + * Mark the given provider as registered. + * + * @param ServiceProvider $provider + * @return void + */ + protected function markAsRegistered($provider) + { + $this['events']->fire($class = get_class($provider), [$provider]); + + $this->serviceProviders[] = $provider; + + $this->loadedProviders[$class] = true; + } + + /** + * Load and boot all of the remaining deferred providers. + */ + public function loadDeferredProviders() + { + // We will simply spin through each of the deferred providers and register each + // one and boot them if the application has booted. This should make each of + // the remaining services available to this application for immediate use. + foreach ($this->deferredServices as $service => $provider) { + $this->loadDeferredProvider($service); + } + + $this->deferredServices = []; + } + + /** + * Load the provider for a deferred service. + * + * @param string $service + */ + public function loadDeferredProvider($service) + { + if (! isset($this->deferredServices[$service])) { + return; + } + + $provider = $this->deferredServices[$service]; + + // If the service provider has not already been loaded and registered we can + // register it with the application and remove the service from this list + // of deferred services, since it will already be loaded on subsequent. + if (! isset($this->loadedProviders[$provider])) { + $this->registerDeferredProvider($provider, $service); + } + } + + /** + * Register a deferred provider and service. + * + * @param string $provider + * @param string $service + */ + public function registerDeferredProvider($provider, $service = null) + { + // Once the provider that provides the deferred service has been registered we + // will remove it from our local list of the deferred services with related + // providers so that this container does not try to resolve it out again. + if ($service) { + unset($this->deferredServices[$service]); + } + + $this->register($instance = new $provider($this)); + + if (! $this->booted) { + $this->booting(function () use ($instance) { + $this->bootProvider($instance); + }); + } + } + + /** + * Resolve the given type from the container. + * + * (Overriding Container::make) + * + * @param string $abstract + * @param array $parameters + * @return mixed + */ + public function make($abstract, array $parameters = []) + { + $abstract = $this->getAlias($abstract); + + if (isset($this->deferredServices[$abstract])) { + $this->loadDeferredProvider($abstract); + } + + return parent::make($abstract, $parameters); + } + + /** + * Determine if the given abstract type has been bound. + * + * (Overriding Container::bound) + * + * @param string $abstract + * @return bool + */ + public function bound($abstract) + { + return isset($this->deferredServices[$abstract]) || parent::bound($abstract); + } + + /** + * Determine if the application has booted. + * + * @return bool + */ + public function isBooted() + { + return $this->booted; + } + + /** + * Boot the application's service providers. + * + * @return void + */ + public function boot() + { + if ($this->booted) { + return; + } + + // Once the application has booted we will also fire some "booted" callbacks + // for any listeners that need to do work after this initial booting gets + // finished. This is useful when ordering the boot-up processes we run. + $this->fireAppCallbacks($this->bootingCallbacks); + + array_walk($this->serviceProviders, function ($p) { + $this->bootProvider($p); + }); + + $this->booted = true; + + $this->fireAppCallbacks($this->bootedCallbacks); + } + + /** + * Boot the given service provider. + * + * @param ServiceProvider $provider + * @return mixed + */ + protected function bootProvider(ServiceProvider $provider) + { + if (method_exists($provider, 'boot')) { + return $this->call([$provider, 'boot']); + } + } + + /** + * Register a new boot listener. + * + * @param mixed $callback + * @return void + */ + public function booting($callback) + { + $this->bootingCallbacks[] = $callback; + } + + /** + * Register a new "booted" listener. + * + * @param mixed $callback + * @return void + */ + public function booted($callback) + { + $this->bootedCallbacks[] = $callback; + + if ($this->isBooted()) { + $this->fireAppCallbacks([$callback]); + } + } + + /** + * Call the booting callbacks for the application. + * + * @param array $callbacks + * @return void + */ + protected function fireAppCallbacks(array $callbacks) + { + foreach ($callbacks as $callback) { + call_user_func($callback, $this); + } + } + + /** + * Get the path to the cached "compiled.php" file. + * + * @return string + */ + public function getCachedCompilePath() + { + return $this->basePath().'/bootstrap/cache/compiled.php'; + } + + /** + * Get the path to the cached services.json file. + * + * @return string + */ + public function getCachedServicesPath() + { + return $this->basePath().'/bootstrap/cache/services.json'; + } + + /** + * Determine if the application is currently down for maintenance. + * + * @return bool + */ + public function isDownForMaintenance() + { + return $this->config('offline'); + } + + /** + * Get the service providers that have been loaded. + * + * @return array + */ + public function getLoadedProviders() + { + return $this->loadedProviders; + } + + /** + * Get the application's deferred services. + * + * @return array + */ + public function getDeferredServices() + { + return $this->deferredServices; + } + + /** + * Set the application's deferred services. + * + * @param array $services + * @return void + */ + public function setDeferredServices(array $services) + { + $this->deferredServices = $services; + } + + /** + * Add an array of services to the application's deferred services. + * + * @param array $services + * @return void + */ + public function addDeferredServices(array $services) + { + $this->deferredServices = array_merge($this->deferredServices, $services); + } + + /** + * Determine if the given service is a deferred service. + * + * @param string $service + * @return bool + */ + public function isDeferredService($service) + { + return isset($this->deferredServices[$service]); + } + + /** + * Register the core class aliases in the container. + */ + public function registerCoreContainerAliases() + { + $aliases = [ + 'app' => ['MyBB\Core\Kernel\Application', 'Illuminate\Contracts\Container\Container', 'Illuminate\Contracts\Foundation\Application'], + 'blade.compiler' => 'Illuminate\View\Compilers\BladeCompiler', + 'cache' => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'], + 'cache.store' => ['Illuminate\Cache\Repository', 'Illuminate\Contracts\Cache\Repository'], + 'config' => ['Illuminate\Config\Repository', 'Illuminate\Contracts\Config\Repository'], + 'db' => 'Illuminate\Database\DatabaseManager', + 'events' => ['Illuminate\Events\Dispatcher', 'Illuminate\Contracts\Events\Dispatcher'], + 'files' => 'Illuminate\Filesystem\Filesystem', + 'filesystem' => ['Illuminate\Filesystem\FilesystemManager', 'Illuminate\Contracts\Filesystem\Factory'], + 'filesystem.disk' => 'Illuminate\Contracts\Filesystem\Filesystem', + 'filesystem.cloud' => 'Illuminate\Contracts\Filesystem\Cloud', + 'hash' => 'Illuminate\Contracts\Hashing\Hasher', + 'mailer' => ['Illuminate\Mail\Mailer', 'Illuminate\Contracts\Mail\Mailer', 'Illuminate\Contracts\Mail\MailQueue'], + 'validator' => ['Illuminate\Validation\Factory', 'Illuminate\Contracts\Validation\Factory'], + 'view' => ['Illuminate\View\Factory', 'Illuminate\Contracts\View\Factory'], + ]; + + foreach ($aliases as $key => $aliases) { + foreach ((array) $aliases as $alias) { + $this->alias($key, $alias); + } + } + } + + /** + * Flush the container of all bindings and resolved instances. + */ + public function flush() + { + parent::flush(); + + $this->loadedProviders = []; + } +} diff --git a/composer.json b/composer.json index 6b5b180a..d89cebf8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "mccool/laravel-auto-presenter": "^4.2", "league/fractal": "^0.13.0", "davejamesmiller/laravel-breadcrumbs": "^3.0", - "doctrine/dbal": "^2.5" + "doctrine/dbal": "^2.5", + "monolog/monolog": "^1.22" }, "require-dev": { "phpunit/phpunit": "~4.0", diff --git a/composer.lock b/composer.lock index e44b0b87..ffc2caa5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "cf12f09e682d04250c038ad11e9b2d0b", - "content-hash": "d89e922c1957917cf6da5549bdfd1fd8", + "hash": "863bb86a53037076c01008c9ab1f4525", + "content-hash": "8420d042af5cb5678906cdb2612d3633", "packages": [ { "name": "classpreloader/classpreloader", From c79f8991953e528746168ebee5e9254081055ebd Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 16 Dec 2016 20:13:50 -0500 Subject: [PATCH 2/6] Implement new database method, add the begining of the new routes providor, and new settings repository --- app/Database/AbstractModel.php | 204 +++++++++++ app/Database/DatabaseMigrationRepository.php | 163 +++++++++ app/Database/DatabaseServiceProvider.php | 69 ++++ app/Database/Migration.php | 112 ++++++ app/Database/MigrationCreator.php | 144 ++++++++ app/Database/MigrationRepositoryInterface.php | 60 ++++ app/Database/Migrator.php | 326 ++++++++++++++++++ app/Events/ConfigureModelDates.php | 48 +++ .../ConfigureModelDefaultAttributes.php | 44 +++ app/Events/GetModelRelationship.php | 49 +++ app/Http/AbstractServer.php | 69 ++++ app/Kernel/AbstractServer.php | 8 +- app/Kernel/Application.php | 3 +- app/Settings/DatabaseSettingsRepository.php | 50 +++ .../MemoryCacheSettingsRepository.php | 59 ++++ app/Settings/SettingsRepositoryInterface.php | 21 ++ app/Settings/SettingsServiceProvider.php | 31 ++ composer.json | 5 +- composer.lock | 253 +++++++++++++- 19 files changed, 1709 insertions(+), 9 deletions(-) create mode 100644 app/Database/AbstractModel.php create mode 100644 app/Database/DatabaseMigrationRepository.php create mode 100644 app/Database/DatabaseServiceProvider.php create mode 100644 app/Database/Migration.php create mode 100644 app/Database/MigrationCreator.php create mode 100644 app/Database/MigrationRepositoryInterface.php create mode 100644 app/Database/Migrator.php create mode 100644 app/Events/ConfigureModelDates.php create mode 100644 app/Events/ConfigureModelDefaultAttributes.php create mode 100644 app/Events/GetModelRelationship.php create mode 100644 app/Http/AbstractServer.php create mode 100644 app/Settings/DatabaseSettingsRepository.php create mode 100644 app/Settings/MemoryCacheSettingsRepository.php create mode 100644 app/Settings/SettingsRepositoryInterface.php create mode 100644 app/Settings/SettingsServiceProvider.php diff --git a/app/Database/AbstractModel.php b/app/Database/AbstractModel.php new file mode 100644 index 00000000..79a83183 --- /dev/null +++ b/app/Database/AbstractModel.php @@ -0,0 +1,204 @@ +releaseAfterSaveCallbacks() as $callback) { + $callback($model); + } + }); + + static::deleted(function (AbstractModel $model) { + foreach ($model->releaseAfterDeleteCallbacks() as $callback) { + $callback($model); + } + }); + } + + /** + * {@inheritdoc} + */ + public function __construct(array $attributes = []) + { + $defaults = []; + + static::$dispatcher->fire( + new ConfigureModelDefaultAttributes($this, $defaults) + ); + + $this->attributes = $defaults; + + parent::__construct($attributes); + } + + /** + * Get the attributes that should be converted to dates. + * + * @return array + */ + public function getDates() + { + static $dates = []; + + $class = get_class($this); + + if (! isset($dates[$class])) { + static::$dispatcher->fire( + new ConfigureModelDates($this, $this->dates) + ); + + $dates[$class] = $this->dates; + } + + return $dates[$class]; + } + + /** + * Get an attribute from the model. If nothing is found, attempt to load + * a custom relation method with this key. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) + { + if (! is_null($value = parent::getAttribute($key))) { + return $value; + } + + // If a custom relation with this key has been set up, then we will load + // and return results from the query and hydrate the relationship's + // value on the "relationships" array. + if (! $this->relationLoaded($key) && ($relation = $this->getCustomRelation($key))) { + if (! $relation instanceof Relation) { + throw new LogicException( + 'Relationship method must return an object of type '.Relation::class + ); + } + + return $this->relations[$key] = $relation->getResults(); + } + } + + /** + * Get a custom relation object. + * + * @param string $name + * @return mixed + */ + protected function getCustomRelation($name) + { + return static::$dispatcher->until( + new GetModelRelationship($this, $name) + ); + } + + /** + * Register a callback to be run once after the model is saved. + * + * @param callable $callback + * @return void + */ + public function afterSave($callback) + { + $this->afterSaveCallbacks[] = $callback; + } + + /** + * Register a callback to be run once after the model is deleted. + * + * @param callable $callback + * @return void + */ + public function afterDelete($callback) + { + $this->afterDeleteCallbacks[] = $callback; + } + + /** + * @return callable[] + */ + public function releaseAfterSaveCallbacks() + { + $callbacks = $this->afterSaveCallbacks; + + $this->afterSaveCallbacks = []; + + return $callbacks; + } + + /** + * @return callable[] + */ + public function releaseAfterDeleteCallbacks() + { + $callbacks = $this->afterDeleteCallbacks; + + $this->afterDeleteCallbacks = []; + + return $callbacks; + } + + /** + * {@inheritdoc} + */ + public function __call($method, $arguments) + { + if ($relation = $this->getCustomRelation($method)) { + return $relation; + } + + return parent::__call($method, $arguments); + } +} diff --git a/app/Database/DatabaseMigrationRepository.php b/app/Database/DatabaseMigrationRepository.php new file mode 100644 index 00000000..03f2a55e --- /dev/null +++ b/app/Database/DatabaseMigrationRepository.php @@ -0,0 +1,163 @@ +table = $table; + $this->resolver = $resolver; + } + + /** + * Get the ran migrations. + * + * @return array + */ + public function getRan($extension = null) + { + return $this->table() + ->where('extension', $extension) + ->orderBy('migration', 'asc') + ->lists('migration'); + } + + /** + * Log that a migration was run. + * + * @param string $file + * @param string $extension + * @return void + */ + public function log($file, $extension = null) + { + $record = ['migration' => $file, 'extension' => $extension]; + + $this->table()->insert($record); + } + + /** + * Remove a migration from the log. + * + * @param string $file + * @param string $extension + * @return void + */ + public function delete($file, $extension = null) + { + $query = $this->table()->where('migration', $file); + + if (is_null($extension)) { + $query->whereNull('extension'); + } else { + $query->where('extension', $extension); + } + + $query->delete(); + } + + /** + * Create the migration repository data store. + * + * @return void + */ + public function createRepository() + { + $schema = $this->getConnection()->getSchemaBuilder(); + + $schema->create($this->table, function ($table) { + $table->string('migration'); + $table->string('extension')->nullable(); + }); + } + + /** + * Determine if the migration repository exists. + * + * @return bool + */ + public function repositoryExists() + { + $schema = $this->getConnection()->getSchemaBuilder(); + + return $schema->hasTable($this->table); + } + + /** + * Get a query builder for the migration table. + * + * @return \Illuminate\Database\Query\Builder + */ + protected function table() + { + return $this->getConnection()->table($this->table); + } + + /** + * Get the connection resolver instance. + * + * @return \Illuminate\Database\ConnectionResolverInterface + */ + public function getConnectionResolver() + { + return $this->resolver; + } + + /** + * Resolve the database connection instance. + * + * @return \Illuminate\Database\Connection + */ + public function getConnection() + { + return $this->resolver->connection($this->connection); + } + + /** + * Set the information source to gather data. + * + * @param string $name + * @return void + */ + public function setSource($name) + { + $this->connection = $name; + } +} diff --git a/app/Database/DatabaseServiceProvider.php b/app/Database/DatabaseServiceProvider.php new file mode 100644 index 00000000..06e710fb --- /dev/null +++ b/app/Database/DatabaseServiceProvider.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace MyBB\Core\Database; + +use Flarum\Foundation\AbstractServiceProvider; +use Flarum\Foundation\Application; +use Illuminate\Database\ConnectionResolver; +use Illuminate\Database\Connectors\ConnectionFactory; +use PDO; + +class DatabaseServiceProvider extends AbstractServiceProvider +{ + /** + * {@inheritdoc} + */ + public function register() + { + $this->app->singleton('flarum.db', function () { + $factory = new ConnectionFactory($this->app); + + $connection = $factory->make($this->app->config('database')); + $connection->setEventDispatcher($this->app->make('Illuminate\Contracts\Events\Dispatcher')); + $connection->setFetchMode(PDO::FETCH_CLASS); + + return $connection; + }); + + $this->app->alias('flarum.db', 'Illuminate\Database\ConnectionInterface'); + + $this->app->singleton('Illuminate\Database\ConnectionResolverInterface', function () { + $resolver = new ConnectionResolver([ + 'flarum' => $this->app->make('flarum.db'), + ]); + $resolver->setDefaultConnection('flarum'); + + return $resolver; + }); + + $this->app->alias('Illuminate\Database\ConnectionResolverInterface', 'db'); + + $this->app->singleton('Flarum\Database\MigrationRepositoryInterface', function ($app) { + return new DatabaseMigrationRepository($app['db'], 'migrations'); + }); + + $this->app->bind(MigrationCreator::class, function (Application $app) { + return new MigrationCreator($app->make('Illuminate\Filesystem\Filesystem'), $app->basePath()); + }); + } + + /** + * {@inheritdoc} + */ + public function boot() + { + if ($this->app->isInstalled()) { + AbstractModel::setConnectionResolver($this->app->make('Illuminate\Database\ConnectionResolverInterface')); + AbstractModel::setEventDispatcher($this->app->make('events')); + } + } +} diff --git a/app/Database/Migration.php b/app/Database/Migration.php new file mode 100644 index 00000000..3c7c126e --- /dev/null +++ b/app/Database/Migration.php @@ -0,0 +1,112 @@ + function (Builder $schema) use ($name, $definition) { + $schema->create($name, $definition); + }, + 'down' => function (Builder $schema) use ($name) { + $schema->drop($name); + } + ]; + } + + /** + * Rename a table. + */ + public static function renameTable($from, $to) + { + return [ + 'up' => function (Builder $schema) use ($from, $to) { + $schema->rename($from, $to); + }, + 'down' => function (Builder $schema) use ($from, $to) { + $schema->rename($to, $from); + } + ]; + } + + /** + * Add columns to a table. + */ + public static function addColumns($tableName, array $columnDefinitions) + { + return [ + 'up' => function (Builder $schema) use ($tableName, $columnDefinitions) { + $schema->table($tableName, function (Blueprint $table) use ($columnDefinitions) { + foreach ($columnDefinitions as $columnName => $options) { + $type = array_shift($options); + $table->addColumn($type, $columnName, $options); + } + }); + }, + 'down' => function (Builder $schema) use ($tableName, $columnDefinitions) { + $schema->table($tableName, function (Blueprint $table) use ($columnDefinitions) { + $table->dropColumn(array_keys($columnDefinitions)); + }); + } + ]; + } + + /** + * Rename a column. + */ + public static function renameColumn($tableName, $from, $to) + { + return [ + 'up' => function (Builder $schema) use ($tableName, $from, $to) { + $schema->table($tableName, function (Blueprint $table) use ($from, $to) { + $table->renameColumn($from, $to); + }); + }, + 'down' => function (Builder $schema) use ($tableName, $from, $to) { + $schema->table($tableName, function (Blueprint $table) use ($from, $to) { + $table->renameColumn($to, $from); + }); + } + ]; + } + + /** + * Add default values for config values. + */ + public static function addSettings($defaults) + { + return [ + 'up' => function (SettingsRepositoryInterface $settings) use ($defaults) { + foreach ($defaults as $key => $value) { + $settings->set($key, $value); + } + }, + 'down' => function (SettingsRepositoryInterface $settings) use ($defaults) { + foreach (array_keys($defaults) as $key) { + $settings->delete($key); + } + } + ]; + } +} diff --git a/app/Database/MigrationCreator.php b/app/Database/MigrationCreator.php new file mode 100644 index 00000000..24f4ff29 --- /dev/null +++ b/app/Database/MigrationCreator.php @@ -0,0 +1,144 @@ +files = $files; + $this->publicPath = $publicPath; + } + + /** + * Create a new migration for the given extension. + * + * @param string $name + * @param Extension $extension + * @param string $table + * @param bool $create + * @return string + */ + public function create($name, $extension = null, $table = null, $create = false) + { + $migrationPath = $this->getMigrationPath($extension); + + $path = $this->getPath($name, $migrationPath); + + $stub = $this->getStub($table, $create); + + $this->files->put($path, $this->populateStub($stub, $table)); + + return $path; + } + + /** + * Get the migration stub file. + * + * @param string $table + * @param bool $create + * @return string + */ + protected function getStub($table, $create) + { + if (is_null($table)) { + return $this->files->get($this->getStubPath().'/blank.stub'); + } + + // We also have stubs for creating new tables and modifying existing tables + // to save the developer some typing when they are creating a new tables + // or modifying existing tables. We'll grab the appropriate stub here. + $stub = $create ? 'create.stub' : 'update.stub'; + + return $this->files->get($this->getStubPath()."/{$stub}"); + } + + /** + * Populate the place-holders in the migration stub. + * + * @param string $stub + * @param string $table + * @return string + */ + protected function populateStub($stub, $table) + { + $replacements = [ + '{{table}}' => $table + ]; + + return str_replace(array_keys($replacements), array_values($replacements), $stub); + } + + /** + * Get the full path name to the migration directory. + * + * @param string $extension + * @return string + */ + protected function getMigrationPath($extension) + { + $parent = $extension ? public_path().'/extensions/'.$extension : __DIR__.'/../..'; + + return $parent.'/migrations'; + } + + /** + * Get the full path name to the migration. + * + * @param string $name + * @param string $path + * @return string + */ + protected function getPath($name, $path) + { + return $path.'/'.$this->getDatePrefix().'_'.$name.'.php'; + } + + /** + * Get the date prefix for the migration. + * + * @return string + */ + protected function getDatePrefix() + { + return date('Y_m_d_His'); + } + + /** + * Get the path to the stubs. + * + * @return string + */ + protected function getStubPath() + { + return __DIR__.'/../../stubs/migrations'; + } +} diff --git a/app/Database/MigrationRepositoryInterface.php b/app/Database/MigrationRepositoryInterface.php new file mode 100644 index 00000000..ade0c653 --- /dev/null +++ b/app/Database/MigrationRepositoryInterface.php @@ -0,0 +1,60 @@ +files = $files; + $this->resolver = $resolver; + $this->repository = $repository; + } + + /** + * Run the outstanding migrations at a given path. + * + * @param string $path + * @param Extension $extension + * @return void + */ + public function run($path, Extension $extension = null) + { + $this->notes = []; + + $files = $this->getMigrationFiles($path); + + $ran = $this->repository->getRan($extension ? $extension->getId() : null); + + $migrations = array_diff($files, $ran); + + $this->runMigrationList($path, $migrations, $extension); + } + + /** + * Run an array of migrations. + * + * @param string $path + * @param array $migrations + * @param Extension $extension + * @return void + */ + public function runMigrationList($path, $migrations, Extension $extension = null) + { + // First we will just make sure that there are any migrations to run. If there + // aren't, we will just make a note of it to the developer so they're aware + // that all of the migrations have been run against this database system. + if (count($migrations) == 0) { + $this->note('Nothing to migrate.'); + + return; + } + + // Once we have the array of migrations, we will spin through them and run the + // migrations "up" so the changes are made to the databases. We'll then log + // that the migration was run so we don't repeat it next time we execute. + foreach ($migrations as $file) { + $this->runUp($path, $file, $extension); + } + } + + /** + * Run "up" a migration instance. + * + * @param string $path + * @param string $file + * @param string $path + * @param Extension $extension + * @return void + */ + protected function runUp($path, $file, Extension $extension = null) + { + $migration = $this->resolve($path, $file); + + $this->runClosureMigration($migration); + + // Once we have run a migrations class, we will log that it was run in this + // repository so that we don't try to run it next time we do a migration + // in the application. A migration repository keeps the migrate order. + $this->repository->log($file, $extension ? $extension->getId() : null); + + $this->note("Migrated: $file"); + } + + /** + * Rolls all of the currently applied migrations back. + * + * @param string $path + * @param Extension $extension + * @return int + */ + public function reset($path, Extension $extension = null) + { + $this->notes = []; + + $migrations = array_reverse($this->repository->getRan($extension->getId())); + + $count = count($migrations); + + if ($count === 0) { + $this->note('Nothing to rollback.'); + } else { + foreach ($migrations as $migration) { + $this->runDown($path, $migration, $extension); + } + } + + return $count; + } + + /** + * Run "down" a migration instance. + * + * @param $path + * @param string $file + * @param string $path + * @param Extension $extension + * @return void + */ + protected function runDown($path, $file, Extension $extension = null) + { + $migration = $this->resolve($path, $file); + + $this->runClosureMigration($migration, 'down'); + + // Once we have successfully run the migration "down" we will remove it from + // the migration repository so it will be considered to have not been run + // by the application then will be able to fire by any later operation. + $this->repository->delete($file, $extension ? $extension->getId() : null); + + $this->note("Rolled back: $file"); + } + + /** + * Runs a closure migration based on the migrate direction. + * + * @param $migration + * @param string $direction + * @throws Exception + */ + protected function runClosureMigration($migration, $direction = 'up') + { + if (is_array($migration) && array_key_exists($direction, $migration)) { + app()->call($migration[$direction]); + } else { + throw new Exception('Migration file should contain an array with up/down.'); + } + } + + /** + * Get all of the migration files in a given path. + * + * @param string $path + * @return array + */ + public function getMigrationFiles($path) + { + $files = $this->files->glob($path.'/*_*.php'); + + if ($files === false) { + return []; + } + + $files = array_map(function ($file) { + return str_replace('.php', '', basename($file)); + }, $files); + + // Once we have all of the formatted file names we will sort them and since + // they all start with a timestamp this should give us the migrations in + // the order they were actually created by the application developers. + sort($files); + + return $files; + } + + /** + * Resolve a migration instance from a file. + * + * @param string $path + * @param string $file + * @return array + */ + public function resolve($path, $file) + { + $migration = "$path/$file.php"; + + if ($this->files->exists($migration)) { + return $this->files->getRequire($migration); + } + } + + /** + * Raise a note event for the migrator. + * + * @param string $message + * @return void + */ + protected function note($message) + { + $this->notes[] = $message; + } + + /** + * Get the notes for the last operation. + * + * @return array + */ + public function getNotes() + { + return $this->notes; + } + + /** + * Resolve the database connection instance. + * + * @param string $connection + * @return \Illuminate\Database\Connection + */ + public function resolveConnection($connection) + { + return $this->resolver->connection($connection); + } + + /** + * Set the default connection name. + * + * @param string $name + * @return void + */ + public function setConnection($name) + { + if (! is_null($name)) { + $this->resolver->setDefaultConnection($name); + } + + $this->repository->setSource($name); + + $this->connection = $name; + } + + /** + * Get the migration repository instance. + * + * @return \Illuminate\Database\Migrations\MigrationRepositoryInterface + */ + public function getRepository() + { + return $this->repository; + } + + /** + * Determine if the migration repository exists. + * + * @return bool + */ + public function repositoryExists() + { + return $this->repository->repositoryExists(); + } + + /** + * Get the file system instance. + * + * @return \Illuminate\Filesystem\Filesystem + */ + public function getFilesystem() + { + return $this->files; + } +} diff --git a/app/Events/ConfigureModelDates.php b/app/Events/ConfigureModelDates.php new file mode 100644 index 00000000..eb842667 --- /dev/null +++ b/app/Events/ConfigureModelDates.php @@ -0,0 +1,48 @@ +model = $model; + $this->dates = &$dates; + } + + /** + * @param string $model + * @return bool + */ + public function isModel($model) + { + return $this->model instanceof $model; + } +} diff --git a/app/Events/ConfigureModelDefaultAttributes.php b/app/Events/ConfigureModelDefaultAttributes.php new file mode 100644 index 00000000..8eddec2a --- /dev/null +++ b/app/Events/ConfigureModelDefaultAttributes.php @@ -0,0 +1,44 @@ +model = $model; + $this->attributes = &$attributes; + } + + /** + * @param string $model + * @return bool + */ + public function isModel($model) + { + return $this->model instanceof $model; + } +} diff --git a/app/Events/GetModelRelationship.php b/app/Events/GetModelRelationship.php new file mode 100644 index 00000000..0302d0d9 --- /dev/null +++ b/app/Events/GetModelRelationship.php @@ -0,0 +1,49 @@ +model = $model; + $this->relationship = $relationship; + } + + /** + * @param string $model + * @param string $relationship + * @return bool + */ + public function isRelationship($model, $relationship) + { + return $this->model instanceof $model && $this->relationship === $relationship; + } +} diff --git a/app/Http/AbstractServer.php b/app/Http/AbstractServer.php new file mode 100644 index 00000000..97696c21 --- /dev/null +++ b/app/Http/AbstractServer.php @@ -0,0 +1,69 @@ +listen(); + } + + /** + * Use as PSR-7 middleware. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callable|null $out + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null) + { + $app = $this->getApp(); + + $this->collectGarbage($app); + + $middleware = $this->getMiddleware($app); + + return $middleware($request, $response, $out); + } + + /** + * @param Application $app + * @return MiddlewareInterface + */ + abstract protected function getMiddleware(Application $app); + + private function collectGarbage() + { + if ($this->hitsLottery()) { + //todo: Implement AccessToken, EmailToken, and PasswordToken models + + $earliestToKeep = date('Y-m-d H:i:s', time() - 24 * 60 * 60); + } + } + + private function hitsLottery() + { + return mt_rand(1, 100) <= 2; + } +} \ No newline at end of file diff --git a/app/Kernel/AbstractServer.php b/app/Kernel/AbstractServer.php index 7ddf8664..1b59cc8d 100644 --- a/app/Kernel/AbstractServer.php +++ b/app/Kernel/AbstractServer.php @@ -165,7 +165,8 @@ public function getApp() $this->registerCache($app); - //todo: Register Settings, Local, and Database providors here + //todo: Register Local, Twig, Database, and other needed providors here + $app->register('MyBB\Core\Settings\SettingsServiceProvider'); $app->register('Illuminate\Bus\BusServiceProvider'); $app->register('Illuminate\Filesystem\FilesystemServiceProvider'); $app->register('Illuminate\Hashing\HashServiceProvider'); @@ -174,8 +175,7 @@ public function getApp() $app->register('Illuminate\Validation\ValidationServiceProvider'); if ($app->isInstalled() && $app->isUpToDate()) { - //todo: Implement new settings repository interface - //$settings = $app->make('MyBB\Settings\SettingsRepositoryInterface'); + $settings = $app->make('MyBB\Core\Settings\SettingsRepositoryInterface'); $config->set('mail.driver', $settings->get('mail_driver')); $config->set('mail.host', $settings->get('mail_host')); @@ -186,7 +186,7 @@ public function getApp() $config->set('mail.username', $settings->get('mail_username')); $config->set('mail.password', $settings->get('mail_password')); - //todo: Register Core, API, Forum, and Admin ServiceProviders here + //todo: Register API, Forum, and Admin ServiceProviders here foreach ($this->extendCallbacks as $callback) { $app->call($callback); diff --git a/app/Kernel/Application.php b/app/Kernel/Application.php index 2c06e28f..77039fdc 100644 --- a/app/Kernel/Application.php +++ b/app/Kernel/Application.php @@ -123,8 +123,7 @@ public function isInstalled() public function isUpToDate() { - //todo: Rewrite settings repository - $settings = $this->make('MyBB\Settings\SettingsRepositoryInterface'); + $settings = $this->make('MyBB\Core\Settings\SettingsRepositoryInterface'); try { $version = $settings->get('version'); diff --git a/app/Settings/DatabaseSettingsRepository.php b/app/Settings/DatabaseSettingsRepository.php new file mode 100644 index 00000000..5c1e4071 --- /dev/null +++ b/app/Settings/DatabaseSettingsRepository.php @@ -0,0 +1,50 @@ +database = $connection; + } + + public function all() + { + return $this->database->table('settings')->lists('value', 'key'); + } + + public function get($key, $default = null) + { + if (is_null($value = $this->database->table('settings')->where('key', $key)->value('value'))) { + return $default; + } + + return $value; + } + + public function set($key, $value) + { + $query = $this->database->table('settings')->where('key', $key); + + $method = $query->exists() ? 'update' : 'insert'; + + $query->$method(compact('key', 'value')); + } + + public function delete($keyLike) + { + $this->database->table('settings')->where('key', 'like', $keyLike)->delete(); + } +} diff --git a/app/Settings/MemoryCacheSettingsRepository.php b/app/Settings/MemoryCacheSettingsRepository.php new file mode 100644 index 00000000..9d7c3b3b --- /dev/null +++ b/app/Settings/MemoryCacheSettingsRepository.php @@ -0,0 +1,59 @@ +inner = $inner; + } + + public function all() + { + if (! $this->isCached) { + $this->cache = $this->inner->all(); + $this->isCached = true; + } + + return $this->cache; + } + + public function get($key, $default = null) + { + if (array_key_exists($key, $this->cache)) { + return $this->cache[$key]; + } elseif (! $this->isCached) { + return array_get($this->all(), $key, $default); + } + + return $default; + } + + public function set($key, $value) + { + $this->cache[$key] = $value; + + $this->inner->set($key, $value); + } + + public function delete($key) + { + unset($this->cache[$key]); + + $this->inner->delete($key); + } +} diff --git a/app/Settings/SettingsRepositoryInterface.php b/app/Settings/SettingsRepositoryInterface.php new file mode 100644 index 00000000..ac866211 --- /dev/null +++ b/app/Settings/SettingsRepositoryInterface.php @@ -0,0 +1,21 @@ +app->singleton('MyBB\Settings\SettingsRepositoryInterface', function () { + return new MemoryCacheSettingsRepository( + new DatabaseSettingsRepository( + $this->app->make('Illuminate\Database\ConnectionInterface') + ) + ); + }); + + $this->app->alias('MyBB\Settings\SettingsRepositoryInterface', 'mybb.settings'); + } +} diff --git a/composer.json b/composer.json index d89cebf8..7ec46f47 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,10 @@ "league/fractal": "^0.13.0", "davejamesmiller/laravel-breadcrumbs": "^3.0", "doctrine/dbal": "^2.5", - "monolog/monolog": "^1.22" + "monolog/monolog": "^1.22", + "psr/http-message": "^1.0", + "zendframework/zend-diactoros": "^1.3", + "zendframework/zend-stratigility": "^1.3" }, "require-dev": { "phpunit/phpunit": "~4.0", diff --git a/composer.lock b/composer.lock index ffc2caa5..e2cc6370 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "863bb86a53037076c01008c9ab1f4525", - "content-hash": "8420d042af5cb5678906cdb2612d3633", + "hash": "0169595fa3fe89000a7d9df59e4152e3", + "content-hash": "6b10be36c9a170d475019f725df37afa", "packages": [ { "name": "classpreloader/classpreloader", @@ -664,6 +664,58 @@ ], "time": "2016-02-17 12:41:57" }, + { + "name": "http-interop/http-middleware", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-middleware.git", + "reference": "ff545c87e97bf4d88f0cb7eb3e89f99aaa53d7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-middleware/zipball/ff545c87e97bf4d88f0cb7eb3e89f99aaa53d7a9", + "reference": "ff545c87e97bf4d88f0cb7eb3e89f99aaa53d7a9", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Interop\\Http\\Middleware\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP middleware", + "keywords": [ + "factory", + "http", + "middleware", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2016-09-25 13:30:27" + }, { "name": "jakub-onderka/php-console-color", "version": "0.1", @@ -1729,6 +1781,56 @@ ], "time": "2016-03-18 20:34:03" }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06 14:39:51" + }, { "name": "psr/log", "version": "1.0.2", @@ -2915,6 +3017,153 @@ "environment" ], "time": "2016-09-01 10:05:43" + }, + { + "name": "zendframework/zend-diactoros", + "version": "1.3.7", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "969ff423d3f201da3ff718a5831bb999bb0669b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/969ff423d3f201da3ff718a5831bb999bb0669b0", + "reference": "969ff423d3f201da3ff718a5831bb999bb0669b0", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "~1.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6 || ^5.5", + "squizlabs/php_codesniffer": "^2.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev", + "dev-develop": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://github.com/zendframework/zend-diactoros", + "keywords": [ + "http", + "psr", + "psr-7" + ], + "time": "2016-10-11 13:25:21" + }, + { + "name": "zendframework/zend-escaper", + "version": "2.5.2", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-escaper.git", + "reference": "2dcd14b61a72d8b8e27d579c6344e12c26141d4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/2dcd14b61a72d8b8e27d579c6344e12c26141d4e", + "reference": "2dcd14b61a72d8b8e27d579c6344e12c26141d4e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev", + "dev-develop": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-escaper", + "keywords": [ + "escaper", + "zf2" + ], + "time": "2016-06-30 19:48:38" + }, + { + "name": "zendframework/zend-stratigility", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-stratigility.git", + "reference": "c410d367bb85f0a3cca44f112957d0ee28895d19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-stratigility/zipball/c410d367bb85f0a3cca44f112957d0ee28895d19", + "reference": "c410d367bb85f0a3cca44f112957d0ee28895d19", + "shasum": "" + }, + "require": { + "http-interop/http-middleware": "^0.2.0", + "php": "^5.6 || ^7.0", + "psr/http-message": "^1.0", + "zendframework/zend-escaper": "^2.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.7 || ^5.5", + "squizlabs/php_codesniffer": "^2.6.2", + "zendframework/zend-diactoros": "^1.0" + }, + "suggest": { + "psr/http-message-implementation": "Please install a psr/http-message-implementation to consume Stratigility; e.g., zendframework/zend-diactoros" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.0-dev", + "dev-develop": "2.0.0-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Stratigility\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Middleware for PHP", + "homepage": "https://github.com/zendframework/zend-stratigility", + "keywords": [ + "http", + "middleware", + "psr-7" + ], + "time": "2016-11-11 00:16:05" } ], "packages-dev": [ From e93fc4574e38035d0e7a1dac8fb5283a02527d66 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 16 Dec 2016 20:18:27 -0500 Subject: [PATCH 3/6] updates --- app/Database/DatabaseServiceProvider.php | 26 +++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/Database/DatabaseServiceProvider.php b/app/Database/DatabaseServiceProvider.php index 06e710fb..f6f3a13c 100644 --- a/app/Database/DatabaseServiceProvider.php +++ b/app/Database/DatabaseServiceProvider.php @@ -1,18 +1,16 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. +/** + * @author MyBB Group + * @version 2.0.0 + * @package mybb/core + * @license http://www.mybb.com/licenses/bsd3 BSD-3 */ namespace MyBB\Core\Database; -use Flarum\Foundation\AbstractServiceProvider; -use Flarum\Foundation\Application; +use MyBB\Core\Kernel\AbstractServiceProvider; +use MyBB\Core\Kernel\Application; use Illuminate\Database\ConnectionResolver; use Illuminate\Database\Connectors\ConnectionFactory; use PDO; @@ -24,7 +22,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider */ public function register() { - $this->app->singleton('flarum.db', function () { + $this->app->singleton('mybb.db', function () { $factory = new ConnectionFactory($this->app); $connection = $factory->make($this->app->config('database')); @@ -34,20 +32,20 @@ public function register() return $connection; }); - $this->app->alias('flarum.db', 'Illuminate\Database\ConnectionInterface'); + $this->app->alias('mybb.db', 'Illuminate\Database\ConnectionInterface'); $this->app->singleton('Illuminate\Database\ConnectionResolverInterface', function () { $resolver = new ConnectionResolver([ - 'flarum' => $this->app->make('flarum.db'), + 'mybb' => $this->app->make('mybb.db'), ]); - $resolver->setDefaultConnection('flarum'); + $resolver->setDefaultConnection('mybb'); return $resolver; }); $this->app->alias('Illuminate\Database\ConnectionResolverInterface', 'db'); - $this->app->singleton('Flarum\Database\MigrationRepositoryInterface', function ($app) { + $this->app->singleton('MyBB\Core\Database\MigrationRepositoryInterface', function ($app) { return new DatabaseMigrationRepository($app['db'], 'migrations'); }); From 665c9502673a883c9624593452d00e3004249be3 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 16 Dec 2016 20:49:03 -0500 Subject: [PATCH 4/6] Add access token, and auth token models, and add new tables for config, and new models --- app/Database/Models/AccessToken.php | 70 +++++++++++++ app/Database/Models/AuthToken.php | 97 +++++++++++++++++++ app/Database/Models/User.php | 10 ++ app/Exceptions/FloodingException.php | 17 ++++ .../InvalidConfirmationTokenException.php | 17 ++++ app/Exceptions/PermissionDeniedException.php | 21 ++++ app/Exceptions/ValidationException.php | 39 ++++++++ app/Http/AbstractServer.php | 4 +- 8 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 app/Database/Models/AccessToken.php create mode 100644 app/Database/Models/AuthToken.php create mode 100644 app/Exceptions/FloodingException.php create mode 100644 app/Exceptions/InvalidConfirmationTokenException.php create mode 100644 app/Exceptions/PermissionDeniedException.php create mode 100644 app/Exceptions/ValidationException.php diff --git a/app/Database/Models/AccessToken.php b/app/Database/Models/AccessToken.php new file mode 100644 index 00000000..a135b659 --- /dev/null +++ b/app/Database/Models/AccessToken.php @@ -0,0 +1,70 @@ +id = str_random(40); + $token->user_id = $userId; + $token->last_activity = time(); + $token->lifetime = $lifetime; + + return $token; + } + + public function touch() + { + $this->last_activity = time(); + + return $this->save(); + } + + /** + * Define the relationship with the owner of this access token. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo('MyBB\Core\Database\Models\User'); + } +} diff --git a/app/Database/Models/AuthToken.php b/app/Database/Models/AuthToken.php new file mode 100644 index 00000000..44b56458 --- /dev/null +++ b/app/Database/Models/AuthToken.php @@ -0,0 +1,97 @@ +id = str_random(40); + $token->payload = $payload; + $token->created_at = time(); + + return $token; + } + + /** + * Unserialize the payload attribute from the database's JSON value. + * + * @param string $value + * @return string + */ + public function getPayloadAttribute($value) + { + return json_decode($value, true); + } + + /** + * Serialize the payload attribute to be stored in the database as JSON. + * + * @param string $value + */ + public function setPayloadAttribute($value) + { + $this->attributes['payload'] = json_encode($value); + } + + /** + * Find the token with the given ID, and assert that it has not expired. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $id + * + * @throws InvalidConfirmationTokenException + * + * @return static + */ + public function scopeValidOrFail($query, $id) + { + $token = $query->find($id); + + if (! $token || $token->created_at < new DateTime('-1 day')) { + throw new InvalidConfirmationTokenException; + } + + return $token; + } +} diff --git a/app/Database/Models/User.php b/app/Database/Models/User.php index 9ef3cd8e..57d4621a 100644 --- a/app/Database/Models/User.php +++ b/app/Database/Models/User.php @@ -194,4 +194,14 @@ public function getHasher() { return $this->hasher; } + + /** + * Define the relationship with the user's access tokens. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function accessTokens() + { + return $this->hasMany('MyBB\Core\Database\Models\AccessToken'); + } } diff --git a/app/Exceptions/FloodingException.php b/app/Exceptions/FloodingException.php new file mode 100644 index 00000000..d79ff5bf --- /dev/null +++ b/app/Exceptions/FloodingException.php @@ -0,0 +1,17 @@ +attributes = $attributes; + $this->relationships = $relationships; + + $messages = [implode("\n", $attributes), implode("\n", $relationships)]; + + parent::__construct(implode("\n", $messages)); + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getRelationships() + { + return $this->relationships; + } +} diff --git a/app/Http/AbstractServer.php b/app/Http/AbstractServer.php index 97696c21..1779acff 100644 --- a/app/Http/AbstractServer.php +++ b/app/Http/AbstractServer.php @@ -7,6 +7,7 @@ * @license http://www.mybb.com/licenses/bsd3 BSD-3 */ +use MyBB\Core\Database\Models\AccessToken; use MyBB\Core\Kernel\AbstractServer as BaseAbstractServer; use MyBB\Core\Kernel\Application; use Psr\Http\Message\ResponseInterface; @@ -56,7 +57,8 @@ abstract protected function getMiddleware(Application $app); private function collectGarbage() { if ($this->hitsLottery()) { - //todo: Implement AccessToken, EmailToken, and PasswordToken models + //todo: Implement EmailToken, and PasswordToken models + AccessToken::whereRaw('last_activity <= ? - lifetime', [time()])->delete(); $earliestToKeep = date('Y-m-d H:i:s', time() - 24 * 60 * 60); } From df1f7a4ca7042cf9610e1e2e9bc69be6fbc3de2a Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 16 Dec 2016 21:10:01 -0500 Subject: [PATCH 5/6] Add table migration files, and more http server stuff --- app/Forum/UrlGenerator.php | 16 ++++ app/Http/AbstractUrlGenerator.php | 76 +++++++++++++++++++ app/Http/Controller/ControllerInterface.php | 21 +++++ ...2_17_013923_create_access_tokens_table.php | 32 ++++++++ ...016_12_17_014105_create_api_keys_table.php | 29 +++++++ .../2016_12_17_014247_create_config_table.php | 30 ++++++++ ..._12_17_014458_create_auth_tokens_table.php | 31 ++++++++ ...12_17_014559_create_email_tokens_table.php | 32 ++++++++ 8 files changed, 267 insertions(+) create mode 100644 app/Forum/UrlGenerator.php create mode 100644 app/Http/AbstractUrlGenerator.php create mode 100644 app/Http/Controller/ControllerInterface.php create mode 100644 database/migrations/2016_12_17_013923_create_access_tokens_table.php create mode 100644 database/migrations/2016_12_17_014105_create_api_keys_table.php create mode 100644 database/migrations/2016_12_17_014247_create_config_table.php create mode 100644 database/migrations/2016_12_17_014458_create_auth_tokens_table.php create mode 100644 database/migrations/2016_12_17_014559_create_email_tokens_table.php diff --git a/app/Forum/UrlGenerator.php b/app/Forum/UrlGenerator.php new file mode 100644 index 00000000..4bc45aef --- /dev/null +++ b/app/Forum/UrlGenerator.php @@ -0,0 +1,16 @@ +app = $app; + $this->routes = $routes; + } + + /** + * Generate a URL to a named route. + * + * @param string $name + * @param array $parameters + * @return string + */ + public function toRoute($name, $parameters = []) + { + $path = $this->routes->getPath($name, $parameters); + $path = ltrim($path, '/'); + + return $this->toBase().'/'.$path; + } + + /** + * Generate a URL to a path. + * + * @param string $path + * @return string + */ + public function toPath($path) + { + return $this->toBase().'/'.$path; + } + + /** + * Generate a URL to base with UrlGenerator's prefix. + * + * @return string + */ + public function toBase() + { + return $this->app->url($this->path); + } +} diff --git a/app/Http/Controller/ControllerInterface.php b/app/Http/Controller/ControllerInterface.php new file mode 100644 index 00000000..9349e2d0 --- /dev/null +++ b/app/Http/Controller/ControllerInterface.php @@ -0,0 +1,21 @@ +string('id', 100)->primary(); + $table->integer('user_id')->unsigned(); + $table->timestamp('created_at'); + $table->timestamp('expires_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('access_tokens'); + } +} diff --git a/database/migrations/2016_12_17_014105_create_api_keys_table.php b/database/migrations/2016_12_17_014105_create_api_keys_table.php new file mode 100644 index 00000000..1e88be55 --- /dev/null +++ b/database/migrations/2016_12_17_014105_create_api_keys_table.php @@ -0,0 +1,29 @@ +string('id', 100)->primary(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('api_keys'); + } +} diff --git a/database/migrations/2016_12_17_014247_create_config_table.php b/database/migrations/2016_12_17_014247_create_config_table.php new file mode 100644 index 00000000..7165c836 --- /dev/null +++ b/database/migrations/2016_12_17_014247_create_config_table.php @@ -0,0 +1,30 @@ +string('key', 100)->primary(); + $table->binary('value')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('config'); + } +} diff --git a/database/migrations/2016_12_17_014458_create_auth_tokens_table.php b/database/migrations/2016_12_17_014458_create_auth_tokens_table.php new file mode 100644 index 00000000..c33d94b3 --- /dev/null +++ b/database/migrations/2016_12_17_014458_create_auth_tokens_table.php @@ -0,0 +1,31 @@ +string('id', 100)->primary(); + $table->string('payload', 150); + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('auth_tokens'); + } +} diff --git a/database/migrations/2016_12_17_014559_create_email_tokens_table.php b/database/migrations/2016_12_17_014559_create_email_tokens_table.php new file mode 100644 index 00000000..cc867688 --- /dev/null +++ b/database/migrations/2016_12_17_014559_create_email_tokens_table.php @@ -0,0 +1,32 @@ +string('id', 100)->primary(); + $table->string('email', 150); + $table->integer('user_id')->unsigned(); + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('email_tokens'); + } +} From 28f26e079598455f9cf95487e8981a7c72288f83 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 17 Dec 2016 15:47:39 -0500 Subject: [PATCH 6/6] Fixes and added ExtensionManager and ExtensionServiceProvider --- app/Console/Command/AbstractCommand.php | 64 ++++ app/Database/Migration.php | 4 +- app/Database/MigrationCreator.php | 2 +- app/Database/Models/AccessToken.php | 2 +- app/Database/Models/AuthToken.php | 2 +- app/Database/Models/User.php | 2 +- app/Events/ConfigureModelDates.php | 14 +- .../ConfigureModelDefaultAttributes.php | 14 +- app/Events/ExtensionWasDisabled.php | 28 ++ app/Events/ExtensionWasEnabled.php | 28 ++ app/Events/ExtensionWasUninstalled.php | 26 ++ app/Extension/ExtensionManager.php | 322 ++++++++++++++++++ app/Extension/ExtensionServiceProvider.php | 31 ++ .../Controller/AbstractHtmlController.php | 36 ++ app/Kernel/AbstractServer.php | 5 +- 15 files changed, 559 insertions(+), 21 deletions(-) create mode 100644 app/Console/Command/AbstractCommand.php create mode 100644 app/Events/ExtensionWasDisabled.php create mode 100644 app/Events/ExtensionWasEnabled.php create mode 100644 app/Events/ExtensionWasUninstalled.php create mode 100644 app/Extension/ExtensionManager.php create mode 100644 app/Extension/ExtensionServiceProvider.php create mode 100644 app/Http/Controller/AbstractHtmlController.php diff --git a/app/Console/Command/AbstractCommand.php b/app/Console/Command/AbstractCommand.php new file mode 100644 index 00000000..d87269a9 --- /dev/null +++ b/app/Console/Command/AbstractCommand.php @@ -0,0 +1,64 @@ +input = $input; + $this->output = $output; + + $this->fire(); + } + + /** + * Fire the command. + */ + abstract protected function fire(); + + /** + * Did the user pass the given option? + * + * @param string $name + * @return bool + */ + protected function hasOption(string $name) + { + return $this->input->hasOption($name); + } + + /** + * Send an info string to the user. + * + * @param string $string + */ + protected function info(string $string) + { + $this->output->writeln("$string"); + } +} diff --git a/app/Database/Migration.php b/app/Database/Migration.php index 3c7c126e..74e9878f 100644 --- a/app/Database/Migration.php +++ b/app/Database/Migration.php @@ -93,8 +93,10 @@ public static function renameColumn($tableName, $from, $to) /** * Add default values for config values. + * @param array $defaults + * @return array */ - public static function addSettings($defaults) + public static function addSettings(array $defaults) { return [ 'up' => function (SettingsRepositoryInterface $settings) use ($defaults) { diff --git a/app/Database/MigrationCreator.php b/app/Database/MigrationCreator.php index 24f4ff29..cf278169 100644 --- a/app/Database/MigrationCreator.php +++ b/app/Database/MigrationCreator.php @@ -32,7 +32,7 @@ class MigrationCreator * @param Filesystem $files * @param string $publicPath */ - public function __construct(Filesystem $files, $publicPath) + public function __construct(Filesystem $files, string $publicPath) { $this->files = $files; $this->publicPath = $publicPath; diff --git a/app/Database/Models/AccessToken.php b/app/Database/Models/AccessToken.php index a135b659..426def02 100644 --- a/app/Database/Models/AccessToken.php +++ b/app/Database/Models/AccessToken.php @@ -65,6 +65,6 @@ public function touch() */ public function user() { - return $this->belongsTo('MyBB\Core\Database\Models\User'); + return $this->belongsTo(\MyBB\Core\Database\Models\User::class); } } diff --git a/app/Database/Models/AuthToken.php b/app/Database/Models/AuthToken.php index 44b56458..1749e670 100644 --- a/app/Database/Models/AuthToken.php +++ b/app/Database/Models/AuthToken.php @@ -42,7 +42,7 @@ class AuthToken extends AbstractModel * * @return static */ - public static function generate($payload) + public function generate($payload) { $token = new static; diff --git a/app/Database/Models/User.php b/app/Database/Models/User.php index 57d4621a..75d9a19c 100644 --- a/app/Database/Models/User.php +++ b/app/Database/Models/User.php @@ -202,6 +202,6 @@ public function getHasher() */ public function accessTokens() { - return $this->hasMany('MyBB\Core\Database\Models\AccessToken'); + return $this->hasMany(\MyBB\Core\Database\Models\AccessToken::class); } } diff --git a/app/Events/ConfigureModelDates.php b/app/Events/ConfigureModelDates.php index eb842667..fd85f543 100644 --- a/app/Events/ConfigureModelDates.php +++ b/app/Events/ConfigureModelDates.php @@ -20,7 +20,7 @@ class ConfigureModelDates /** * @var AbstractModel */ - public $model; + public $modelClass; /** * @var array @@ -28,21 +28,21 @@ class ConfigureModelDates public $dates; /** - * @param AbstractModel $model + * @param AbstractModel $modelClass * @param array $dates */ - public function __construct(AbstractModel $model, array &$dates) + public function __construct(AbstractModel $modelClass, array &$dates) { - $this->model = $model; + $this->model = $modelClass; $this->dates = &$dates; } /** - * @param string $model + * @param string $modelClass * @return bool */ - public function isModel($model) + public function isModel(string $modelClass) { - return $this->model instanceof $model; + return $this->modelClass instanceof $modelClass; } } diff --git a/app/Events/ConfigureModelDefaultAttributes.php b/app/Events/ConfigureModelDefaultAttributes.php index 8eddec2a..ddd8f76f 100644 --- a/app/Events/ConfigureModelDefaultAttributes.php +++ b/app/Events/ConfigureModelDefaultAttributes.php @@ -16,7 +16,7 @@ class ConfigureModelDefaultAttributes /** * @var AbstractModel */ - public $model; + public $modelClass; /** * @var array @@ -24,21 +24,21 @@ class ConfigureModelDefaultAttributes public $attributes; /** - * @param AbstractModel $model + * @param AbstractModel $modelClass * @param array $attributes */ - public function __construct(AbstractModel $model, array &$attributes) + public function __construct(AbstractModel $modelClass, array &$attributes) { - $this->model = $model; + $this->modelClass = $modelClass; $this->attributes = &$attributes; } /** - * @param string $model + * @param string $modelClass * @return bool */ - public function isModel($model) + public function isModel($modelClass) { - return $this->model instanceof $model; + return $this->modelClass instanceof $modelClass; } } diff --git a/app/Events/ExtensionWasDisabled.php b/app/Events/ExtensionWasDisabled.php new file mode 100644 index 00000000..7f670ec0 --- /dev/null +++ b/app/Events/ExtensionWasDisabled.php @@ -0,0 +1,28 @@ +extension = $extension; + } +} diff --git a/app/Events/ExtensionWasEnabled.php b/app/Events/ExtensionWasEnabled.php new file mode 100644 index 00000000..8636c8a4 --- /dev/null +++ b/app/Events/ExtensionWasEnabled.php @@ -0,0 +1,28 @@ +extension = $extension; + } +} diff --git a/app/Events/ExtensionWasUninstalled.php b/app/Events/ExtensionWasUninstalled.php new file mode 100644 index 00000000..cddb0ee9 --- /dev/null +++ b/app/Events/ExtensionWasUninstalled.php @@ -0,0 +1,26 @@ +extension = $extension; + } +} diff --git a/app/Extension/ExtensionManager.php b/app/Extension/ExtensionManager.php new file mode 100644 index 00000000..d52da268 --- /dev/null +++ b/app/Extension/ExtensionManager.php @@ -0,0 +1,322 @@ +config = $config; + $this->app = $app; + $this->migrator = $migrator; + $this->dispatcher = $dispatcher; + $this->filesystem = $filesystem; + } + + /** + * @return Collection + */ + public function getExtensions() + { + if (is_null($this->extensions) && $this->filesystem->exists($this->app->basePath().'/vendor/composer/installed.json')) { + $extensions = new Collection(); + + // Load all packages installed by composer. + $installed = json_decode($this->filesystem->get($this->app->basePath().'/vendor/composer/installed.json'), true); + + foreach ($installed as $package) { + if (Arr::get($package, 'type') != 'mybb-extension' || empty(Arr::get($package, 'name'))) { + continue; + } + // Instantiates an Extension object using the package path and composer.json file. + $extension = new Extension($this->getExtensionsDir().'/'.Arr::get($package, 'name'), $package); + + // Per default all extensions are installed if they are registered in composer. + $extension->setInstalled(true); + $extension->setVersion(Arr::get($package, 'version')); + $extension->setEnabled($this->isEnabled($extension->getId())); + + $extensions->put($extension->getId(), $extension); + } + $this->extensions = $extensions->sortBy(function ($extension, $name) { + return $extension->composerJsonAttribute('extra.mybb-extension.title'); + }); + } + + return $this->extensions; + } + + /** + * Loads an Extension with all information. + * + * @param string $name + * @return Extension|null + */ + public function getExtension($name) + { + return $this->getExtensions()->get($name); + } + + /** + * Enables the extension. + * + * @param string $name + */ + public function enable($name) + { + if (! $this->isEnabled($name)) { + $extension = $this->getExtension($name); + + $enabled = $this->getEnabled(); + + $enabled[] = $name; + + $this->migrate($extension); + + $this->publishAssets($extension); + + $this->setEnabled($enabled); + + $extension->setEnabled(true); + + $this->dispatcher->fire(new ExtensionWasEnabled($extension)); + } + } + + /** + * Disables an extension. + * + * @param string $name + */ + public function disable($name) + { + $enabled = $this->getEnabled(); + + if (($k = array_search($name, $enabled)) !== false) { + unset($enabled[$k]); + + $extension = $this->getExtension($name); + + $this->setEnabled($enabled); + + $extension->setEnabled(false); + + $this->dispatcher->fire(new ExtensionWasDisabled($extension)); + } + } + + /** + * Uninstalls an extension. + * + * @param string $name + */ + public function uninstall($name) + { + $extension = $this->getExtension($name); + + $this->disable($name); + + $this->migrateDown($extension); + + $this->unpublishAssets($extension); + + $extension->setInstalled(false); + + $this->dispatcher->fire(new ExtensionWasUninstalled($extension)); + } + + /** + * Copy the assets from an extension's assets directory into public view. + * + * @param Extension $extension + */ + protected function publishAssets(Extension $extension) + { + if ($extension->hasAssets()) { + $this->filesystem->copyDirectory( + $extension->getPath().'/assets', + $this->app->publicPath().'/assets/extensions/'.$extension->getId() + ); + } + } + + /** + * Delete an extension's assets from public view. + * + * @param Extension $extension + */ + protected function unpublishAssets(Extension $extension) + { + $this->filesystem->deleteDirectory($this->app->publicPath().'/assets/extensions/'.$extension); + } + + /** + * Get the path to an extension's published asset. + * + * @param Extension $extension + * @param string $path + * @return string + */ + public function getAsset(Extension $extension, string $path) + { + return $this->app->publicPath().'/assets/extensions/'.$extension->getId().$path; + } + + /** + * Runs the database migrations for the extension. + * + * @param Extension $extension + * @param bool|true $up + */ + public function migrate(Extension $extension, $up = true) + { + if ($extension->hasMigrations()) { + $migrationDir = $extension->getPath().'/migrations'; + + $this->app->bind('Illuminate\Database\Schema\Builder', function ($container) { + return $container->make('Illuminate\Database\ConnectionInterface')->getSchemaBuilder(); + }); + + if ($up) { + $this->migrator->run($migrationDir, $extension); + } else { + $this->migrator->reset($migrationDir, $extension); + } + } + } + + /** + * Runs the database migrations to reset the database to its old state. + * + * @param Extension $extension + */ + public function migrateDown(Extension $extension) + { + $this->migrate($extension, false); + } + + /** + * The database migrator. + * + * @return Migrator + */ + public function getMigrator() + { + return $this->migrator; + } + + /** + * Get only enabled extensions. + * + * @return Collection + */ + public function getEnabledExtensions() + { + return $this->getExtensions()->only($this->getEnabled()); + } + + /** + * Loads all bootstrap.php files of the enabled extensions. + * + * @return Collection + */ + public function getEnabledBootstrappers() + { + $bootstrappers = new Collection; + + foreach ($this->getEnabledExtensions() as $extension) { + if ($this->filesystem->exists($file = $extension->getPath().'/bootstrap.php')) { + $bootstrappers->push($file); + } + } + + return $bootstrappers; + } + + /** + * The id's of the enabled extensions. + * + * @return array + */ + public function getEnabled() + { + return json_decode($this->config->get('extensions_enabled'), true); + } + + /** + * Persist the currently enabled extensions. + * + * @param array $enabled + */ + protected function setEnabled(array $enabled) + { + $enabled = array_values(array_unique($enabled)); + + $this->config->set('extensions_enabled', json_encode($enabled)); + } + + /** + * Whether the extension is enabled. + * + * @param $extension + * @return bool + */ + public function isEnabled($extension) + { + return in_array($extension, $this->getEnabled()); + } + + /** + * The extensions path. + * + * @return string + */ + protected function getExtensionsDir() + { + return $this->app->basePath().'/vendor'; + } +} diff --git a/app/Extension/ExtensionServiceProvider.php b/app/Extension/ExtensionServiceProvider.php new file mode 100644 index 00000000..a571b7ec --- /dev/null +++ b/app/Extension/ExtensionServiceProvider.php @@ -0,0 +1,31 @@ +app->bind('mybb.extensions', \MyBB\Core\Extension\ExtensionManager::class); + + $bootstrappers = $this->app->make('mybb.extensions')->getEnabledBootstrappers(); + + foreach ($bootstrappers as $file) { + $bootstrapper = require $file; + + $this->app->call($bootstrapper); + } + } +} diff --git a/app/Http/Controller/AbstractHtmlController.php b/app/Http/Controller/AbstractHtmlController.php new file mode 100644 index 00000000..843ecacc --- /dev/null +++ b/app/Http/Controller/AbstractHtmlController.php @@ -0,0 +1,36 @@ +render($request); + + $response = new Response; + $response->getBody()->write($view); + + return $response; + } + + /** + * @param Request $request + * @return \Illuminate\Contracts\Support\Renderable + */ + abstract protected function render(Request $request); +} \ No newline at end of file diff --git a/app/Kernel/AbstractServer.php b/app/Kernel/AbstractServer.php index 1b59cc8d..8f8a3df0 100644 --- a/app/Kernel/AbstractServer.php +++ b/app/Kernel/AbstractServer.php @@ -166,7 +166,7 @@ public function getApp() $this->registerCache($app); //todo: Register Local, Twig, Database, and other needed providors here - $app->register('MyBB\Core\Settings\SettingsServiceProvider'); + $app->register(\MyBB\Core\Settings\SettingsRepositoryInterface::class); $app->register('Illuminate\Bus\BusServiceProvider'); $app->register('Illuminate\Filesystem\FilesystemServiceProvider'); $app->register('Illuminate\Hashing\HashServiceProvider'); @@ -192,7 +192,8 @@ public function getApp() $app->call($callback); } - //todo: Register ExtensionServiceProvider here + //register the ExtensionServiceProvider + $app->register(\MyBB\Core\Extension\ExtensionServiceProvider::class); } $app->boot();