From 92b7dd3335a6572debeacfb5faa82c63a5e67888 Mon Sep 17 00:00:00 2001 From: Marvin Borner Date: Fri, 8 Jun 2018 20:03:25 +0200 Subject: Some minor fixes --- main/app/sprinkles/core/src/Alert/AlertStream.php | 276 ++-- .../sprinkles/core/src/Alert/CacheAlertStream.php | 162 +-- .../core/src/Alert/SessionAlertStream.php | 134 +- .../core/src/Controller/SimpleController.php | 72 +- main/app/sprinkles/core/src/Core.php | 236 ++-- main/app/sprinkles/core/src/Database/Builder.php | 400 +++--- .../core/src/Database/DatabaseInvalidException.php | 42 +- .../src/Database/Migrations/v400/SessionsTable.php | 94 +- .../Database/Migrations/v400/ThrottlesTable.php | 102 +- .../Database/Models/Concerns/HasRelationships.php | 544 ++++---- .../sprinkles/core/src/Database/Models/Model.php | 266 ++-- .../core/src/Database/Models/Throttle.php | 74 +- .../Relations/BelongsToManyConstrained.php | 236 ++-- .../Database/Relations/BelongsToManyThrough.php | 446 +++---- .../src/Database/Relations/BelongsToManyUnique.php | 46 +- .../src/Database/Relations/Concerns/Syncable.php | 260 ++-- .../src/Database/Relations/Concerns/Unique.php | 1086 +++++++-------- .../src/Database/Relations/HasManySyncable.php | 46 +- .../src/Database/Relations/MorphManySyncable.php | 46 +- .../src/Database/Relations/MorphToManyUnique.php | 46 +- .../core/src/Error/ExceptionHandlerManager.php | 182 +-- .../core/src/Error/Handler/ExceptionHandler.php | 534 ++++---- .../Error/Handler/ExceptionHandlerInterface.php | 66 +- .../src/Error/Handler/HttpExceptionHandler.php | 124 +- .../src/Error/Handler/NotFoundExceptionHandler.php | 76 +- .../Error/Handler/PhpMailerExceptionHandler.php | 60 +- .../core/src/Error/Renderer/ErrorRenderer.php | 126 +- .../src/Error/Renderer/ErrorRendererInterface.php | 60 +- .../core/src/Error/Renderer/HtmlRenderer.php | 294 ++--- .../core/src/Error/Renderer/JsonRenderer.php | 110 +- .../core/src/Error/Renderer/PlainTextRenderer.php | 126 +- .../core/src/Error/Renderer/WhoopsRenderer.php | 1376 ++++++++++---------- .../core/src/Error/Renderer/XmlRenderer.php | 94 +- main/app/sprinkles/core/src/Facades/Debug.php | 56 +- main/app/sprinkles/core/src/Facades/Translator.php | 56 +- .../src/Http/Concerns/DeterminesContentType.php | 152 +-- .../app/sprinkles/core/src/Log/DatabaseHandler.php | 104 +- main/app/sprinkles/core/src/Log/MixedFormatter.php | 116 +- .../app/sprinkles/core/src/Mail/EmailRecipient.php | 258 ++-- main/app/sprinkles/core/src/Mail/MailMessage.php | 350 ++--- main/app/sprinkles/core/src/Mail/Mailer.php | 400 +++--- .../sprinkles/core/src/Mail/StaticMailMessage.php | 148 +-- .../sprinkles/core/src/Mail/TwigMailMessage.php | 178 +-- main/app/sprinkles/core/src/Model/UFModel.php | 54 +- main/app/sprinkles/core/src/Router.php | 200 +-- .../core/src/ServicesProvider/ServicesProvider.php | 1242 +++++++++--------- main/app/sprinkles/core/src/Sprunje/Sprunje.php | 1094 ++++++++-------- .../sprinkles/core/src/Throttle/ThrottleRule.php | 266 ++-- main/app/sprinkles/core/src/Throttle/Throttler.php | 344 ++--- .../core/src/Throttle/ThrottlerException.php | 38 +- main/app/sprinkles/core/src/Twig/CacheHelper.php | 114 +- main/app/sprinkles/core/src/Twig/CoreExtension.php | 240 ++-- .../core/src/Util/BadClassNameException.php | 38 +- main/app/sprinkles/core/src/Util/Captcha.php | 308 ++--- .../sprinkles/core/src/Util/CheckEnvironment.php | 662 +++++----- main/app/sprinkles/core/src/Util/ClassMapper.php | 180 +-- .../sprinkles/core/src/Util/EnvironmentInfo.php | 134 +- .../sprinkles/core/src/Util/ShutdownHandler.php | 324 ++--- main/app/sprinkles/core/src/Util/Util.php | 348 ++--- 59 files changed, 7623 insertions(+), 7623 deletions(-) (limited to 'main/app/sprinkles/core/src') diff --git a/main/app/sprinkles/core/src/Alert/AlertStream.php b/main/app/sprinkles/core/src/Alert/AlertStream.php index adb9b5b..2db441f 100644 --- a/main/app/sprinkles/core/src/Alert/AlertStream.php +++ b/main/app/sprinkles/core/src/Alert/AlertStream.php @@ -1,138 +1,138 @@ -messagesKey = $messagesKey; - - $this->setTranslator($translator); - } - - /** - * Set the translator to be used for all message streams. Must be done before `addMessageTranslated` can be used. - * - * @param UserFrosting\I18n\MessageTranslator $translator A MessageTranslator to be used to translate messages when added via `addMessageTranslated`. - */ - public function setTranslator($translator) { - $this->messageTranslator = $translator; - return $this; - } - - /** - * Adds a raw text message to the cache message stream. - * - * @param string $type The type of message, indicating how it will be styled when outputted. Should be set to "success", "danger", "warning", or "info". - * @param string $message The message to be added to the message stream. - * @return MessageStream this MessageStream object. - */ - public function addMessage($type, $message) { - $messages = $this->messages(); - $messages[] = array( - "type" => $type, - "message" => $message - ); - $this->saveMessages($messages); - return $this; - } - - /** - * Adds a text message to the cache message stream, translated into the currently selected language. - * - * @param string $type The type of message, indicating how it will be styled when outputted. Should be set to "success", "danger", "warning", or "info". - * @param string $messageId The message id for the message to be added to the message stream. - * @param array[string] $placeholders An optional hash of placeholder names => placeholder values to substitute into the translated message. - * @return MessageStream this MessageStream object. - */ - public function addMessageTranslated($type, $messageId, $placeholders = array()) { - if (!$this->messageTranslator) { - throw new \RuntimeException("No translator has been set! Please call MessageStream::setTranslator first."); - } - - $message = $this->messageTranslator->translate($messageId, $placeholders); - return $this->addMessage($type, $message); - } - - /** - * Get the messages and then clear the message stream. - * This function does the same thing as `messages()`, except that it also clears all messages afterwards. - * This is useful, because typically we don't want to view the same messages more than once. - * - * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. - */ - public function getAndClearMessages() { - $messages = $this->messages(); - $this->resetMessageStream(); - return $messages; - } - - /** - * Add error messages from a ServerSideValidator object to the message stream. - * - * @param ServerSideValidator $validator - */ - public function addValidationErrors(ServerSideValidator $validator) { - foreach ($validator->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $this->addMessage("danger", $error); - } - } - } - - /** - * Return the translator for this message stream. - * - * @return MessageTranslator The translator for this message stream. - */ - public function translator() { - return $this->messageTranslator; - } - - /** - * Get the messages from this message stream. - * - * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. - */ - abstract public function messages(); - - /** - * Clear all messages from this message stream. - */ - abstract public function resetMessageStream(); - - /** - * Save messages to the stream - */ - abstract protected function saveMessages($message); -} +messagesKey = $messagesKey; + + $this->setTranslator($translator); + } + + /** + * Set the translator to be used for all message streams. Must be done before `addMessageTranslated` can be used. + * + * @param UserFrosting\I18n\MessageTranslator $translator A MessageTranslator to be used to translate messages when added via `addMessageTranslated`. + */ + public function setTranslator($translator) { + $this->messageTranslator = $translator; + return $this; + } + + /** + * Adds a raw text message to the cache message stream. + * + * @param string $type The type of message, indicating how it will be styled when outputted. Should be set to "success", "danger", "warning", or "info". + * @param string $message The message to be added to the message stream. + * @return MessageStream this MessageStream object. + */ + public function addMessage($type, $message) { + $messages = $this->messages(); + $messages[] = array( + "type" => $type, + "message" => $message + ); + $this->saveMessages($messages); + return $this; + } + + /** + * Adds a text message to the cache message stream, translated into the currently selected language. + * + * @param string $type The type of message, indicating how it will be styled when outputted. Should be set to "success", "danger", "warning", or "info". + * @param string $messageId The message id for the message to be added to the message stream. + * @param array[string] $placeholders An optional hash of placeholder names => placeholder values to substitute into the translated message. + * @return MessageStream this MessageStream object. + */ + public function addMessageTranslated($type, $messageId, $placeholders = array()) { + if (!$this->messageTranslator) { + throw new \RuntimeException("No translator has been set! Please call MessageStream::setTranslator first."); + } + + $message = $this->messageTranslator->translate($messageId, $placeholders); + return $this->addMessage($type, $message); + } + + /** + * Get the messages and then clear the message stream. + * This function does the same thing as `messages()`, except that it also clears all messages afterwards. + * This is useful, because typically we don't want to view the same messages more than once. + * + * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. + */ + public function getAndClearMessages() { + $messages = $this->messages(); + $this->resetMessageStream(); + return $messages; + } + + /** + * Add error messages from a ServerSideValidator object to the message stream. + * + * @param ServerSideValidator $validator + */ + public function addValidationErrors(ServerSideValidator $validator) { + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $this->addMessage("danger", $error); + } + } + } + + /** + * Return the translator for this message stream. + * + * @return MessageTranslator The translator for this message stream. + */ + public function translator() { + return $this->messageTranslator; + } + + /** + * Get the messages from this message stream. + * + * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. + */ + abstract public function messages(); + + /** + * Clear all messages from this message stream. + */ + abstract public function resetMessageStream(); + + /** + * Save messages to the stream + */ + abstract protected function saveMessages($message); +} diff --git a/main/app/sprinkles/core/src/Alert/CacheAlertStream.php b/main/app/sprinkles/core/src/Alert/CacheAlertStream.php index f3f6489..8d31462 100644 --- a/main/app/sprinkles/core/src/Alert/CacheAlertStream.php +++ b/main/app/sprinkles/core/src/Alert/CacheAlertStream.php @@ -1,81 +1,81 @@ -cache = $cache; - $this->config = $config; - parent::__construct($messagesKey, $translator); - } - - /** - * Get the messages from this message stream. - * - * @return array An array of messages, each of which is itself an array containing 'type' and 'message' fields. - */ - public function messages() { - if ($this->cache->tags('_s' . session_id())->has($this->messagesKey)) { - return $this->cache->tags('_s' . session_id())->get($this->messagesKey) ?: []; - } else { - return []; - } - } - - /** - * Clear all messages from this message stream. - * - * @return void - */ - public function resetMessageStream() { - $this->cache->tags('_s' . session_id())->forget($this->messagesKey); - } - - /** - * Save messages to the stream - * - * @param string $messages The message - * @return void - */ - protected function saveMessages($messages) { - $this->cache->tags('_s' . session_id())->forever($this->messagesKey, $messages); - } -} +cache = $cache; + $this->config = $config; + parent::__construct($messagesKey, $translator); + } + + /** + * Get the messages from this message stream. + * + * @return array An array of messages, each of which is itself an array containing 'type' and 'message' fields. + */ + public function messages() { + if ($this->cache->tags('_s' . session_id())->has($this->messagesKey)) { + return $this->cache->tags('_s' . session_id())->get($this->messagesKey) ?: []; + } else { + return []; + } + } + + /** + * Clear all messages from this message stream. + * + * @return void + */ + public function resetMessageStream() { + $this->cache->tags('_s' . session_id())->forget($this->messagesKey); + } + + /** + * Save messages to the stream + * + * @param string $messages The message + * @return void + */ + protected function saveMessages($messages) { + $this->cache->tags('_s' . session_id())->forever($this->messagesKey, $messages); + } +} diff --git a/main/app/sprinkles/core/src/Alert/SessionAlertStream.php b/main/app/sprinkles/core/src/Alert/SessionAlertStream.php index fec0973..47a7ff7 100644 --- a/main/app/sprinkles/core/src/Alert/SessionAlertStream.php +++ b/main/app/sprinkles/core/src/Alert/SessionAlertStream.php @@ -1,67 +1,67 @@ -session = $session; - parent::__construct($messagesKey, $translator); - } - - /** - * Get the messages from this message stream. - * - * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. - */ - public function messages() { - return $this->session[$this->messagesKey] ?: []; - } - - /** - * Clear all messages from this message stream. - * - * @return void - */ - public function resetMessageStream() { - $this->session[$this->messagesKey] = []; - } - - /** - * Save messages to the stream - * - * @param string $messages The message - * @return void - */ - protected function saveMessages($messages) { - $this->session[$this->messagesKey] = $messages; - } -} +session = $session; + parent::__construct($messagesKey, $translator); + } + + /** + * Get the messages from this message stream. + * + * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. + */ + public function messages() { + return $this->session[$this->messagesKey] ?: []; + } + + /** + * Clear all messages from this message stream. + * + * @return void + */ + public function resetMessageStream() { + $this->session[$this->messagesKey] = []; + } + + /** + * Save messages to the stream + * + * @param string $messages The message + * @return void + */ + protected function saveMessages($messages) { + $this->session[$this->messagesKey] = $messages; + } +} diff --git a/main/app/sprinkles/core/src/Controller/SimpleController.php b/main/app/sprinkles/core/src/Controller/SimpleController.php index 1e3303a..1e5c45a 100644 --- a/main/app/sprinkles/core/src/Controller/SimpleController.php +++ b/main/app/sprinkles/core/src/Controller/SimpleController.php @@ -1,36 +1,36 @@ -ci = $ci; - } -} +ci = $ci; + } +} diff --git a/main/app/sprinkles/core/src/Core.php b/main/app/sprinkles/core/src/Core.php index 9518fe2..6bf1e36 100644 --- a/main/app/sprinkles/core/src/Core.php +++ b/main/app/sprinkles/core/src/Core.php @@ -1,118 +1,118 @@ - ['onSprinklesInitialized', 0], - 'onSprinklesRegisterServices' => ['onSprinklesRegisterServices', 0], - 'onAddGlobalMiddleware' => ['onAddGlobalMiddleware', 0] - ]; - } - - /** - * Set static references to DI container in necessary classes. - */ - public function onSprinklesInitialized() { - // Set container for data model - Model::$ci = $this->ci; - - // Set container for environment info class - EnvironmentInfo::$ci = $this->ci; - } - - /** - * Get shutdownHandler set up. This needs to be constructed explicitly because it's invoked natively by PHP. - */ - public function onSprinklesRegisterServices() { - // Set up any global PHP settings from the config service. - $config = $this->ci->config; - - // Display PHP fatal errors natively. - if (isset($config['php.display_errors_native'])) { - ini_set('display_errors', $config['php.display_errors_native']); - } - - // Log PHP fatal errors - if (isset($config['php.log_errors'])) { - ini_set('log_errors', $config['php.log_errors']); - } - - // Configure error-reporting level - if (isset($config['php.error_reporting'])) { - error_reporting($config['php.error_reporting']); - } - - // Configure time zone - if (isset($config['php.timezone'])) { - date_default_timezone_set($config['php.timezone']); - } - - // Determine if error display is enabled in the shutdown handler. - $displayErrors = FALSE; - if (in_array(strtolower($config['php.display_errors']), [ - '1', - 'on', - 'true', - 'yes' - ])) { - $displayErrors = TRUE; - } - - $sh = new ShutdownHandler($this->ci, $displayErrors); - $sh->register(); - } - - /** - * Add CSRF middleware. - */ - public function onAddGlobalMiddleware(Event $event) { - $request = $this->ci->request; - $path = $request->getUri()->getPath(); - $method = $request->getMethod(); - - // Normalize path to always have a leading slash - $path = '/' . ltrim($path, '/'); - // Normalize method to uppercase - $method = strtoupper($method); - - $csrfBlacklist = $this->ci->config['csrf.blacklist']; - $isBlacklisted = FALSE; - - // Go through the blacklist and determine if the path and method match any of the blacklist entries. - foreach ($csrfBlacklist as $pattern => $methods) { - $methods = array_map('strtoupper', (array)$methods); - if (in_array($method, $methods) && $pattern != '' && preg_match('~' . $pattern . '~', $path)) { - $isBlacklisted = TRUE; - break; - } - } - - if (!$path || !$isBlacklisted) { - $app = $event->getApp(); - $app->add($this->ci->csrf); - } - } -} + ['onSprinklesInitialized', 0], + 'onSprinklesRegisterServices' => ['onSprinklesRegisterServices', 0], + 'onAddGlobalMiddleware' => ['onAddGlobalMiddleware', 0] + ]; + } + + /** + * Set static references to DI container in necessary classes. + */ + public function onSprinklesInitialized() { + // Set container for data model + Model::$ci = $this->ci; + + // Set container for environment info class + EnvironmentInfo::$ci = $this->ci; + } + + /** + * Get shutdownHandler set up. This needs to be constructed explicitly because it's invoked natively by PHP. + */ + public function onSprinklesRegisterServices() { + // Set up any global PHP settings from the config service. + $config = $this->ci->config; + + // Display PHP fatal errors natively. + if (isset($config['php.display_errors_native'])) { + ini_set('display_errors', $config['php.display_errors_native']); + } + + // Log PHP fatal errors + if (isset($config['php.log_errors'])) { + ini_set('log_errors', $config['php.log_errors']); + } + + // Configure error-reporting level + if (isset($config['php.error_reporting'])) { + error_reporting($config['php.error_reporting']); + } + + // Configure time zone + if (isset($config['php.timezone'])) { + date_default_timezone_set($config['php.timezone']); + } + + // Determine if error display is enabled in the shutdown handler. + $displayErrors = FALSE; + if (in_array(strtolower($config['php.display_errors']), [ + '1', + 'on', + 'true', + 'yes' + ])) { + $displayErrors = TRUE; + } + + $sh = new ShutdownHandler($this->ci, $displayErrors); + $sh->register(); + } + + /** + * Add CSRF middleware. + */ + public function onAddGlobalMiddleware(Event $event) { + $request = $this->ci->request; + $path = $request->getUri()->getPath(); + $method = $request->getMethod(); + + // Normalize path to always have a leading slash + $path = '/' . ltrim($path, '/'); + // Normalize method to uppercase + $method = strtoupper($method); + + $csrfBlacklist = $this->ci->config['csrf.blacklist']; + $isBlacklisted = FALSE; + + // Go through the blacklist and determine if the path and method match any of the blacklist entries. + foreach ($csrfBlacklist as $pattern => $methods) { + $methods = array_map('strtoupper', (array)$methods); + if (in_array($method, $methods) && $pattern != '' && preg_match('~' . $pattern . '~', $path)) { + $isBlacklisted = TRUE; + break; + } + } + + if (!$path || !$isBlacklisted) { + $app = $event->getApp(); + $app->add($this->ci->csrf); + } + } +} diff --git a/main/app/sprinkles/core/src/Database/Builder.php b/main/app/sprinkles/core/src/Database/Builder.php index cebc318..1ed26ff 100644 --- a/main/app/sprinkles/core/src/Database/Builder.php +++ b/main/app/sprinkles/core/src/Database/Builder.php @@ -1,200 +1,200 @@ -where($field, 'LIKE', "$value%"); - } - - /** - * Perform an "ends with" pattern match on a specified column in a query. - * - * @param $query - * @param $field string The column to match - * @param $value string The value to match - */ - public function endsWith($field, $value) { - return $this->where($field, 'LIKE', "%$value"); - } - - /** - * Add columns to be excluded from the query. - * - * @param $value array|string The column(s) to exclude - * @return $this - */ - public function exclude($column) { - $column = is_array($column) ? $column : func_get_args(); - - $this->excludedColumns = array_merge((array)$this->excludedColumns, $column); - - return $this; - } - - /** - * Perform a pattern match on a specified column in a query. - * @param $query - * @param $field string The column to match - * @param $value string The value to match - */ - public function like($field, $value) { - return $this->where($field, 'LIKE', "%$value%"); - } - - /** - * Perform a pattern match on a specified column in a query. - * @param $query - * @param $field string The column to match - * @param $value string The value to match - */ - public function orLike($field, $value) { - return $this->orWhere($field, 'LIKE', "%$value%"); - } - - /** - * Execute the query as a "select" statement. - * - * @param array $columns - * @return \Illuminate\Support\Collection - */ - public function get($columns = ['*']) { - $original = $this->columns; - - if (is_null($original)) { - $this->columns = $columns; - } - - // Exclude any explicitly excluded columns - if (!is_null($this->excludedColumns)) { - $this->removeExcludedSelectColumns(); - } - - $results = $this->processor->processSelect($this, $this->runSelect()); - - $this->columns = $original; - - return collect($results); - } - - /** - * Remove excluded columns from the select column list. - */ - protected function removeExcludedSelectColumns() { - // Convert current column list and excluded column list to fully-qualified list - $this->columns = $this->convertColumnsToFullyQualified($this->columns); - $excludedColumns = $this->convertColumnsToFullyQualified($this->excludedColumns); - - // Remove any explicitly referenced excludable columns - $this->columns = array_diff($this->columns, $excludedColumns); - - // Replace any remaining wildcard columns (*, table.*, etc) with a list - // of fully-qualified column names - $this->columns = $this->replaceWildcardColumns($this->columns); - - $this->columns = array_diff($this->columns, $excludedColumns); - } - - /** - * Find any wildcard columns ('*'), remove it from the column list and replace with an explicit list of columns. - * - * @param array $columns - * @return array - */ - protected function replaceWildcardColumns(array $columns) { - $wildcardTables = $this->findWildcardTables($columns); - - foreach ($wildcardTables as $wildColumn => $table) { - $schemaColumns = $this->getQualifiedColumnNames($table); - - // Remove the `*` or `.*` column and replace with the individual schema columns - $columns = array_diff($columns, [$wildColumn]); - $columns = array_merge($columns, $schemaColumns); - } - - return $columns; - } - - /** - * Return a list of wildcard columns from the list of columns, mapping columns to their corresponding tables. - * - * @param array $columns - * @return array - */ - protected function findWildcardTables(array $columns) { - $tables = []; - - foreach ($columns as $column) { - if ($column == '*') { - $tables[$column] = $this->from; - continue; - } - - if (substr($column, -1) == '*') { - $tableName = explode('.', $column)[0]; - if ($tableName) { - $tables[$column] = $tableName; - } - } - } - - return $tables; - } - - /** - * Gets the fully qualified column names for a specified table. - * - * @param string $table - * @return array - */ - protected function getQualifiedColumnNames($table = NULL) { - $schema = $this->getConnection()->getSchemaBuilder(); - - return $this->convertColumnsToFullyQualified($schema->getColumnListing($table), $table); - } - - /** - * Fully qualify any unqualified columns in a list with this builder's table name. - * - * @param array $columns - * @return array - */ - protected function convertColumnsToFullyQualified($columns, $table = NULL) { - if (is_null($table)) { - $table = $this->from; - } - - array_walk($columns, function (&$item, $key) use ($table) { - if (strpos($item, '.') === FALSE) { - $item = "$table.$item"; - } - }); - - return $columns; - } -} +where($field, 'LIKE', "$value%"); + } + + /** + * Perform an "ends with" pattern match on a specified column in a query. + * + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function endsWith($field, $value) { + return $this->where($field, 'LIKE', "%$value"); + } + + /** + * Add columns to be excluded from the query. + * + * @param $value array|string The column(s) to exclude + * @return $this + */ + public function exclude($column) { + $column = is_array($column) ? $column : func_get_args(); + + $this->excludedColumns = array_merge((array)$this->excludedColumns, $column); + + return $this; + } + + /** + * Perform a pattern match on a specified column in a query. + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function like($field, $value) { + return $this->where($field, 'LIKE', "%$value%"); + } + + /** + * Perform a pattern match on a specified column in a query. + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function orLike($field, $value) { + return $this->orWhere($field, 'LIKE', "%$value%"); + } + + /** + * Execute the query as a "select" statement. + * + * @param array $columns + * @return \Illuminate\Support\Collection + */ + public function get($columns = ['*']) { + $original = $this->columns; + + if (is_null($original)) { + $this->columns = $columns; + } + + // Exclude any explicitly excluded columns + if (!is_null($this->excludedColumns)) { + $this->removeExcludedSelectColumns(); + } + + $results = $this->processor->processSelect($this, $this->runSelect()); + + $this->columns = $original; + + return collect($results); + } + + /** + * Remove excluded columns from the select column list. + */ + protected function removeExcludedSelectColumns() { + // Convert current column list and excluded column list to fully-qualified list + $this->columns = $this->convertColumnsToFullyQualified($this->columns); + $excludedColumns = $this->convertColumnsToFullyQualified($this->excludedColumns); + + // Remove any explicitly referenced excludable columns + $this->columns = array_diff($this->columns, $excludedColumns); + + // Replace any remaining wildcard columns (*, table.*, etc) with a list + // of fully-qualified column names + $this->columns = $this->replaceWildcardColumns($this->columns); + + $this->columns = array_diff($this->columns, $excludedColumns); + } + + /** + * Find any wildcard columns ('*'), remove it from the column list and replace with an explicit list of columns. + * + * @param array $columns + * @return array + */ + protected function replaceWildcardColumns(array $columns) { + $wildcardTables = $this->findWildcardTables($columns); + + foreach ($wildcardTables as $wildColumn => $table) { + $schemaColumns = $this->getQualifiedColumnNames($table); + + // Remove the `*` or `.*` column and replace with the individual schema columns + $columns = array_diff($columns, [$wildColumn]); + $columns = array_merge($columns, $schemaColumns); + } + + return $columns; + } + + /** + * Return a list of wildcard columns from the list of columns, mapping columns to their corresponding tables. + * + * @param array $columns + * @return array + */ + protected function findWildcardTables(array $columns) { + $tables = []; + + foreach ($columns as $column) { + if ($column == '*') { + $tables[$column] = $this->from; + continue; + } + + if (substr($column, -1) == '*') { + $tableName = explode('.', $column)[0]; + if ($tableName) { + $tables[$column] = $tableName; + } + } + } + + return $tables; + } + + /** + * Gets the fully qualified column names for a specified table. + * + * @param string $table + * @return array + */ + protected function getQualifiedColumnNames($table = NULL) { + $schema = $this->getConnection()->getSchemaBuilder(); + + return $this->convertColumnsToFullyQualified($schema->getColumnListing($table), $table); + } + + /** + * Fully qualify any unqualified columns in a list with this builder's table name. + * + * @param array $columns + * @return array + */ + protected function convertColumnsToFullyQualified($columns, $table = NULL) { + if (is_null($table)) { + $table = $this->from; + } + + array_walk($columns, function (&$item, $key) use ($table) { + if (strpos($item, '.') === FALSE) { + $item = "$table.$item"; + } + }); + + return $columns; + } +} diff --git a/main/app/sprinkles/core/src/Database/DatabaseInvalidException.php b/main/app/sprinkles/core/src/Database/DatabaseInvalidException.php index 0eba67b..ec30aa6 100644 --- a/main/app/sprinkles/core/src/Database/DatabaseInvalidException.php +++ b/main/app/sprinkles/core/src/Database/DatabaseInvalidException.php @@ -1,21 +1,21 @@ -schema->hasTable('sessions')) { - $this->schema->create('sessions', function (Blueprint $table) { - $table->string('id')->unique(); - $table->integer('user_id')->nullable(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->text('payload'); - $table->integer('last_activity'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('sessions'); - } -} +schema->hasTable('sessions')) { + $this->schema->create('sessions', function (Blueprint $table) { + $table->string('id')->unique(); + $table->integer('user_id')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->text('payload'); + $table->integer('last_activity'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('sessions'); + } +} diff --git a/main/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php b/main/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php index f74fee8..676b44e 100644 --- a/main/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php +++ b/main/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php @@ -1,51 +1,51 @@ -schema->hasTable('throttles')) { - $this->schema->create('throttles', function (Blueprint $table) { - $table->increments('id'); - $table->string('type'); - $table->string('ip')->nullable(); - $table->text('request_data')->nullable(); - $table->timestamps(); - - $table->engine = 'InnoDB'; - $table->collation = 'utf8_unicode_ci'; - $table->charset = 'utf8'; - $table->index('type'); - $table->index('ip'); - }); - } - } - - /** - * {@inheritDoc} - */ - public function down() { - $this->schema->drop('throttles'); - } -} +schema->hasTable('throttles')) { + $this->schema->create('throttles', function (Blueprint $table) { + $table->increments('id'); + $table->string('type'); + $table->string('ip')->nullable(); + $table->text('request_data')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->index('type'); + $table->index('ip'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() { + $this->schema->drop('throttles'); + } +} diff --git a/main/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php b/main/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php index 919c108..ccccf32 100644 --- a/main/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php +++ b/main/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php @@ -1,272 +1,272 @@ -newRelatedInstance($related); - - $foreignKey = $foreignKey ?: $this->getForeignKey(); - - $localKey = $localKey ?: $this->getKeyName(); - - return new HasManySyncable( - $instance->newQuery(), $this, $instance->getTable() . '.' . $foreignKey, $localKey - ); - } - - /** - * Overrides the default Eloquent morphMany relationship to return a MorphManySyncable. - * - * {@inheritDoc} - * @return \UserFrosting\Sprinkle\Core\Database\Relations\MorphManySyncable - */ - public function morphMany($related, $name, $type = NULL, $id = NULL, $localKey = NULL) { - $instance = $this->newRelatedInstance($related); - - // Here we will gather up the morph type and ID for the relationship so that we - // can properly query the intermediate table of a relation. Finally, we will - // get the table and create the relationship instances for the developers. - list($type, $id) = $this->getMorphs($name, $type, $id); - $table = $instance->getTable(); - $localKey = $localKey ?: $this->getKeyName(); - - return new MorphManySyncable($instance->newQuery(), $this, $table . '.' . $type, $table . '.' . $id, $localKey); - } - - /** - * Define a many-to-many 'through' relationship. - * This is basically hasManyThrough for many-to-many relationships. - * - * @param string $related - * @param string $through - * @param string $firstJoiningTable - * @param string $firstForeignKey - * @param string $firstRelatedKey - * @param string $secondJoiningTable - * @param string $secondForeignKey - * @param string $secondRelatedKey - * @param string $throughRelation - * @param string $relation - * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough - */ - public function belongsToManyThrough( - $related, - $through, - $firstJoiningTable = NULL, - $firstForeignKey = NULL, - $firstRelatedKey = NULL, - $secondJoiningTable = NULL, - $secondForeignKey = NULL, - $secondRelatedKey = NULL, - $throughRelation = NULL, - $relation = NULL - ) { - // If no relationship name was passed, we will pull backtraces to get the - // name of the calling function. We will use that function name as the - // title of this relation since that is a great convention to apply. - if (is_null($relation)) { - $relation = $this->guessBelongsToManyRelation(); - } - - // Create models for through and related - $through = new $through; - $related = $this->newRelatedInstance($related); - - if (is_null($throughRelation)) { - $throughRelation = $through->getTable(); - } - - // If no table names were provided, we can guess it by concatenating the parent - // and through table names. The two model names are transformed to snake case - // from their default CamelCase also. - if (is_null($firstJoiningTable)) { - $firstJoiningTable = $this->joiningTable($through); - } - - if (is_null($secondJoiningTable)) { - $secondJoiningTable = $through->joiningTable($related); - } - - $firstForeignKey = $firstForeignKey ?: $this->getForeignKey(); - $firstRelatedKey = $firstRelatedKey ?: $through->getForeignKey(); - $secondForeignKey = $secondForeignKey ?: $through->getForeignKey(); - $secondRelatedKey = $secondRelatedKey ?: $related->getForeignKey(); - - // This relationship maps the top model (this) to the through model. - $intermediateRelationship = $this->belongsToMany($through, $firstJoiningTable, $firstForeignKey, $firstRelatedKey, $throughRelation) - ->withPivot($firstForeignKey); - - // Now we set up the relationship with the related model. - $query = new BelongsToManyThrough( - $related->newQuery(), $this, $intermediateRelationship, $secondJoiningTable, $secondForeignKey, $secondRelatedKey, $relation - ); - - return $query; - } - - /** - * Define a unique many-to-many relationship. Similar to a regular many-to-many relationship, but removes duplicate child objects. - * Can also be used to implement ternary relationships. - * - * {@inheritDoc} - * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyUnique - */ - public function belongsToManyUnique($related, $table = NULL, $foreignKey = NULL, $relatedKey = NULL, $relation = NULL) { - // If no relationship name was passed, we will pull backtraces to get the - // name of the calling function. We will use that function name as the - // title of this relation since that is a great convention to apply. - if (is_null($relation)) { - $relation = $this->guessBelongsToManyRelation(); - } - - // First, we'll need to determine the foreign key and "other key" for the - // relationship. Once we have determined the keys we'll make the query - // instances as well as the relationship instances we need for this. - $instance = $this->newRelatedInstance($related); - - $foreignKey = $foreignKey ?: $this->getForeignKey(); - - $relatedKey = $relatedKey ?: $instance->getForeignKey(); - - // If no table name was provided, we can guess it by concatenating the two - // models using underscores in alphabetical order. The two model names - // are transformed to snake case from their default CamelCase also. - if (is_null($table)) { - $table = $this->joiningTable($related); - } - - return new BelongsToManyUnique( - $instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation - ); - } - - /** - * Define a unique morphs-to-many relationship. Similar to a regular morphs-to-many relationship, but removes duplicate child objects. - * - * {@inheritDoc} - * @return \UserFrosting\Sprinkle\Core\Database\Relations\MorphToManyUnique - */ - public function morphToManyUnique($related, $name, $table = NULL, $foreignKey = NULL, $otherKey = NULL, $inverse = FALSE) { - $caller = $this->getBelongsToManyCaller(); - - // First, we will need to determine the foreign key and "other key" for the - // relationship. Once we have determined the keys we will make the query - // instances, as well as the relationship instances we need for these. - $foreignKey = $foreignKey ?: $name . '_id'; - - $instance = new $related; - - $otherKey = $otherKey ?: $instance->getForeignKey(); - - // Now we're ready to create a new query builder for this related model and - // the relationship instances for this relation. This relations will set - // appropriate query constraints then entirely manages the hydrations. - $query = $instance->newQuery(); - - $table = $table ?: Str::plural($name); - - return new MorphToManyUnique( - $query, $this, $name, $table, $foreignKey, - $otherKey, $caller, $inverse - ); - } - - /** - * Define a constrained many-to-many relationship. - * This is similar to a regular many-to-many, but constrains the child results to match an additional constraint key in the parent object. - * This has been superseded by the belongsToManyUnique relationship's `withTernary` method since 4.1.7. - * - * @deprecated since 4.1.6 - * @param string $related - * @param string $constraintKey - * @param string $table - * @param string $foreignKey - * @param string $relatedKey - * @param string $relation - * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyConstrained - */ - public function belongsToManyConstrained($related, $constraintKey, $table = NULL, $foreignKey = NULL, $relatedKey = NULL, $relation = NULL) { - // If no relationship name was passed, we will pull backtraces to get the - // name of the calling function. We will use that function name as the - // title of this relation since that is a great convention to apply. - if (is_null($relation)) { - $relation = $this->guessBelongsToManyRelation(); - } - - // First, we'll need to determine the foreign key and "other key" for the - // relationship. Once we have determined the keys we'll make the query - // instances as well as the relationship instances we need for this. - $instance = $this->newRelatedInstance($related); - - $foreignKey = $foreignKey ?: $this->getForeignKey(); - - $relatedKey = $relatedKey ?: $instance->getForeignKey(); - - // If no table name was provided, we can guess it by concatenating the two - // models using underscores in alphabetical order. The two model names - // are transformed to snake case from their default CamelCase also. - if (is_null($table)) { - $table = $this->joiningTable($related); - } - - return new BelongsToManyConstrained( - $instance->newQuery(), $this, $constraintKey, $table, $foreignKey, $relatedKey, $relation - ); - } - - /** - * Get the relationship name of the belongs to many. - * - * @return string - */ - protected function getBelongsToManyCaller() { - $self = __FUNCTION__; - - $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($key, $trace) use ($self) { - $caller = $trace['function']; - return !in_array($caller, HasRelationships::$manyMethodsExtended) && $caller != $self; - }); - - return !is_null($caller) ? $caller['function'] : NULL; - } -} +newRelatedInstance($related); + + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $localKey = $localKey ?: $this->getKeyName(); + + return new HasManySyncable( + $instance->newQuery(), $this, $instance->getTable() . '.' . $foreignKey, $localKey + ); + } + + /** + * Overrides the default Eloquent morphMany relationship to return a MorphManySyncable. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\MorphManySyncable + */ + public function morphMany($related, $name, $type = NULL, $id = NULL, $localKey = NULL) { + $instance = $this->newRelatedInstance($related); + + // Here we will gather up the morph type and ID for the relationship so that we + // can properly query the intermediate table of a relation. Finally, we will + // get the table and create the relationship instances for the developers. + list($type, $id) = $this->getMorphs($name, $type, $id); + $table = $instance->getTable(); + $localKey = $localKey ?: $this->getKeyName(); + + return new MorphManySyncable($instance->newQuery(), $this, $table . '.' . $type, $table . '.' . $id, $localKey); + } + + /** + * Define a many-to-many 'through' relationship. + * This is basically hasManyThrough for many-to-many relationships. + * + * @param string $related + * @param string $through + * @param string $firstJoiningTable + * @param string $firstForeignKey + * @param string $firstRelatedKey + * @param string $secondJoiningTable + * @param string $secondForeignKey + * @param string $secondRelatedKey + * @param string $throughRelation + * @param string $relation + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function belongsToManyThrough( + $related, + $through, + $firstJoiningTable = NULL, + $firstForeignKey = NULL, + $firstRelatedKey = NULL, + $secondJoiningTable = NULL, + $secondForeignKey = NULL, + $secondRelatedKey = NULL, + $throughRelation = NULL, + $relation = NULL + ) { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->guessBelongsToManyRelation(); + } + + // Create models for through and related + $through = new $through; + $related = $this->newRelatedInstance($related); + + if (is_null($throughRelation)) { + $throughRelation = $through->getTable(); + } + + // If no table names were provided, we can guess it by concatenating the parent + // and through table names. The two model names are transformed to snake case + // from their default CamelCase also. + if (is_null($firstJoiningTable)) { + $firstJoiningTable = $this->joiningTable($through); + } + + if (is_null($secondJoiningTable)) { + $secondJoiningTable = $through->joiningTable($related); + } + + $firstForeignKey = $firstForeignKey ?: $this->getForeignKey(); + $firstRelatedKey = $firstRelatedKey ?: $through->getForeignKey(); + $secondForeignKey = $secondForeignKey ?: $through->getForeignKey(); + $secondRelatedKey = $secondRelatedKey ?: $related->getForeignKey(); + + // This relationship maps the top model (this) to the through model. + $intermediateRelationship = $this->belongsToMany($through, $firstJoiningTable, $firstForeignKey, $firstRelatedKey, $throughRelation) + ->withPivot($firstForeignKey); + + // Now we set up the relationship with the related model. + $query = new BelongsToManyThrough( + $related->newQuery(), $this, $intermediateRelationship, $secondJoiningTable, $secondForeignKey, $secondRelatedKey, $relation + ); + + return $query; + } + + /** + * Define a unique many-to-many relationship. Similar to a regular many-to-many relationship, but removes duplicate child objects. + * Can also be used to implement ternary relationships. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyUnique + */ + public function belongsToManyUnique($related, $table = NULL, $foreignKey = NULL, $relatedKey = NULL, $relation = NULL) { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->guessBelongsToManyRelation(); + } + + // First, we'll need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we'll make the query + // instances as well as the relationship instances we need for this. + $instance = $this->newRelatedInstance($related); + + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $relatedKey = $relatedKey ?: $instance->getForeignKey(); + + // If no table name was provided, we can guess it by concatenating the two + // models using underscores in alphabetical order. The two model names + // are transformed to snake case from their default CamelCase also. + if (is_null($table)) { + $table = $this->joiningTable($related); + } + + return new BelongsToManyUnique( + $instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation + ); + } + + /** + * Define a unique morphs-to-many relationship. Similar to a regular morphs-to-many relationship, but removes duplicate child objects. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\MorphToManyUnique + */ + public function morphToManyUnique($related, $name, $table = NULL, $foreignKey = NULL, $otherKey = NULL, $inverse = FALSE) { + $caller = $this->getBelongsToManyCaller(); + + // First, we will need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we will make the query + // instances, as well as the relationship instances we need for these. + $foreignKey = $foreignKey ?: $name . '_id'; + + $instance = new $related; + + $otherKey = $otherKey ?: $instance->getForeignKey(); + + // Now we're ready to create a new query builder for this related model and + // the relationship instances for this relation. This relations will set + // appropriate query constraints then entirely manages the hydrations. + $query = $instance->newQuery(); + + $table = $table ?: Str::plural($name); + + return new MorphToManyUnique( + $query, $this, $name, $table, $foreignKey, + $otherKey, $caller, $inverse + ); + } + + /** + * Define a constrained many-to-many relationship. + * This is similar to a regular many-to-many, but constrains the child results to match an additional constraint key in the parent object. + * This has been superseded by the belongsToManyUnique relationship's `withTernary` method since 4.1.7. + * + * @deprecated since 4.1.6 + * @param string $related + * @param string $constraintKey + * @param string $table + * @param string $foreignKey + * @param string $relatedKey + * @param string $relation + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyConstrained + */ + public function belongsToManyConstrained($related, $constraintKey, $table = NULL, $foreignKey = NULL, $relatedKey = NULL, $relation = NULL) { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->guessBelongsToManyRelation(); + } + + // First, we'll need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we'll make the query + // instances as well as the relationship instances we need for this. + $instance = $this->newRelatedInstance($related); + + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $relatedKey = $relatedKey ?: $instance->getForeignKey(); + + // If no table name was provided, we can guess it by concatenating the two + // models using underscores in alphabetical order. The two model names + // are transformed to snake case from their default CamelCase also. + if (is_null($table)) { + $table = $this->joiningTable($related); + } + + return new BelongsToManyConstrained( + $instance->newQuery(), $this, $constraintKey, $table, $foreignKey, $relatedKey, $relation + ); + } + + /** + * Get the relationship name of the belongs to many. + * + * @return string + */ + protected function getBelongsToManyCaller() { + $self = __FUNCTION__; + + $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($key, $trace) use ($self) { + $caller = $trace['function']; + return !in_array($caller, HasRelationships::$manyMethodsExtended) && $caller != $self; + }); + + return !is_null($caller) ? $caller['function'] : NULL; + } +} diff --git a/main/app/sprinkles/core/src/Database/Models/Model.php b/main/app/sprinkles/core/src/Database/Models/Model.php index 28b6be0..38e49fd 100644 --- a/main/app/sprinkles/core/src/Database/Models/Model.php +++ b/main/app/sprinkles/core/src/Database/Models/Model.php @@ -1,133 +1,133 @@ -attributes); - } - - /** - * Determines whether a model exists by checking a unique column, including checking soft-deleted records - * - * @param mixed $value - * @param string $identifier - * @param bool $checkDeleted set to true to include soft-deleted records - * @return \UserFrosting\Sprinkle\Core\Database\Models\Model|null - */ - public static function findUnique($value, $identifier, $checkDeleted = TRUE) { - $query = static::where($identifier, $value); - - if ($checkDeleted) { - $query = $query->withTrashed(); - } - - return $query->first(); - } - - /** - * Determine if an relation exists on the model - even if it is null. - * - * @param string $key - * @return bool - */ - public function relationExists($key) { - return array_key_exists($key, $this->relations); - } - - /** - * Store the object in the DB, creating a new row if one doesn't already exist. - * - * Calls save(), then returns the id of the new record in the database. - * @return int the id of this object. - */ - public function store() { - $this->save(); - - // Store function should always return the id of the object - return $this->id; - } - - /** - * Overrides Laravel's base Model to return our custom query builder object. - * - * @return \UserFrosting\Sprinkles\Core\Database\Builder - */ - protected function newBaseQueryBuilder() { - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - $connection = $this->getConnection(); - - return $classMapper->createInstance( - 'query_builder', - $connection, - $connection->getQueryGrammar(), - $connection->getPostProcessor() - ); - } - - /** - * Get the properties of this object as an associative array. Alias for toArray(). - * - * @deprecated since 4.1.8 There is no point in having this alias. - * @return array - */ - public function export() { - return $this->toArray(); - } - - /** - * For raw array fetching. Must be static, otherwise PHP gets confused about where to find $table. - * - * @deprecated since 4.1.8 setFetchMode is no longer available as of Laravel 5.4. - * @link https://github.com/laravel/framework/issues/17728 - */ - public static function queryBuilder() { - // Set query builder to fetch result sets as associative arrays (instead of creating stdClass objects) - DB::connection()->setFetchMode(\PDO::FETCH_ASSOC); - return DB::table(static::$table); - } -} +attributes); + } + + /** + * Determines whether a model exists by checking a unique column, including checking soft-deleted records + * + * @param mixed $value + * @param string $identifier + * @param bool $checkDeleted set to true to include soft-deleted records + * @return \UserFrosting\Sprinkle\Core\Database\Models\Model|null + */ + public static function findUnique($value, $identifier, $checkDeleted = TRUE) { + $query = static::where($identifier, $value); + + if ($checkDeleted) { + $query = $query->withTrashed(); + } + + return $query->first(); + } + + /** + * Determine if an relation exists on the model - even if it is null. + * + * @param string $key + * @return bool + */ + public function relationExists($key) { + return array_key_exists($key, $this->relations); + } + + /** + * Store the object in the DB, creating a new row if one doesn't already exist. + * + * Calls save(), then returns the id of the new record in the database. + * @return int the id of this object. + */ + public function store() { + $this->save(); + + // Store function should always return the id of the object + return $this->id; + } + + /** + * Overrides Laravel's base Model to return our custom query builder object. + * + * @return \UserFrosting\Sprinkles\Core\Database\Builder + */ + protected function newBaseQueryBuilder() { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + $connection = $this->getConnection(); + + return $classMapper->createInstance( + 'query_builder', + $connection, + $connection->getQueryGrammar(), + $connection->getPostProcessor() + ); + } + + /** + * Get the properties of this object as an associative array. Alias for toArray(). + * + * @deprecated since 4.1.8 There is no point in having this alias. + * @return array + */ + public function export() { + return $this->toArray(); + } + + /** + * For raw array fetching. Must be static, otherwise PHP gets confused about where to find $table. + * + * @deprecated since 4.1.8 setFetchMode is no longer available as of Laravel 5.4. + * @link https://github.com/laravel/framework/issues/17728 + */ + public static function queryBuilder() { + // Set query builder to fetch result sets as associative arrays (instead of creating stdClass objects) + DB::connection()->setFetchMode(\PDO::FETCH_ASSOC); + return DB::table(static::$table); + } +} diff --git a/main/app/sprinkles/core/src/Database/Models/Throttle.php b/main/app/sprinkles/core/src/Database/Models/Throttle.php index 82d2c87..5810e0d 100644 --- a/main/app/sprinkles/core/src/Database/Models/Throttle.php +++ b/main/app/sprinkles/core/src/Database/Models/Throttle.php @@ -1,37 +1,37 @@ -constraintKey = $constraintKey; - parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName); - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - * @return void - */ - public function addEagerConstraints(array $models) { - // To make the query more efficient, we only bother querying related models if their pivot key value - // matches the pivot key value of one of the parent models. - $pivotKeys = $this->getPivotKeys($models, $this->constraintKey); - $this->query->whereIn($this->getQualifiedForeignKeyName(), $this->getKeys($models)) - ->whereIn($this->constraintKey, $pivotKeys); - } - - /** - * Gets a list of unique pivot key values from an array of models. - */ - protected function getPivotKeys(array $models, $pivotKey) { - $pivotKeys = []; - foreach ($models as $model) { - $pivotKeys[] = $model->getRelation('pivot')->{$pivotKey}; - } - return array_unique($pivotKeys); - } - - /** - * Match the eagerly loaded results to their parents, constraining the results by matching the values of $constraintKey - * in the parent object to the child objects. - * - * @link Called in https://github.com/laravel/framework/blob/2f4135d8db5ded851d1f4f611124c53b768a3c08/src/Illuminate/Database/Eloquent/Builder.php - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * @return array - */ - public function match(array $models, Collection $results, $relation) { - $dictionary = $this->buildDictionary($results); - - // Once we have an array dictionary of child objects we can easily match the - // children back to their parent using the dictionary and the keys on the - // the parent models. Then we will return the hydrated models back out. - foreach ($models as $model) { - $pivotValue = $model->getRelation('pivot')->{$this->constraintKey}; - if (isset($dictionary[$key = $model->getKey()])) { - // Only match children if their pivot key value matches that of the parent model - $items = $this->findMatchingPivots($dictionary[$key], $pivotValue); - $model->setRelation( - $relation, $this->related->newCollection($items) - ); - } - } - - return $models; - } - - /** - * Filter an array of models, only taking models whose $constraintKey value matches $pivotValue. - * - * @param mixed $pivotValue - * @return array - */ - protected function findMatchingPivots($items, $pivotValue) { - $result = []; - foreach ($items as $item) { - if ($item->getRelation('pivot')->{$this->constraintKey} == $pivotValue) { - $result[] = $item; - } - } - return $result; - } -} +constraintKey = $constraintKey; + parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) { + // To make the query more efficient, we only bother querying related models if their pivot key value + // matches the pivot key value of one of the parent models. + $pivotKeys = $this->getPivotKeys($models, $this->constraintKey); + $this->query->whereIn($this->getQualifiedForeignKeyName(), $this->getKeys($models)) + ->whereIn($this->constraintKey, $pivotKeys); + } + + /** + * Gets a list of unique pivot key values from an array of models. + */ + protected function getPivotKeys(array $models, $pivotKey) { + $pivotKeys = []; + foreach ($models as $model) { + $pivotKeys[] = $model->getRelation('pivot')->{$pivotKey}; + } + return array_unique($pivotKeys); + } + + /** + * Match the eagerly loaded results to their parents, constraining the results by matching the values of $constraintKey + * in the parent object to the child objects. + * + * @link Called in https://github.com/laravel/framework/blob/2f4135d8db5ded851d1f4f611124c53b768a3c08/src/Illuminate/Database/Eloquent/Builder.php + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) { + $dictionary = $this->buildDictionary($results); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + $pivotValue = $model->getRelation('pivot')->{$this->constraintKey}; + if (isset($dictionary[$key = $model->getKey()])) { + // Only match children if their pivot key value matches that of the parent model + $items = $this->findMatchingPivots($dictionary[$key], $pivotValue); + $model->setRelation( + $relation, $this->related->newCollection($items) + ); + } + } + + return $models; + } + + /** + * Filter an array of models, only taking models whose $constraintKey value matches $pivotValue. + * + * @param mixed $pivotValue + * @return array + */ + protected function findMatchingPivots($items, $pivotValue) { + $result = []; + foreach ($items as $item) { + if ($item->getRelation('pivot')->{$this->constraintKey} == $pivotValue) { + $result[] = $item; + } + } + return $result; + } +} diff --git a/main/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php b/main/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php index 67304be..24aa4cf 100644 --- a/main/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php +++ b/main/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php @@ -1,223 +1,223 @@ -intermediateRelation = $intermediateRelation; - - parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName); - } - - /** - * Use the intermediate relationship to determine the "parent" pivot key name - * - * This is a crazy roundabout way to get the name of the intermediate relation's foreign key. - * It would be better if BelongsToMany had a simple accessor for its foreign key. - * @return string - */ - public function getParentKeyName() { - return $this->intermediateRelation->newExistingPivot()->getForeignKey(); - } - - /** - * Get the key for comparing against the parent key in "has" query. - * - * @see \Illuminate\Database\Eloquent\Relations\BelongsToMany - * @return string - */ - public function getExistenceCompareKey() { - return $this->intermediateRelation->getQualifiedForeignKeyName(); - } - - /** - * Add a "via" query to load the intermediate models through which the child models are related. - * - * @param string $viaRelationName - * @param callable $viaCallback - * @return $this - */ - public function withVia($viaRelationName = NULL, $viaCallback = NULL) { - $this->tertiaryRelated = $this->intermediateRelation->getRelated(); - - // Set tertiary key and related model - $this->tertiaryKey = $this->foreignKey; - - $this->tertiaryRelationName = is_null($viaRelationName) ? $this->intermediateRelation->getRelationName() . '_via' : $viaRelationName; - - $this->tertiaryCallback = is_null($viaCallback) - ? function () { - // - } - : $viaCallback; - - return $this; - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - * @return void - */ - public function addEagerConstraints(array $models) { - // Constraint to only load models where the intermediate relation's foreign key matches the parent model - $intermediateForeignKeyName = $this->intermediateRelation->getQualifiedForeignKeyName(); - - return $this->query->whereIn($intermediateForeignKeyName, $this->getKeys($models)); - } - - /** - * Set the where clause for the relation query. - * - * @return $this - */ - protected function addWhereConstraints() { - $parentKeyName = $this->getParentKeyName(); - - $this->query->where( - $parentKeyName, '=', $this->parent->getKey() - ); - - return $this; - } - - /** - * Match the eagerly loaded results to their parents - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * @return array - */ - public function match(array $models, Collection $results, $relation) { - // Build dictionary of parent (e.g. user) to related (e.g. permission) models - list($dictionary, $nestedViaDictionary) = $this->buildDictionary($results, $this->getParentKeyName()); - - // Once we have an array dictionary of child objects we can easily match the - // children back to their parent using the dictionary and the keys on the - // the parent models. Then we will return the hydrated models back out. - foreach ($models as $model) { - if (isset($dictionary[$key = $model->getKey()])) { - /** @var array */ - $items = $dictionary[$key]; - - // Eliminate any duplicates - $items = $this->related->newCollection($items)->unique(); - - // If set, match up the via models to the models in the related collection - if (!is_null($nestedViaDictionary)) { - $this->matchTertiaryModels($nestedViaDictionary[$key], $items); - } - - // Remove the tertiary pivot key from the condensed models - foreach ($items as $relatedModel) { - unset($relatedModel->pivot->{$this->foreignKey}); - } - - $model->setRelation( - $relation, $items - ); - } - } - - return $models; - } - - /** - * Unset tertiary pivots on a collection or array of models. - * - * @param \Illuminate\Database\Eloquent\Collection $models - * @return void - */ - protected function unsetTertiaryPivots(Collection $models) { - foreach ($models as $model) { - unset($model->pivot->{$this->foreignKey}); - } - } - - /** - * Set the join clause for the relation query. - * - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return $this - */ - protected function performJoin($query = NULL) { - $query = $query ?: $this->query; - - parent::performJoin($query); - - // We need to join to the intermediate table on the related model's primary - // key column with the intermediate table's foreign key for the related - // model instance. Then we can set the "where" for the parent models. - $intermediateTable = $this->intermediateRelation->getTable(); - - $key = $this->intermediateRelation->getQualifiedRelatedKeyName(); - - $query->join($intermediateTable, $key, '=', $this->getQualifiedForeignKeyName()); - - return $this; - } - - /** - * Get the pivot columns for the relation. - * - * "pivot_" is prefixed to each column for easy removal later. - * - * @return array - */ - protected function aliasedPivotColumns() { - $defaults = [$this->foreignKey, $this->relatedKey]; - $aliasedPivotColumns = collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { - return $this->table . '.' . $column . ' as pivot_' . $column; - }); - - $parentKeyName = $this->getParentKeyName(); - - // Add pivot column for the intermediate relation - $aliasedPivotColumns[] = "{$this->intermediateRelation->getQualifiedForeignKeyName()} as pivot_$parentKeyName"; - - return $aliasedPivotColumns->unique()->all(); - } -} +intermediateRelation = $intermediateRelation; + + parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName); + } + + /** + * Use the intermediate relationship to determine the "parent" pivot key name + * + * This is a crazy roundabout way to get the name of the intermediate relation's foreign key. + * It would be better if BelongsToMany had a simple accessor for its foreign key. + * @return string + */ + public function getParentKeyName() { + return $this->intermediateRelation->newExistingPivot()->getForeignKey(); + } + + /** + * Get the key for comparing against the parent key in "has" query. + * + * @see \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return string + */ + public function getExistenceCompareKey() { + return $this->intermediateRelation->getQualifiedForeignKeyName(); + } + + /** + * Add a "via" query to load the intermediate models through which the child models are related. + * + * @param string $viaRelationName + * @param callable $viaCallback + * @return $this + */ + public function withVia($viaRelationName = NULL, $viaCallback = NULL) { + $this->tertiaryRelated = $this->intermediateRelation->getRelated(); + + // Set tertiary key and related model + $this->tertiaryKey = $this->foreignKey; + + $this->tertiaryRelationName = is_null($viaRelationName) ? $this->intermediateRelation->getRelationName() . '_via' : $viaRelationName; + + $this->tertiaryCallback = is_null($viaCallback) + ? function () { + // + } + : $viaCallback; + + return $this; + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) { + // Constraint to only load models where the intermediate relation's foreign key matches the parent model + $intermediateForeignKeyName = $this->intermediateRelation->getQualifiedForeignKeyName(); + + return $this->query->whereIn($intermediateForeignKeyName, $this->getKeys($models)); + } + + /** + * Set the where clause for the relation query. + * + * @return $this + */ + protected function addWhereConstraints() { + $parentKeyName = $this->getParentKeyName(); + + $this->query->where( + $parentKeyName, '=', $this->parent->getKey() + ); + + return $this; + } + + /** + * Match the eagerly loaded results to their parents + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) { + // Build dictionary of parent (e.g. user) to related (e.g. permission) models + list($dictionary, $nestedViaDictionary) = $this->buildDictionary($results, $this->getParentKeyName()); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + /** @var array */ + $items = $dictionary[$key]; + + // Eliminate any duplicates + $items = $this->related->newCollection($items)->unique(); + + // If set, match up the via models to the models in the related collection + if (!is_null($nestedViaDictionary)) { + $this->matchTertiaryModels($nestedViaDictionary[$key], $items); + } + + // Remove the tertiary pivot key from the condensed models + foreach ($items as $relatedModel) { + unset($relatedModel->pivot->{$this->foreignKey}); + } + + $model->setRelation( + $relation, $items + ); + } + } + + return $models; + } + + /** + * Unset tertiary pivots on a collection or array of models. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @return void + */ + protected function unsetTertiaryPivots(Collection $models) { + foreach ($models as $model) { + unset($model->pivot->{$this->foreignKey}); + } + } + + /** + * Set the join clause for the relation query. + * + * @param \Illuminate\Database\Eloquent\Builder|null $query + * @return $this + */ + protected function performJoin($query = NULL) { + $query = $query ?: $this->query; + + parent::performJoin($query); + + // We need to join to the intermediate table on the related model's primary + // key column with the intermediate table's foreign key for the related + // model instance. Then we can set the "where" for the parent models. + $intermediateTable = $this->intermediateRelation->getTable(); + + $key = $this->intermediateRelation->getQualifiedRelatedKeyName(); + + $query->join($intermediateTable, $key, '=', $this->getQualifiedForeignKeyName()); + + return $this; + } + + /** + * Get the pivot columns for the relation. + * + * "pivot_" is prefixed to each column for easy removal later. + * + * @return array + */ + protected function aliasedPivotColumns() { + $defaults = [$this->foreignKey, $this->relatedKey]; + $aliasedPivotColumns = collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { + return $this->table . '.' . $column . ' as pivot_' . $column; + }); + + $parentKeyName = $this->getParentKeyName(); + + // Add pivot column for the intermediate relation + $aliasedPivotColumns[] = "{$this->intermediateRelation->getQualifiedForeignKeyName()} as pivot_$parentKeyName"; + + return $aliasedPivotColumns->unique()->all(); + } +} diff --git a/main/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php b/main/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php index d5d473d..1bde954 100644 --- a/main/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php +++ b/main/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php @@ -1,23 +1,23 @@ - [], 'deleted' => [], 'updated' => [], - ]; - - if (is_null($relatedKeyName)) { - $relatedKeyName = $this->related->getKeyName(); - } - - // First we need to attach any of the associated models that are not currently - // in the child entity table. We'll spin through the given IDs, checking to see - // if they exist in the array of current ones, and if not we will insert. - $current = $this->newQuery()->pluck( - $relatedKeyName - )->all(); - - // Separate the submitted data into "update" and "new" - $updateRows = []; - $newRows = []; - foreach ($data as $row) { - // We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and - // match a related row in the database. - if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) { - $id = $row[$relatedKeyName]; - $updateRows[$id] = $row; - } else { - $newRows[] = $row; - } - } - - // Next, we'll determine the rows in the database that aren't in the "update" list. - // These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id'). - $updateIds = array_keys($updateRows); - $deleteIds = []; - foreach ($current as $currentId) { - if (!in_array($currentId, $updateIds)) { - $deleteIds[] = $currentId; - } - } - - // Delete any non-matching rows - if ($deleting && count($deleteIds) > 0) { - // Remove global scopes to avoid ambiguous keys - $this->getRelated() - ->withoutGlobalScopes() - ->whereIn($relatedKeyName, $deleteIds) - ->delete(); - - $changes['deleted'] = $this->castKeys($deleteIds); - } - - // Update the updatable rows - foreach ($updateRows as $id => $row) { - // Remove global scopes to avoid ambiguous keys - $this->getRelated() - ->withoutGlobalScopes() - ->where($relatedKeyName, $id) - ->update($row); - } - - $changes['updated'] = $this->castKeys($updateIds); - - // Insert the new rows - $newIds = []; - foreach ($newRows as $row) { - if ($forceCreate) { - $newModel = $this->forceCreate($row); - } else { - $newModel = $this->create($row); - } - $newIds[] = $newModel->$relatedKeyName; - } - - $changes['created'] = $this->castKeys($newIds); - - return $changes; - } - - - /** - * Cast the given keys to integers if they are numeric and string otherwise. - * - * @param array $keys - * @return array - */ - protected function castKeys(array $keys) { - return (array)array_map(function ($v) { - return $this->castKey($v); - }, $keys); - } - - /** - * Cast the given key to an integer if it is numeric. - * - * @param mixed $key - * @return mixed - */ - protected function castKey($key) { - return is_numeric($key) ? (int)$key : (string)$key; - } -} + [], 'deleted' => [], 'updated' => [], + ]; + + if (is_null($relatedKeyName)) { + $relatedKeyName = $this->related->getKeyName(); + } + + // First we need to attach any of the associated models that are not currently + // in the child entity table. We'll spin through the given IDs, checking to see + // if they exist in the array of current ones, and if not we will insert. + $current = $this->newQuery()->pluck( + $relatedKeyName + )->all(); + + // Separate the submitted data into "update" and "new" + $updateRows = []; + $newRows = []; + foreach ($data as $row) { + // We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and + // match a related row in the database. + if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) { + $id = $row[$relatedKeyName]; + $updateRows[$id] = $row; + } else { + $newRows[] = $row; + } + } + + // Next, we'll determine the rows in the database that aren't in the "update" list. + // These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id'). + $updateIds = array_keys($updateRows); + $deleteIds = []; + foreach ($current as $currentId) { + if (!in_array($currentId, $updateIds)) { + $deleteIds[] = $currentId; + } + } + + // Delete any non-matching rows + if ($deleting && count($deleteIds) > 0) { + // Remove global scopes to avoid ambiguous keys + $this->getRelated() + ->withoutGlobalScopes() + ->whereIn($relatedKeyName, $deleteIds) + ->delete(); + + $changes['deleted'] = $this->castKeys($deleteIds); + } + + // Update the updatable rows + foreach ($updateRows as $id => $row) { + // Remove global scopes to avoid ambiguous keys + $this->getRelated() + ->withoutGlobalScopes() + ->where($relatedKeyName, $id) + ->update($row); + } + + $changes['updated'] = $this->castKeys($updateIds); + + // Insert the new rows + $newIds = []; + foreach ($newRows as $row) { + if ($forceCreate) { + $newModel = $this->forceCreate($row); + } else { + $newModel = $this->create($row); + } + $newIds[] = $newModel->$relatedKeyName; + } + + $changes['created'] = $this->castKeys($newIds); + + return $changes; + } + + + /** + * Cast the given keys to integers if they are numeric and string otherwise. + * + * @param array $keys + * @return array + */ + protected function castKeys(array $keys) { + return (array)array_map(function ($v) { + return $this->castKey($v); + }, $keys); + } + + /** + * Cast the given key to an integer if it is numeric. + * + * @param mixed $key + * @return mixed + */ + protected function castKey($key) { + return is_numeric($key) ? (int)$key : (string)$key; + } +} diff --git a/main/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php b/main/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php index 3a321e4..deb2673 100644 --- a/main/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php +++ b/main/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php @@ -1,543 +1,543 @@ -offset($value); - } - - /** - * Set the "offset" value of the query. - * - * Implement for 'unionOffset' as well? (By checking the value of $this->query->getQuery()->unions) - * @see \Illuminate\Database\Query\Builder - * @param int $value - * @return $this - */ - public function offset($value) { - $this->offset = max(0, $value); - - return $this; - } - - /** - * Alias to set the "limit" value of the query. - * - * @param int $value - * @return $this - */ - public function take($value) { - return $this->limit($value); - } - - /** - * Set the "limit" value of the query. - * - * Implement for 'unionLimit' as well? (By checking the value of $this->query->getQuery()->unions) - * @see \Illuminate\Database\Query\Builder - * @param int $value - * @return $this - */ - public function limit($value) { - if ($value >= 0) { - $this->limit = $value; - } - - return $this; - } - - /** - * Set the limit on the number of intermediate models to load. - * - * @deprecated since 4.1.7 - * @param int $value - * @return $this - */ - public function withLimit($value) { - return $this->limit($value); - } - - /** - * Set the offset when loading the intermediate models. - * - * @deprecated since 4.1.7 - * @param int $value - * @return $this - */ - public function withOffset($value) { - return $this->offset($value); - } - - /** - * Add a query to load the nested tertiary models for this relationship. - * - * @param \Illuminate\Database\Eloquent\Model $tertiaryRelated - * @param string $tertiaryRelationName - * @param string $tertiaryKey - * @param callable $tertiaryCallback - * @return $this - */ - public function withTertiary($tertiaryRelated, $tertiaryRelationName = NULL, $tertiaryKey = NULL, $tertiaryCallback = NULL) { - $this->tertiaryRelated = new $tertiaryRelated; - - // Try to guess the tertiary related key from the tertiaryRelated model. - $this->tertiaryKey = $tertiaryKey ?: $this->tertiaryRelated->getForeignKey(); - - // Also add the tertiary key as a pivot - $this->withPivot($this->tertiaryKey); - - $this->tertiaryRelationName = is_null($tertiaryRelationName) ? $this->tertiaryRelated->getTable() : $tertiaryRelationName; - - $this->tertiaryCallback = is_null($tertiaryCallback) - ? function () { - // - } - : $tertiaryCallback; - - return $this; - } - - /** - * Return the count of child models for this relationship. - * - * @see http://stackoverflow.com/a/29728129/2970321 - * @return int - */ - public function count() { - $constrainedBuilder = clone $this->query; - - $constrainedBuilder = $constrainedBuilder->distinct(); - - return $constrainedBuilder->count($this->relatedKey); - } - - /** - * Add the constraints for a relationship count query. - * - * @see \Illuminate\Database\Eloquent\Relations\Relation - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getRelationExistenceCountQuery(Builder $query, Builder $parentQuery) { - return $this->getRelationExistenceQuery( - $query, $parentQuery, new Expression("count(distinct {$this->relatedKey})") - ); - } - - /** - * Match the eagerly loaded results to their parents - * - * @param array $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * @return array - */ - public function match(array $models, Collection $results, $relation) { - // Build dictionary of parent (e.g. user) to related (e.g. permission) models - list($dictionary, $nestedTertiaryDictionary) = $this->buildDictionary($results, $this->foreignKey); - - // Once we have an array dictionary of child objects we can easily match the - // children back to their parent using the dictionary and the keys on the - // the parent models. Then we will return the hydrated models back out. - foreach ($models as $model) { - if (isset($dictionary[$key = $model->getKey()])) { - /** @var array */ - $items = $dictionary[$key]; - - // Eliminate any duplicates - $items = $this->related->newCollection($items)->unique(); - - // If set, match up the tertiary models to the models in the related collection - if (!is_null($nestedTertiaryDictionary)) { - $this->matchTertiaryModels($nestedTertiaryDictionary[$key], $items); - } - - $model->setRelation( - $relation, $items - ); - } - } - - return $models; - } - - /** - * Execute the query as a "select" statement, getting all requested models - * and matching up any tertiary models. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function get($columns = ['*']) { - // Get models and condense the result set - $models = $this->getModels($columns, TRUE); - - // Remove the tertiary pivot key from the condensed models - $this->unsetTertiaryPivots($models); - - return $models; - } - - /** - * If we are applying either a limit or offset, we'll first determine a limited/offset list of model ids - * to select from in the final query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param int $limit - * @param int $offset - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getPaginatedQuery(Builder $query, $limit = NULL, $offset = NULL) { - $constrainedBuilder = clone $query; - - // Since some unique models will be represented by more than one row in the database, - // we cannot apply limit/offset directly to the query. If we did that, we'd miss - // some of the records that are to be coalesced into the final set of models. - // Instead, we perform an additional query with grouping and limit/offset to determine - // the desired set of unique model _ids_, and then constrain our final query - // to these models with a whereIn clause. - $relatedKeyName = $this->related->getQualifiedKeyName(); - - // Apply an additional scope to override any selected columns in other global scopes - $uniqueIdScope = function ($subQuery) use ($relatedKeyName) { - $subQuery->select($relatedKeyName) - ->groupBy($relatedKeyName); - }; - - $identifier = spl_object_hash($uniqueIdScope); - - $constrainedBuilder->withGlobalScope($identifier, $uniqueIdScope); - - if ($limit) { - $constrainedBuilder->limit($limit); - } - - if ($offset) { - $constrainedBuilder->offset($offset); - } - - $primaryKeyName = $this->getParent()->getKeyName(); - $modelIds = $constrainedBuilder->get()->pluck($primaryKeyName)->toArray(); - - // Modify the unconstrained query to limit to these models - return $query->whereIn($relatedKeyName, $modelIds); - } - - /** - * Get the full join results for this query, overriding the default getEager() method. - * The default getEager() method would normally just call get() on this relationship. - * This is not what we want here though, because our get() method removes records before - * `match` has a chance to build out the substructures. - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getEager() { - return $this->getModels(['*'], FALSE); - } - - /** - * Get the hydrated models and eager load their relations, optionally - * condensing the set of models before performing the eager loads. - * - * @param array $columns - * @param bool $condenseModels - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getModels($columns = ['*'], $condenseModels = TRUE) { - // First we'll add the proper select columns onto the query so it is run with - // the proper columns. Then, we will get the results and hydrate out pivot - // models with the result of those columns as a separate model relation. - $columns = $this->query->getQuery()->columns ? [] : $columns; - - // Add any necessary pagination on the related models - if ($this->limit || $this->offset) { - $this->getPaginatedQuery($this->query, $this->limit, $this->offset); - } - - // Apply scopes to the Eloquent\Builder instance. - $builder = $this->query->applyScopes(); - - $builder = $builder->addSelect( - $this->shouldSelect($columns) - ); - - $models = $builder->getModels(); - - // Hydrate the pivot models so we can load the via models - $this->hydratePivotRelation($models); - - if ($condenseModels) { - $models = $this->condenseModels($models); - } - - // If we actually found models we will also eager load any relationships that - // have been specified as needing to be eager loaded. This will solve the - // n + 1 query problem for the developer and also increase performance. - if (count($models) > 0) { - $models = $builder->eagerLoadRelations($models); - } - - return $this->related->newCollection($models); - } - - /** - * Condense the raw join query results into a set of unique models. - * - * Before doing this, we may optionally find any tertiary models that should be - * set as sub-relations on these models. - * @param array $models - * @return array - */ - protected function condenseModels(array $models) { - // Build dictionary of tertiary models, if `withTertiary` was called - $dictionary = NULL; - if ($this->tertiaryRelated) { - $dictionary = $this->buildTertiaryDictionary($models); - } - - // Remove duplicate models from collection - $models = $this->related->newCollection($models)->unique(); - - // If using withTertiary, use the dictionary to set the tertiary relation on each model. - if (!is_null($dictionary)) { - $this->matchTertiaryModels($dictionary, $models); - } - - return $models->all(); - } - - /** - * Build dictionary of related models keyed by the top-level "parent" id. - * If there is a tertiary query set as well, then also build a two-level dictionary - * that maps parent ids to arrays of related ids, which in turn map to arrays - * of tertiary models corresponding to each relationship. - * - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $parentKey - * @return array - */ - protected function buildDictionary(Collection $results, $parentKey = NULL) { - // First we will build a dictionary of child models keyed by the "parent key" (foreign key - // of the intermediate relation) so that we will easily and quickly match them to their - // parents without having a possibly slow inner loops for every models. - $dictionary = []; - - //Example nested dictionary: - //[ - // // User 1 - // '1' => [ - // // Permission 3 - // '3' => [ - // Role1, - // Role2 - // ], - // ... - // ], - // ... - //] - $nestedTertiaryDictionary = NULL; - $tertiaryModels = NULL; - - if ($this->tertiaryRelationName) { - // Get all tertiary models from the result set matching any of the parent models. - $tertiaryModels = $this->getTertiaryModels($results->all()); - } - - foreach ($results as $result) { - $parentKeyValue = $result->pivot->$parentKey; - - // Set the related model in the main dictionary. - // Note that this can end up adding duplicate models. It's cheaper to simply - // go back and remove the duplicates when we actually use the dictionary, - // rather than check for duplicates on each insert. - $dictionary[$parentKeyValue][] = $result; - - // If we're loading tertiary models, then set the keys in the nested dictionary as well. - if (!is_null($tertiaryModels)) { - $tertiaryKeyValue = $result->pivot->{$this->tertiaryKey}; - - if (!is_null($tertiaryKeyValue)) { - $tertiaryModel = clone $tertiaryModels[$tertiaryKeyValue]; - - // We also transfer the pivot relation at this point, since we have already coalesced - // any tertiary models into the nested dictionary. - $this->transferPivotsToTertiary($result, $tertiaryModel); - - $nestedTertiaryDictionary[$parentKeyValue][$result->getKey()][] = $tertiaryModel; - } - } - } - - return [$dictionary, $nestedTertiaryDictionary]; - } - - /** - * Build dictionary of tertiary models keyed by the corresponding related model keys. - * - * @param array $models - * @return array - */ - protected function buildTertiaryDictionary(array $models) { - $dictionary = []; - - // Find the related tertiary entities (e.g. tasks) for all related models (e.g. locations) - $tertiaryModels = $this->getTertiaryModels($models); - - // Now for each related model (e.g. location), we will build out a dictionary of their tertiary models (e.g. tasks) - foreach ($models as $model) { - $tertiaryKeyValue = $model->pivot->{$this->tertiaryKey}; - - $tertiaryModel = clone $tertiaryModels[$tertiaryKeyValue]; - - $this->transferPivotsToTertiary($model, $tertiaryModel); - - $dictionary[$model->getKey()][] = $tertiaryModel; - } - - return $dictionary; - } - - protected function transferPivotsToTertiary($model, $tertiaryModel) { - $pivotAttributes = []; - foreach ($this->pivotColumns as $column) { - $pivotAttributes[$column] = $model->pivot->$column; - unset($model->pivot->$column); - } - // Copy the related key pivot as well, but don't unset on the related model - $pivotAttributes[$this->relatedKey] = $model->pivot->{$this->relatedKey}; - - // Set the tertiary key pivot as well - $pivotAttributes[$this->tertiaryKey] = $tertiaryModel->getKey(); - - $pivot = $this->newExistingPivot($pivotAttributes); - $tertiaryModel->setRelation('pivot', $pivot); - } - - /** - * Get the tertiary models for the relationship. - * - * @param array $models - * @return \Illuminate\Database\Eloquent\Collection - */ - protected function getTertiaryModels(array $models) { - $tertiaryClass = $this->tertiaryRelated; - - $keys = []; - foreach ($models as $model) { - $keys[] = $model->getRelation('pivot')->{$this->tertiaryKey}; - } - $keys = array_unique($keys); - - $query = $tertiaryClass->whereIn($tertiaryClass->getQualifiedKeyName(), $keys); - - // Add any additional constraints/eager loads to the tertiary query - $callback = $this->tertiaryCallback; - $callback($query); - - $tertiaryModels = $query - ->get() - ->keyBy($tertiaryClass->getKeyName()); - - return $tertiaryModels; - } - - /** - * Match a collection of child models into a collection of parent models using a dictionary. - * - * @param array $dictionary - * @param \Illuminate\Database\Eloquent\Collection $results - * @return void - */ - protected function matchTertiaryModels(array $dictionary, Collection $results) { - // Now go through and set the tertiary relation on each child model - foreach ($results as $model) { - if (isset($dictionary[$key = $model->getKey()])) { - $tertiaryModels = $dictionary[$key]; - - $model->setRelation( - $this->tertiaryRelationName, $this->tertiaryRelated->newCollection($tertiaryModels) - ); - } - } - } - - /** - * Unset tertiary pivots on a collection or array of models. - * - * @param \Illuminate\Database\Eloquent\Collection $models - * @return void - */ - protected function unsetTertiaryPivots(Collection $models) { - foreach ($models as $model) { - foreach ($this->pivotColumns as $column) { - unset($model->pivot->$column); - } - } - } -} +offset($value); + } + + /** + * Set the "offset" value of the query. + * + * Implement for 'unionOffset' as well? (By checking the value of $this->query->getQuery()->unions) + * @see \Illuminate\Database\Query\Builder + * @param int $value + * @return $this + */ + public function offset($value) { + $this->offset = max(0, $value); + + return $this; + } + + /** + * Alias to set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function take($value) { + return $this->limit($value); + } + + /** + * Set the "limit" value of the query. + * + * Implement for 'unionLimit' as well? (By checking the value of $this->query->getQuery()->unions) + * @see \Illuminate\Database\Query\Builder + * @param int $value + * @return $this + */ + public function limit($value) { + if ($value >= 0) { + $this->limit = $value; + } + + return $this; + } + + /** + * Set the limit on the number of intermediate models to load. + * + * @deprecated since 4.1.7 + * @param int $value + * @return $this + */ + public function withLimit($value) { + return $this->limit($value); + } + + /** + * Set the offset when loading the intermediate models. + * + * @deprecated since 4.1.7 + * @param int $value + * @return $this + */ + public function withOffset($value) { + return $this->offset($value); + } + + /** + * Add a query to load the nested tertiary models for this relationship. + * + * @param \Illuminate\Database\Eloquent\Model $tertiaryRelated + * @param string $tertiaryRelationName + * @param string $tertiaryKey + * @param callable $tertiaryCallback + * @return $this + */ + public function withTertiary($tertiaryRelated, $tertiaryRelationName = NULL, $tertiaryKey = NULL, $tertiaryCallback = NULL) { + $this->tertiaryRelated = new $tertiaryRelated; + + // Try to guess the tertiary related key from the tertiaryRelated model. + $this->tertiaryKey = $tertiaryKey ?: $this->tertiaryRelated->getForeignKey(); + + // Also add the tertiary key as a pivot + $this->withPivot($this->tertiaryKey); + + $this->tertiaryRelationName = is_null($tertiaryRelationName) ? $this->tertiaryRelated->getTable() : $tertiaryRelationName; + + $this->tertiaryCallback = is_null($tertiaryCallback) + ? function () { + // + } + : $tertiaryCallback; + + return $this; + } + + /** + * Return the count of child models for this relationship. + * + * @see http://stackoverflow.com/a/29728129/2970321 + * @return int + */ + public function count() { + $constrainedBuilder = clone $this->query; + + $constrainedBuilder = $constrainedBuilder->distinct(); + + return $constrainedBuilder->count($this->relatedKey); + } + + /** + * Add the constraints for a relationship count query. + * + * @see \Illuminate\Database\Eloquent\Relations\Relation + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceCountQuery(Builder $query, Builder $parentQuery) { + return $this->getRelationExistenceQuery( + $query, $parentQuery, new Expression("count(distinct {$this->relatedKey})") + ); + } + + /** + * Match the eagerly loaded results to their parents + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) { + // Build dictionary of parent (e.g. user) to related (e.g. permission) models + list($dictionary, $nestedTertiaryDictionary) = $this->buildDictionary($results, $this->foreignKey); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + /** @var array */ + $items = $dictionary[$key]; + + // Eliminate any duplicates + $items = $this->related->newCollection($items)->unique(); + + // If set, match up the tertiary models to the models in the related collection + if (!is_null($nestedTertiaryDictionary)) { + $this->matchTertiaryModels($nestedTertiaryDictionary[$key], $items); + } + + $model->setRelation( + $relation, $items + ); + } + } + + return $models; + } + + /** + * Execute the query as a "select" statement, getting all requested models + * and matching up any tertiary models. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function get($columns = ['*']) { + // Get models and condense the result set + $models = $this->getModels($columns, TRUE); + + // Remove the tertiary pivot key from the condensed models + $this->unsetTertiaryPivots($models); + + return $models; + } + + /** + * If we are applying either a limit or offset, we'll first determine a limited/offset list of model ids + * to select from in the final query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $limit + * @param int $offset + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getPaginatedQuery(Builder $query, $limit = NULL, $offset = NULL) { + $constrainedBuilder = clone $query; + + // Since some unique models will be represented by more than one row in the database, + // we cannot apply limit/offset directly to the query. If we did that, we'd miss + // some of the records that are to be coalesced into the final set of models. + // Instead, we perform an additional query with grouping and limit/offset to determine + // the desired set of unique model _ids_, and then constrain our final query + // to these models with a whereIn clause. + $relatedKeyName = $this->related->getQualifiedKeyName(); + + // Apply an additional scope to override any selected columns in other global scopes + $uniqueIdScope = function ($subQuery) use ($relatedKeyName) { + $subQuery->select($relatedKeyName) + ->groupBy($relatedKeyName); + }; + + $identifier = spl_object_hash($uniqueIdScope); + + $constrainedBuilder->withGlobalScope($identifier, $uniqueIdScope); + + if ($limit) { + $constrainedBuilder->limit($limit); + } + + if ($offset) { + $constrainedBuilder->offset($offset); + } + + $primaryKeyName = $this->getParent()->getKeyName(); + $modelIds = $constrainedBuilder->get()->pluck($primaryKeyName)->toArray(); + + // Modify the unconstrained query to limit to these models + return $query->whereIn($relatedKeyName, $modelIds); + } + + /** + * Get the full join results for this query, overriding the default getEager() method. + * The default getEager() method would normally just call get() on this relationship. + * This is not what we want here though, because our get() method removes records before + * `match` has a chance to build out the substructures. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getEager() { + return $this->getModels(['*'], FALSE); + } + + /** + * Get the hydrated models and eager load their relations, optionally + * condensing the set of models before performing the eager loads. + * + * @param array $columns + * @param bool $condenseModels + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getModels($columns = ['*'], $condenseModels = TRUE) { + // First we'll add the proper select columns onto the query so it is run with + // the proper columns. Then, we will get the results and hydrate out pivot + // models with the result of those columns as a separate model relation. + $columns = $this->query->getQuery()->columns ? [] : $columns; + + // Add any necessary pagination on the related models + if ($this->limit || $this->offset) { + $this->getPaginatedQuery($this->query, $this->limit, $this->offset); + } + + // Apply scopes to the Eloquent\Builder instance. + $builder = $this->query->applyScopes(); + + $builder = $builder->addSelect( + $this->shouldSelect($columns) + ); + + $models = $builder->getModels(); + + // Hydrate the pivot models so we can load the via models + $this->hydratePivotRelation($models); + + if ($condenseModels) { + $models = $this->condenseModels($models); + } + + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded. This will solve the + // n + 1 query problem for the developer and also increase performance. + if (count($models) > 0) { + $models = $builder->eagerLoadRelations($models); + } + + return $this->related->newCollection($models); + } + + /** + * Condense the raw join query results into a set of unique models. + * + * Before doing this, we may optionally find any tertiary models that should be + * set as sub-relations on these models. + * @param array $models + * @return array + */ + protected function condenseModels(array $models) { + // Build dictionary of tertiary models, if `withTertiary` was called + $dictionary = NULL; + if ($this->tertiaryRelated) { + $dictionary = $this->buildTertiaryDictionary($models); + } + + // Remove duplicate models from collection + $models = $this->related->newCollection($models)->unique(); + + // If using withTertiary, use the dictionary to set the tertiary relation on each model. + if (!is_null($dictionary)) { + $this->matchTertiaryModels($dictionary, $models); + } + + return $models->all(); + } + + /** + * Build dictionary of related models keyed by the top-level "parent" id. + * If there is a tertiary query set as well, then also build a two-level dictionary + * that maps parent ids to arrays of related ids, which in turn map to arrays + * of tertiary models corresponding to each relationship. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $parentKey + * @return array + */ + protected function buildDictionary(Collection $results, $parentKey = NULL) { + // First we will build a dictionary of child models keyed by the "parent key" (foreign key + // of the intermediate relation) so that we will easily and quickly match them to their + // parents without having a possibly slow inner loops for every models. + $dictionary = []; + + //Example nested dictionary: + //[ + // // User 1 + // '1' => [ + // // Permission 3 + // '3' => [ + // Role1, + // Role2 + // ], + // ... + // ], + // ... + //] + $nestedTertiaryDictionary = NULL; + $tertiaryModels = NULL; + + if ($this->tertiaryRelationName) { + // Get all tertiary models from the result set matching any of the parent models. + $tertiaryModels = $this->getTertiaryModels($results->all()); + } + + foreach ($results as $result) { + $parentKeyValue = $result->pivot->$parentKey; + + // Set the related model in the main dictionary. + // Note that this can end up adding duplicate models. It's cheaper to simply + // go back and remove the duplicates when we actually use the dictionary, + // rather than check for duplicates on each insert. + $dictionary[$parentKeyValue][] = $result; + + // If we're loading tertiary models, then set the keys in the nested dictionary as well. + if (!is_null($tertiaryModels)) { + $tertiaryKeyValue = $result->pivot->{$this->tertiaryKey}; + + if (!is_null($tertiaryKeyValue)) { + $tertiaryModel = clone $tertiaryModels[$tertiaryKeyValue]; + + // We also transfer the pivot relation at this point, since we have already coalesced + // any tertiary models into the nested dictionary. + $this->transferPivotsToTertiary($result, $tertiaryModel); + + $nestedTertiaryDictionary[$parentKeyValue][$result->getKey()][] = $tertiaryModel; + } + } + } + + return [$dictionary, $nestedTertiaryDictionary]; + } + + /** + * Build dictionary of tertiary models keyed by the corresponding related model keys. + * + * @param array $models + * @return array + */ + protected function buildTertiaryDictionary(array $models) { + $dictionary = []; + + // Find the related tertiary entities (e.g. tasks) for all related models (e.g. locations) + $tertiaryModels = $this->getTertiaryModels($models); + + // Now for each related model (e.g. location), we will build out a dictionary of their tertiary models (e.g. tasks) + foreach ($models as $model) { + $tertiaryKeyValue = $model->pivot->{$this->tertiaryKey}; + + $tertiaryModel = clone $tertiaryModels[$tertiaryKeyValue]; + + $this->transferPivotsToTertiary($model, $tertiaryModel); + + $dictionary[$model->getKey()][] = $tertiaryModel; + } + + return $dictionary; + } + + protected function transferPivotsToTertiary($model, $tertiaryModel) { + $pivotAttributes = []; + foreach ($this->pivotColumns as $column) { + $pivotAttributes[$column] = $model->pivot->$column; + unset($model->pivot->$column); + } + // Copy the related key pivot as well, but don't unset on the related model + $pivotAttributes[$this->relatedKey] = $model->pivot->{$this->relatedKey}; + + // Set the tertiary key pivot as well + $pivotAttributes[$this->tertiaryKey] = $tertiaryModel->getKey(); + + $pivot = $this->newExistingPivot($pivotAttributes); + $tertiaryModel->setRelation('pivot', $pivot); + } + + /** + * Get the tertiary models for the relationship. + * + * @param array $models + * @return \Illuminate\Database\Eloquent\Collection + */ + protected function getTertiaryModels(array $models) { + $tertiaryClass = $this->tertiaryRelated; + + $keys = []; + foreach ($models as $model) { + $keys[] = $model->getRelation('pivot')->{$this->tertiaryKey}; + } + $keys = array_unique($keys); + + $query = $tertiaryClass->whereIn($tertiaryClass->getQualifiedKeyName(), $keys); + + // Add any additional constraints/eager loads to the tertiary query + $callback = $this->tertiaryCallback; + $callback($query); + + $tertiaryModels = $query + ->get() + ->keyBy($tertiaryClass->getKeyName()); + + return $tertiaryModels; + } + + /** + * Match a collection of child models into a collection of parent models using a dictionary. + * + * @param array $dictionary + * @param \Illuminate\Database\Eloquent\Collection $results + * @return void + */ + protected function matchTertiaryModels(array $dictionary, Collection $results) { + // Now go through and set the tertiary relation on each child model + foreach ($results as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + $tertiaryModels = $dictionary[$key]; + + $model->setRelation( + $this->tertiaryRelationName, $this->tertiaryRelated->newCollection($tertiaryModels) + ); + } + } + } + + /** + * Unset tertiary pivots on a collection or array of models. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @return void + */ + protected function unsetTertiaryPivots(Collection $models) { + foreach ($models as $model) { + foreach ($this->pivotColumns as $column) { + unset($model->pivot->$column); + } + } + } +} diff --git a/main/app/sprinkles/core/src/Database/Relations/HasManySyncable.php b/main/app/sprinkles/core/src/Database/Relations/HasManySyncable.php index 9d85b1f..d09e25d 100644 --- a/main/app/sprinkles/core/src/Database/Relations/HasManySyncable.php +++ b/main/app/sprinkles/core/src/Database/Relations/HasManySyncable.php @@ -1,23 +1,23 @@ -ci = $ci; - $this->displayErrorDetails = (bool)$displayErrorDetails; - } - - /** - * Invoke error handler - * - * @param ServerRequestInterface $request The most recent Request object - * @param ResponseInterface $response The most recent Response object - * @param Throwable $exception The caught Exception object - * - * @return ResponseInterface - */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $exception) { - // Default exception handler class - $handlerClass = '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandler'; - - // Get the last matching registered handler class, and instantiate it - foreach ($this->exceptionHandlers as $exceptionClass => $matchedHandlerClass) { - if ($exception instanceof $exceptionClass) { - $handlerClass = $matchedHandlerClass; - } - } - - $handler = new $handlerClass($this->ci, $request, $response, $exception, $this->displayErrorDetails); - - return $handler->handle(); - } - - /** - * Register an exception handler for a specified exception class. - * - * The exception handler must implement \UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface. - * - * @param string $exceptionClass The fully qualified class name of the exception to handle. - * @param string $handlerClass The fully qualified class name of the assigned handler. - * @throws InvalidArgumentException If the registered handler fails to implement ExceptionHandlerInterface - */ - public function registerHandler($exceptionClass, $handlerClass) { - if (!is_a($handlerClass, '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandlerInterface', TRUE)) { - throw new \InvalidArgumentException("Registered exception handler must implement ExceptionHandlerInterface!"); - } - - $this->exceptionHandlers[$exceptionClass] = $handlerClass; - } -} +ci = $ci; + $this->displayErrorDetails = (bool)$displayErrorDetails; + } + + /** + * Invoke error handler + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Throwable $exception The caught Exception object + * + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $exception) { + // Default exception handler class + $handlerClass = '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandler'; + + // Get the last matching registered handler class, and instantiate it + foreach ($this->exceptionHandlers as $exceptionClass => $matchedHandlerClass) { + if ($exception instanceof $exceptionClass) { + $handlerClass = $matchedHandlerClass; + } + } + + $handler = new $handlerClass($this->ci, $request, $response, $exception, $this->displayErrorDetails); + + return $handler->handle(); + } + + /** + * Register an exception handler for a specified exception class. + * + * The exception handler must implement \UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface. + * + * @param string $exceptionClass The fully qualified class name of the exception to handle. + * @param string $handlerClass The fully qualified class name of the assigned handler. + * @throws InvalidArgumentException If the registered handler fails to implement ExceptionHandlerInterface + */ + public function registerHandler($exceptionClass, $handlerClass) { + if (!is_a($handlerClass, '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandlerInterface', TRUE)) { + throw new \InvalidArgumentException("Registered exception handler must implement ExceptionHandlerInterface!"); + } + + $this->exceptionHandlers[$exceptionClass] = $handlerClass; + } +} diff --git a/main/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php b/main/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php index 89e890e..1ce5954 100644 --- a/main/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php +++ b/main/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php @@ -1,267 +1,267 @@ -ci = $ci; - $this->request = $request; - $this->response = $response; - $this->exception = $exception; - $this->displayErrorDetails = $displayErrorDetails; - $this->statusCode = $this->determineStatusCode(); - $this->contentType = $this->determineContentType($request, $this->ci->config['site.debug.ajax']); - $this->renderer = $this->determineRenderer(); - } - - /** - * Handle the caught exception. - * The handler may render a detailed debugging error page, a generic error page, write to logs, and/or add messages to the alert stream. - * - * @return ResponseInterface - */ - public function handle() { - // If displayErrorDetails is set to true, we'll halt and immediately respond with a detailed debugging page. - // We do not log errors in this case. - if ($this->displayErrorDetails) { - $response = $this->renderDebugResponse(); - } else { - // Write exception to log - $this->writeToErrorLog(); - - // Render generic error page - $response = $this->renderGenericResponse(); - } - - // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream - if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { - $this->writeAlerts(); - } - - return $response; - } - - /** - * Render a detailed response with debugging information. - * - * @return ResponseInterface - */ - public function renderDebugResponse() { - $body = $this->renderer->renderWithBody(); - - return $this->response - ->withStatus($this->statusCode) - ->withHeader('Content-type', $this->contentType) - ->withBody($body); - } - - /** - * Render a generic, user-friendly response without sensitive debugging information. - * - * @return ResponseInterface - */ - public function renderGenericResponse() { - $messages = $this->determineUserMessages(); - $httpCode = $this->statusCode; - - try { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/$httpCode.html.twig"); - } catch (\Twig_Error_Loader $e) { - $template = $this->ci->view->getEnvironment()->loadTemplate("pages/abstract/error.html.twig"); - } - - return $this->response - ->withStatus($httpCode) - ->withHeader('Content-type', $this->contentType) - ->write($template->render([ - 'messages' => $messages - ])); - } - - /** - * Write to the error log - * - * @return void - */ - public function writeToErrorLog() { - $renderer = new PlainTextRenderer($this->request, $this->response, $this->exception, TRUE); - $error = $renderer->render(); - $error .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; - $this->logError($error); - } - - /** - * Write user-friendly error messages to the alert message stream. - * - * @return void - */ - public function writeAlerts() { - $messages = $this->determineUserMessages(); - - foreach ($messages as $message) { - $this->ci->alerts->addMessageTranslated('danger', $message->message, $message->parameters); - } - } - - /** - * Determine which renderer to use based on content type - * Overloaded $renderer from calling class takes precedence over all - * - * @return ErrorRendererInterface - * - * @throws \RuntimeException - */ - protected function determineRenderer() { - $renderer = $this->renderer; - - if ((!is_null($renderer) && !class_exists($renderer)) - || (!is_null($renderer) && !in_array('UserFrosting\Sprinkle\Core\Error\Renderer\ErrorRendererInterface', class_implements($renderer))) - ) { - throw new \RuntimeException(sprintf( - 'Non compliant error renderer provided (%s). ' . - 'Renderer must implement the ErrorRendererInterface', - $renderer - )); - } - - if (is_null($renderer)) { - switch ($this->contentType) { - case 'application/json': - $renderer = JsonRenderer::class; - break; - - case 'text/xml': - case 'application/xml': - $renderer = XmlRenderer::class; - break; - - case 'text/plain': - $renderer = PlainTextRenderer::class; - break; - - default: - case 'text/html': - $renderer = WhoopsRenderer::class; - break; - } - } - - return new $renderer($this->request, $this->response, $this->exception, $this->displayErrorDetails); - } - - /** - * Resolve the status code to return in the response from this handler. - * - * @return int - */ - protected function determineStatusCode() { - if ($this->request->getMethod() === 'OPTIONS') { - return 200; - } - return 500; - } - - /** - * Resolve a list of error messages to present to the end user. - * - * @return array - */ - protected function determineUserMessages() { - return [ - new UserMessage("ERROR.SERVER") - ]; - } - - /** - * Monolog logging for errors - * - * @param $message - * @return void - */ - protected function logError($message) { - $this->ci->errorLogger->error($message); - } -} +ci = $ci; + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + $this->statusCode = $this->determineStatusCode(); + $this->contentType = $this->determineContentType($request, $this->ci->config['site.debug.ajax']); + $this->renderer = $this->determineRenderer(); + } + + /** + * Handle the caught exception. + * The handler may render a detailed debugging error page, a generic error page, write to logs, and/or add messages to the alert stream. + * + * @return ResponseInterface + */ + public function handle() { + // If displayErrorDetails is set to true, we'll halt and immediately respond with a detailed debugging page. + // We do not log errors in this case. + if ($this->displayErrorDetails) { + $response = $this->renderDebugResponse(); + } else { + // Write exception to log + $this->writeToErrorLog(); + + // Render generic error page + $response = $this->renderGenericResponse(); + } + + // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream + if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + $this->writeAlerts(); + } + + return $response; + } + + /** + * Render a detailed response with debugging information. + * + * @return ResponseInterface + */ + public function renderDebugResponse() { + $body = $this->renderer->renderWithBody(); + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } + + /** + * Render a generic, user-friendly response without sensitive debugging information. + * + * @return ResponseInterface + */ + public function renderGenericResponse() { + $messages = $this->determineUserMessages(); + $httpCode = $this->statusCode; + + try { + $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/$httpCode.html.twig"); + } catch (\Twig_Error_Loader $e) { + $template = $this->ci->view->getEnvironment()->loadTemplate("pages/abstract/error.html.twig"); + } + + return $this->response + ->withStatus($httpCode) + ->withHeader('Content-type', $this->contentType) + ->write($template->render([ + 'messages' => $messages + ])); + } + + /** + * Write to the error log + * + * @return void + */ + public function writeToErrorLog() { + $renderer = new PlainTextRenderer($this->request, $this->response, $this->exception, TRUE); + $error = $renderer->render(); + $error .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + $this->logError($error); + } + + /** + * Write user-friendly error messages to the alert message stream. + * + * @return void + */ + public function writeAlerts() { + $messages = $this->determineUserMessages(); + + foreach ($messages as $message) { + $this->ci->alerts->addMessageTranslated('danger', $message->message, $message->parameters); + } + } + + /** + * Determine which renderer to use based on content type + * Overloaded $renderer from calling class takes precedence over all + * + * @return ErrorRendererInterface + * + * @throws \RuntimeException + */ + protected function determineRenderer() { + $renderer = $this->renderer; + + if ((!is_null($renderer) && !class_exists($renderer)) + || (!is_null($renderer) && !in_array('UserFrosting\Sprinkle\Core\Error\Renderer\ErrorRendererInterface', class_implements($renderer))) + ) { + throw new \RuntimeException(sprintf( + 'Non compliant error renderer provided (%s). ' . + 'Renderer must implement the ErrorRendererInterface', + $renderer + )); + } + + if (is_null($renderer)) { + switch ($this->contentType) { + case 'application/json': + $renderer = JsonRenderer::class; + break; + + case 'text/xml': + case 'application/xml': + $renderer = XmlRenderer::class; + break; + + case 'text/plain': + $renderer = PlainTextRenderer::class; + break; + + default: + case 'text/html': + $renderer = WhoopsRenderer::class; + break; + } + } + + return new $renderer($this->request, $this->response, $this->exception, $this->displayErrorDetails); + } + + /** + * Resolve the status code to return in the response from this handler. + * + * @return int + */ + protected function determineStatusCode() { + if ($this->request->getMethod() === 'OPTIONS') { + return 200; + } + return 500; + } + + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() { + return [ + new UserMessage("ERROR.SERVER") + ]; + } + + /** + * Monolog logging for errors + * + * @param $message + * @return void + */ + protected function logError($message) { + $this->ci->errorLogger->error($message); + } +} diff --git a/main/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php b/main/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php index 2ac9ada..2905382 100644 --- a/main/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php +++ b/main/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php @@ -1,33 +1,33 @@ -statusCode != 500) { - return; - } - - parent::writeToErrorLog(); - } - - /** - * Resolve the status code to return in the response from this handler. - * - * @return int - */ - protected function determineStatusCode() { - if ($this->request->getMethod() === 'OPTIONS') { - return 200; - } else if ($this->exception instanceof HttpException) { - return $this->exception->getHttpErrorCode(); - } - return 500; - } - - /** - * Resolve a list of error messages to present to the end user. - * - * @return array - */ - protected function determineUserMessages() { - if ($this->exception instanceof HttpException) { - return $this->exception->getUserMessages(); - } - - // Fallback - return [ - new UserMessage("ERROR.SERVER") - ]; - } -} +statusCode != 500) { + return; + } + + parent::writeToErrorLog(); + } + + /** + * Resolve the status code to return in the response from this handler. + * + * @return int + */ + protected function determineStatusCode() { + if ($this->request->getMethod() === 'OPTIONS') { + return 200; + } else if ($this->exception instanceof HttpException) { + return $this->exception->getHttpErrorCode(); + } + return 500; + } + + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() { + if ($this->exception instanceof HttpException) { + return $this->exception->getUserMessages(); + } + + // Fallback + return [ + new UserMessage("ERROR.SERVER") + ]; + } +} diff --git a/main/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php b/main/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php index a866cc3..e243a02 100644 --- a/main/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php +++ b/main/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php @@ -1,38 +1,38 @@ -renderGenericResponse(); - - // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream - if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { - $this->writeAlerts(); - } - - return $response; - } -} +renderGenericResponse(); + + // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream + if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + $this->writeAlerts(); + } + + return $response; + } +} diff --git a/main/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php b/main/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php index e7117e9..15e16f8 100644 --- a/main/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php +++ b/main/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php @@ -1,30 +1,30 @@ -request = $request; - $this->response = $response; - $this->exception = $exception; - $this->displayErrorDetails = $displayErrorDetails; - } - - abstract public function render(); - - /** - * @return Body - */ - public function renderWithBody() { - $body = new Body(fopen('php://temp', 'r+')); - $body->write($this->render()); - return $body; - } -} +request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + } + + abstract public function render(); + + /** + * @return Body + */ + public function renderWithBody() { + $body = new Body(fopen('php://temp', 'r+')); + $body->write($this->render()); + return $body; + } +} diff --git a/main/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php b/main/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php index efb0bd6..1633a8b 100644 --- a/main/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php +++ b/main/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php @@ -1,30 +1,30 @@ -displayErrorDetails) { - $html = '

The application could not run because of the following error:

'; - $html .= '

Details

'; - $html .= $this->renderException($this->exception); - - $html .= '

Your request

'; - $html .= $this->renderRequest(); - - $html .= '

Response headers

'; - $html .= $this->renderResponseHeaders(); - - $exception = $this->exception; - while ($exception = $exception->getPrevious()) { - $html .= '

Previous exception

'; - $html .= $this->renderException($exception); - } - } else { - $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; - } - - $output = sprintf( - "" . - "%s

%s

%s", - $title, - $title, - $html - ); - - return $output; - } - - /** - * Render a summary of the exception. - * - * @param Exception $exception - * @return string - */ - public function renderException($exception) { - $html = sprintf('
Type: %s
', get_class($exception)); - - if (($code = $exception->getCode())) { - $html .= sprintf('
Code: %s
', $code); - } - - if (($message = $exception->getMessage())) { - $html .= sprintf('
Message: %s
', htmlentities($message)); - } - - if (($file = $exception->getFile())) { - $html .= sprintf('
File: %s
', $file); - } - - if (($line = $exception->getLine())) { - $html .= sprintf('
Line: %s
', $line); - } - - if (($trace = $exception->getTraceAsString())) { - $html .= '

Trace

'; - $html .= sprintf('
%s
', htmlentities($trace)); - } - - return $html; - } - - /** - * Render HTML representation of original request. - * - * @return string - */ - public function renderRequest() { - $method = $this->request->getMethod(); - $uri = $this->request->getUri(); - $params = $this->request->getParams(); - $requestHeaders = $this->request->getHeaders(); - - $html = '

Request URI:

'; - - $html .= sprintf('
%s %s
', $method, $uri); - - $html .= '

Request parameters:

'; - - $html .= $this->renderTable($params); - - $html .= '

Request headers:

'; - - $html .= $this->renderTable($requestHeaders); - - return $html; - } - - /** - * Render HTML representation of response headers. - * - * @return string - */ - public function renderResponseHeaders() { - $html = '

Response headers:

'; - $html .= 'Additional response headers may have been set by Slim after the error handling routine. Please check your browser console for a complete list.
'; - - $html .= $this->renderTable($this->response->getHeaders()); - - return $html; - } - - /** - * Render HTML representation of a table of data. - * - * @param mixed[] $data the array of data to render. - * - * @return string - */ - protected function renderTable($data) { - $html = ''; - foreach ($data as $name => $value) { - $value = print_r($value, TRUE); - $html .= ""; - } - $html .= '
NameValue
$name$value
'; - - return $html; - } -} +displayErrorDetails) { + $html = '

The application could not run because of the following error:

'; + $html .= '

Details

'; + $html .= $this->renderException($this->exception); + + $html .= '

Your request

'; + $html .= $this->renderRequest(); + + $html .= '

Response headers

'; + $html .= $this->renderResponseHeaders(); + + $exception = $this->exception; + while ($exception = $exception->getPrevious()) { + $html .= '

Previous exception

'; + $html .= $this->renderException($exception); + } + } else { + $html = '

A website error has occurred. Sorry for the temporary inconvenience.

'; + } + + $output = sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render a summary of the exception. + * + * @param Exception $exception + * @return string + */ + public function renderException($exception) { + $html = sprintf('
Type: %s
', get_class($exception)); + + if (($code = $exception->getCode())) { + $html .= sprintf('
Code: %s
', $code); + } + + if (($message = $exception->getMessage())) { + $html .= sprintf('
Message: %s
', htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $html .= sprintf('
File: %s
', $file); + } + + if (($line = $exception->getLine())) { + $html .= sprintf('
Line: %s
', $line); + } + + if (($trace = $exception->getTraceAsString())) { + $html .= '

Trace

'; + $html .= sprintf('
%s
', htmlentities($trace)); + } + + return $html; + } + + /** + * Render HTML representation of original request. + * + * @return string + */ + public function renderRequest() { + $method = $this->request->getMethod(); + $uri = $this->request->getUri(); + $params = $this->request->getParams(); + $requestHeaders = $this->request->getHeaders(); + + $html = '

Request URI:

'; + + $html .= sprintf('
%s %s
', $method, $uri); + + $html .= '

Request parameters:

'; + + $html .= $this->renderTable($params); + + $html .= '

Request headers:

'; + + $html .= $this->renderTable($requestHeaders); + + return $html; + } + + /** + * Render HTML representation of response headers. + * + * @return string + */ + public function renderResponseHeaders() { + $html = '

Response headers:

'; + $html .= 'Additional response headers may have been set by Slim after the error handling routine. Please check your browser console for a complete list.
'; + + $html .= $this->renderTable($this->response->getHeaders()); + + return $html; + } + + /** + * Render HTML representation of a table of data. + * + * @param mixed[] $data the array of data to render. + * + * @return string + */ + protected function renderTable($data) { + $html = ''; + foreach ($data as $name => $value) { + $value = print_r($value, TRUE); + $html .= ""; + } + $html .= '
NameValue
$name$value
'; + + return $html; + } +} diff --git a/main/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php b/main/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php index 6a96780..eb1ddd2 100644 --- a/main/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php +++ b/main/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php @@ -1,55 +1,55 @@ -exception->getMessage(); - return $this->formatExceptionPayload($message); - } - - /** - * @param $message - * @return string - */ - public function formatExceptionPayload($message) { - $e = $this->exception; - $error = ['message' => $message]; - - if ($this->displayErrorDetails) { - $error['exception'] = []; - do { - $error['exception'][] = $this->formatExceptionFragment($e); - } while ($e = $e->getPrevious()); - } - - return json_encode($error, JSON_PRETTY_PRINT); - } - - /** - * @param \Exception|\Throwable $e - * @return array - */ - public function formatExceptionFragment($e) { - return [ - 'type' => get_class($e), - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - ]; - } -} +exception->getMessage(); + return $this->formatExceptionPayload($message); + } + + /** + * @param $message + * @return string + */ + public function formatExceptionPayload($message) { + $e = $this->exception; + $error = ['message' => $message]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + do { + $error['exception'][] = $this->formatExceptionFragment($e); + } while ($e = $e->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + /** + * @param \Exception|\Throwable $e + * @return array + */ + public function formatExceptionFragment($e) { + return [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]; + } +} diff --git a/main/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php b/main/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php index 9922f22..847681e 100644 --- a/main/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php +++ b/main/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php @@ -1,63 +1,63 @@ -displayErrorDetails) { - return $this->formatExceptionBody(); - } - - return $this->exception->getMessage(); - } - - public function formatExceptionBody() { - $e = $this->exception; - - $text = 'UserFrosting Application Error:' . PHP_EOL; - $text .= $this->formatExceptionFragment($e); - - while ($e = $e->getPrevious()) { - $text .= PHP_EOL . 'Previous Error:' . PHP_EOL; - $text .= $this->formatExceptionFragment($e); - } - - return $text; - } - - /** - * @param \Exception|\Throwable $e - * @return string - */ - public function formatExceptionFragment($e) { - $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); - - if ($code = $e->getCode()) { - $text .= sprintf('Code: %s' . PHP_EOL, $code); - } - if ($message = $e->getMessage()) { - $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); - } - if ($file = $e->getFile()) { - $text .= sprintf('File: %s' . PHP_EOL, $file); - } - if ($line = $e->getLine()) { - $text .= sprintf('Line: %s' . PHP_EOL, $line); - } - if ($trace = $e->getTraceAsString()) { - $text .= sprintf('Trace: %s', $trace); - } - - return $text; - } -} +displayErrorDetails) { + return $this->formatExceptionBody(); + } + + return $this->exception->getMessage(); + } + + public function formatExceptionBody() { + $e = $this->exception; + + $text = 'UserFrosting Application Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + + while ($e = $e->getPrevious()) { + $text .= PHP_EOL . 'Previous Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + } + + return $text; + } + + /** + * @param \Exception|\Throwable $e + * @return string + */ + public function formatExceptionFragment($e) { + $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); + + if ($code = $e->getCode()) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + if ($message = $e->getMessage()) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + if ($file = $e->getFile()) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + if ($line = $e->getLine()) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + if ($trace = $e->getTraceAsString()) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } +} diff --git a/main/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php b/main/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php index 4113470..d16d048 100644 --- a/main/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php +++ b/main/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php @@ -1,688 +1,688 @@ - [], - '_POST' => [], - '_FILES' => [], - '_COOKIE' => [], - '_SESSION' => [], - '_SERVER' => ['DB_PASSWORD', 'SMTP_PASSWORD'], - '_ENV' => ['DB_PASSWORD', 'SMTP_PASSWORD'], - ]; - - /** - * A string identifier for a known IDE/text editor, or a closure - * that resolves a string that can be used to open a given file - * in an editor. If the string contains the special substrings - * %file or %line, they will be replaced with the correct data. - * - * @example - * "txmt://open?url=%file&line=%line" - * @var mixed $editor - */ - protected $editor; - - /** - * A list of known editor strings - * @var array - */ - protected $editors = [ - "sublime" => "subl://open?url=file://%file&line=%line", - "textmate" => "txmt://open?url=file://%file&line=%line", - "emacs" => "emacs://open?url=file://%file&line=%line", - "macvim" => "mvim://open/?url=file://%file&line=%line", - "phpstorm" => "phpstorm://open?file=%file&line=%line", - "idea" => "idea://open?file=%file&line=%line", - ]; - - /** - * @var Inspector - */ - protected $inspector; - - /** - * @var TemplateHelper - */ - private $templateHelper; - - /** - * {@inheritDoc} - */ - public function __construct($request, $response, $exception, $displayErrorDetails = FALSE) { - $this->request = $request; - $this->response = $response; - $this->exception = $exception; - $this->displayErrorDetails = $displayErrorDetails; - - if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) { - // Register editor using xdebug's file_link_format option. - $this->editors['xdebug'] = function ($file, $line) { - return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format')); - }; - } - - // Add the default, local resource search path: - $this->searchPaths[] = \UserFrosting\VENDOR_DIR . '/filp/whoops/src/Whoops/Resources'; - - // blacklist php provided auth based values - $this->blacklist('_SERVER', 'PHP_AUTH_PW'); - - $this->templateHelper = new TemplateHelper(); - - // Set up dummy inspector - $this->inspector = new Inspector($exception); - - if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { - $cloner = new VarCloner(); - // Only dump object internals if a custom caster exists. - $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) { - $class = $stub->class; - $classes = [$class => $class] + class_parents($class) + class_implements($class); - - foreach ($classes as $class) { - if (isset(AbstractCloner::$defaultCasters[$class])) { - return $a; - } - } - - // Remove all internals - return []; - }]); - $this->templateHelper->setCloner($cloner); - } - } - - /** - * {@inheritDoc} - */ - public function render() { - if (!$this->handleUnconditionally()) { - // Check conditions for outputting HTML: - // : Make this more robust - if (php_sapi_name() === 'cli') { - // Help users who have been relying on an internal test value - // fix their code to the proper method - if (isset($_ENV['whoops-test'])) { - throw new \Exception( - 'Use handleUnconditionally instead of whoops-test' - . ' environment variable' - ); - } - - return Handler::DONE; - } - } - - $templateFile = $this->getResource("views/layout.html.php"); - $cssFile = $this->getResource("css/whoops.base.css"); - $zeptoFile = $this->getResource("js/zepto.min.js"); - $clipboard = $this->getResource("js/clipboard.min.js"); - $jsFile = $this->getResource("js/whoops.base.js"); - - if ($this->customCss) { - $customCssFile = $this->getResource($this->customCss); - } - - $inspector = $this->getInspector(); - $frames = $inspector->getFrames(); - - $code = $inspector->getException()->getCode(); - - if ($inspector->getException() instanceof \ErrorException) { - // ErrorExceptions wrap the php-error types within the "severity" property - $code = Misc::translateErrorCode($inspector->getException()->getSeverity()); - } - - // Detect frames that belong to the application. - if ($this->applicationPaths) { - /* @var \Whoops\Exception\Frame $frame */ - foreach ($frames as $frame) { - foreach ($this->applicationPaths as $path) { - if (substr($frame->getFile(), 0, strlen($path)) === $path) { - $frame->setApplication(TRUE); - break; - } - } - } - } - - // Nicely format the session object - $session = isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : []; - $session = ['session' => Util::prettyPrintArray($session)]; - - // List of variables that will be passed to the layout template. - $vars = [ - "page_title" => $this->getPageTitle(), - - // : Asset compiler - "stylesheet" => file_get_contents($cssFile), - "zepto" => file_get_contents($zeptoFile), - "clipboard" => file_get_contents($clipboard), - "javascript" => file_get_contents($jsFile), - - // Template paths: - "header" => $this->getResource("views/header.html.php"), - "header_outer" => $this->getResource("views/header_outer.html.php"), - "frame_list" => $this->getResource("views/frame_list.html.php"), - "frames_description" => $this->getResource("views/frames_description.html.php"), - "frames_container" => $this->getResource("views/frames_container.html.php"), - "panel_details" => $this->getResource("views/panel_details.html.php"), - "panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"), - "panel_left" => $this->getResource("views/panel_left.html.php"), - "panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"), - "frame_code" => $this->getResource("views/frame_code.html.php"), - "env_details" => $this->getResource("views/env_details.html.php"), - - "title" => $this->getPageTitle(), - "name" => explode("\\", $inspector->getExceptionName()), - "message" => $inspector->getException()->getMessage(), - "code" => $code, - "plain_exception" => Formatter::formatExceptionPlain($inspector), - "frames" => $frames, - "has_frames" => !!count($frames), - "handler" => $this, - "handlers" => [$this], - - "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all', - "has_frames_tabs" => $this->getApplicationPaths(), - - "tables" => [ - "GET Data" => $this->masked($_GET, '_GET'), - "POST Data" => $this->masked($_POST, '_POST'), - "Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [], - "Cookies" => $this->masked($_COOKIE, '_COOKIE'), - "Session" => $session, - "Server/Request Data" => $this->masked($_SERVER, '_SERVER'), - "Environment Variables" => $this->masked($_ENV, '_ENV'), - ], - ]; - - if (isset($customCssFile)) { - $vars["stylesheet"] .= file_get_contents($customCssFile); - } - - // Add extra entries list of data tables: - // : Consolidate addDataTable and addDataTableCallback - $extraTables = array_map(function ($table) use ($inspector) { - return $table instanceof \Closure ? $table($inspector) : $table; - }, $this->getDataTables()); - $vars["tables"] = array_merge($extraTables, $vars["tables"]); - - $plainTextHandler = new PlainTextHandler(); - $plainTextHandler->setException($this->getException()); - $plainTextHandler->setInspector($this->getInspector()); - $vars["preface"] = ""; - - $this->templateHelper->setVariables($vars); - - ob_start(); - $this->templateHelper->render($templateFile); - - $result = ob_get_clean(); - return $result; - } - - /** - * Adds an entry to the list of tables displayed in the template. - * The expected data is a simple associative array. Any nested arrays - * will be flattened with print_r - * @param string $label - * @param array $data - */ - public function addDataTable($label, array $data) { - $this->extraTables[$label] = $data; - } - - /** - * Lazily adds an entry to the list of tables displayed in the table. - * The supplied callback argument will be called when the error is rendered, - * it should produce a simple associative array. Any nested arrays will - * be flattened with print_r. - * - * @throws InvalidArgumentException If $callback is not callable - * @param string $label - * @param callable $callback Callable returning an associative array - */ - public function addDataTableCallback($label, /* callable */ - $callback) { - if (!is_callable($callback)) { - throw new InvalidArgumentException('Expecting callback argument to be callable'); - } - - $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = NULL) use ($callback) { - try { - $result = call_user_func($callback, $inspector); - - // Only return the result if it can be iterated over by foreach(). - return is_array($result) || $result instanceof \Traversable ? $result : []; - } catch (\Exception $e) { - // Don't allow failure to break the rendering of the original exception. - return []; - } - }; - } - - /** - * blacklist a sensitive value within one of the superglobal arrays. - * - * @param $superGlobalName string the name of the superglobal array, e.g. '_GET' - * @param $key string the key within the superglobal - */ - public function blacklist($superGlobalName, $key) { - $this->blacklist[$superGlobalName][] = $key; - } - - /** - * Returns all the extra data tables registered with this handler. - * Optionally accepts a 'label' parameter, to only return the data - * table under that label. - * @param string|null $label - * @return array[]|callable - */ - public function getDataTables($label = NULL) { - if ($label !== NULL) { - return isset($this->extraTables[$label]) ? - $this->extraTables[$label] : []; - } - - return $this->extraTables; - } - - /** - * Allows to disable all attempts to dynamically decide whether to - * handle or return prematurely. - * Set this to ensure that the handler will perform no matter what. - * @param bool|null $value - * @return bool|null - */ - public function handleUnconditionally($value = NULL) { - if (func_num_args() == 0) { - return $this->handleUnconditionally; - } - - $this->handleUnconditionally = (bool)$value; - } - - /** - * Adds an editor resolver, identified by a string - * name, and that may be a string path, or a callable - * resolver. If the callable returns a string, it will - * be set as the file reference's href attribute. - * - * @example - * $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line") - * @example - * $run->addEditor('remove-it', function($file, $line) { - * unlink($file); - * return "http://stackoverflow.com"; - * }); - * @param string $identifier - * @param string $resolver - */ - public function addEditor($identifier, $resolver) { - $this->editors[$identifier] = $resolver; - } - - /** - * Set the editor to use to open referenced files, by a string - * identifier, or a callable that will be executed for every - * file reference, with a $file and $line argument, and should - * return a string. - * - * @example - * $run->setEditor(function($file, $line) { return "file:///{$file}"; }); - * @example - * $run->setEditor('sublime'); - * - * @throws InvalidArgumentException If invalid argument identifier provided - * @param string|callable $editor - */ - public function setEditor($editor) { - if (!is_callable($editor) && !isset($this->editors[$editor])) { - throw new InvalidArgumentException( - "Unknown editor identifier: $editor. Known editors:" . - implode(",", array_keys($this->editors)) - ); - } - - $this->editor = $editor; - } - - /** - * Given a string file path, and an integer file line, - * executes the editor resolver and returns, if available, - * a string that may be used as the href property for that - * file reference. - * - * @throws InvalidArgumentException If editor resolver does not return a string - * @param string $filePath - * @param int $line - * @return string|bool - */ - public function getEditorHref($filePath, $line) { - $editor = $this->getEditor($filePath, $line); - - if (empty($editor)) { - return FALSE; - } - - // Check that the editor is a string, and replace the - // %line and %file placeholders: - if (!isset($editor['url']) || !is_string($editor['url'])) { - throw new UnexpectedValueException( - __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead." - ); - } - - $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']); - $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']); - - return $editor['url']; - } - - /** - * Given a boolean if the editor link should - * act as an Ajax request. The editor must be a - * valid callable function/closure - * - * @throws UnexpectedValueException If editor resolver does not return a boolean - * @param string $filePath - * @param int $line - * @return bool - */ - public function getEditorAjax($filePath, $line) { - $editor = $this->getEditor($filePath, $line); - - // Check that the ajax is a bool - if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) { - throw new UnexpectedValueException( - __METHOD__ . " should always resolve to a bool; got something else instead." - ); - } - return $editor['ajax']; - } - - /** - * @param string $title - * @return void - */ - public function setPageTitle($title) { - $this->pageTitle = (string)$title; - } - - /** - * @return string - */ - public function getPageTitle() { - return $this->pageTitle; - } - - /** - * Adds a path to the list of paths to be searched for - * resources. - * - * @throws InvalidArgumentException If $path is not a valid directory - * - * @param string $path - * @return void - */ - public function addResourcePath($path) { - if (!is_dir($path)) { - throw new InvalidArgumentException( - "'$path' is not a valid directory" - ); - } - - array_unshift($this->searchPaths, $path); - } - - /** - * Adds a custom css file to be loaded. - * - * @param string $name - * @return void - */ - public function addCustomCss($name) { - $this->customCss = $name; - } - - /** - * @return array - */ - public function getResourcePaths() { - return $this->searchPaths; - } - - /** - * @deprecated - * - * @return string - */ - public function getResourcesPath() { - $allPaths = $this->getResourcePaths(); - - // Compat: return only the first path added - return end($allPaths) ?: NULL; - } - - /** - * @deprecated - * - * @param string $resourcesPath - * @return void - */ - public function setResourcesPath($resourcesPath) { - $this->addResourcePath($resourcesPath); - } - - /** - * Return the application paths. - * - * @return array - */ - public function getApplicationPaths() { - return $this->applicationPaths; - } - - /** - * Set the application paths. - * - * @param array $applicationPaths - */ - public function setApplicationPaths($applicationPaths) { - $this->applicationPaths = $applicationPaths; - } - - /** - * Set the application root path. - * - * @param string $applicationRootPath - */ - public function setApplicationRootPath($applicationRootPath) { - $this->templateHelper->setApplicationRootPath($applicationRootPath); - } - - /** - * Given a boolean if the editor link should - * act as an Ajax request. The editor must be a - * valid callable function/closure - * - * @param string $filePath - * @param int $line - * @return array - */ - protected function getEditor($filePath, $line) { - if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) { - return []; - } - - if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) { - return [ - 'ajax' => FALSE, - 'url' => $this->editors[$this->editor], - ]; - } - - if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) { - if (is_callable($this->editor)) { - $callback = call_user_func($this->editor, $filePath, $line); - } else { - $callback = call_user_func($this->editors[$this->editor], $filePath, $line); - } - - if (is_string($callback)) { - return [ - 'ajax' => FALSE, - 'url' => $callback, - ]; - } - - return [ - 'ajax' => isset($callback['ajax']) ? $callback['ajax'] : FALSE, - 'url' => isset($callback['url']) ? $callback['url'] : $callback, - ]; - } - - return []; - } - - /** - * @return \Throwable - */ - protected function getException() { - return $this->exception; - } - - /** - * @return Inspector - */ - protected function getInspector() { - return $this->inspector; - } - - /** - * Finds a resource, by its relative path, in all available search paths. - * The search is performed starting at the last search path, and all the - * way back to the first, enabling a cascading-type system of overrides - * for all resources. - * - * @throws RuntimeException If resource cannot be found in any of the available paths - * - * @param string $resource - * @return string - */ - protected function getResource($resource) { - // If the resource was found before, we can speed things up - // by caching its absolute, resolved path: - if (isset($this->resourceCache[$resource])) { - return $this->resourceCache[$resource]; - } - - // Search through available search paths, until we find the - // resource we're after: - foreach ($this->searchPaths as $path) { - $fullPath = $path . "/$resource"; - - if (is_file($fullPath)) { - // Cache the result: - $this->resourceCache[$resource] = $fullPath; - return $fullPath; - } - } - - // If we got this far, nothing was found. - throw new RuntimeException( - "Could not find resource '$resource' in any resource paths." - . "(searched: " . join(", ", $this->searchPaths) . ")" - ); - } - - /** - * Checks all values within the given superGlobal array. - * Blacklisted values will be replaced by a equal length string cointaining only '*' characters. - * - * We intentionally dont rely on $GLOBALS as it depends on 'auto_globals_jit' php.ini setting. - * - * @param array $superGlobal One of the superglobal arrays - * @param string $superGlobalName the name of the superglobal array, e.g. '_GET' - * @return array $values without sensitive data - */ - private function masked(array $superGlobal, $superGlobalName) { - $blacklisted = $this->blacklist[$superGlobalName]; - - $values = $superGlobal; - foreach ($blacklisted as $key) { - if (isset($superGlobal[$key])) { - $values[$key] = str_repeat('*', strlen($superGlobal[$key])); - } - } - return $values; - } -} + [], + '_POST' => [], + '_FILES' => [], + '_COOKIE' => [], + '_SESSION' => [], + '_SERVER' => ['DB_PASSWORD', 'SMTP_PASSWORD'], + '_ENV' => ['DB_PASSWORD', 'SMTP_PASSWORD'], + ]; + + /** + * A string identifier for a known IDE/text editor, or a closure + * that resolves a string that can be used to open a given file + * in an editor. If the string contains the special substrings + * %file or %line, they will be replaced with the correct data. + * + * @example + * "txmt://open?url=%file&line=%line" + * @var mixed $editor + */ + protected $editor; + + /** + * A list of known editor strings + * @var array + */ + protected $editors = [ + "sublime" => "subl://open?url=file://%file&line=%line", + "textmate" => "txmt://open?url=file://%file&line=%line", + "emacs" => "emacs://open?url=file://%file&line=%line", + "macvim" => "mvim://open/?url=file://%file&line=%line", + "phpstorm" => "phpstorm://open?file=%file&line=%line", + "idea" => "idea://open?file=%file&line=%line", + ]; + + /** + * @var Inspector + */ + protected $inspector; + + /** + * @var TemplateHelper + */ + private $templateHelper; + + /** + * {@inheritDoc} + */ + public function __construct($request, $response, $exception, $displayErrorDetails = FALSE) { + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + + if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) { + // Register editor using xdebug's file_link_format option. + $this->editors['xdebug'] = function ($file, $line) { + return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format')); + }; + } + + // Add the default, local resource search path: + $this->searchPaths[] = \UserFrosting\VENDOR_DIR . '/filp/whoops/src/Whoops/Resources'; + + // blacklist php provided auth based values + $this->blacklist('_SERVER', 'PHP_AUTH_PW'); + + $this->templateHelper = new TemplateHelper(); + + // Set up dummy inspector + $this->inspector = new Inspector($exception); + + if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { + $cloner = new VarCloner(); + // Only dump object internals if a custom caster exists. + $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) { + $class = $stub->class; + $classes = [$class => $class] + class_parents($class) + class_implements($class); + + foreach ($classes as $class) { + if (isset(AbstractCloner::$defaultCasters[$class])) { + return $a; + } + } + + // Remove all internals + return []; + }]); + $this->templateHelper->setCloner($cloner); + } + } + + /** + * {@inheritDoc} + */ + public function render() { + if (!$this->handleUnconditionally()) { + // Check conditions for outputting HTML: + // : Make this more robust + if (php_sapi_name() === 'cli') { + // Help users who have been relying on an internal test value + // fix their code to the proper method + if (isset($_ENV['whoops-test'])) { + throw new \Exception( + 'Use handleUnconditionally instead of whoops-test' + . ' environment variable' + ); + } + + return Handler::DONE; + } + } + + $templateFile = $this->getResource("views/layout.html.php"); + $cssFile = $this->getResource("css/whoops.base.css"); + $zeptoFile = $this->getResource("js/zepto.min.js"); + $clipboard = $this->getResource("js/clipboard.min.js"); + $jsFile = $this->getResource("js/whoops.base.js"); + + if ($this->customCss) { + $customCssFile = $this->getResource($this->customCss); + } + + $inspector = $this->getInspector(); + $frames = $inspector->getFrames(); + + $code = $inspector->getException()->getCode(); + + if ($inspector->getException() instanceof \ErrorException) { + // ErrorExceptions wrap the php-error types within the "severity" property + $code = Misc::translateErrorCode($inspector->getException()->getSeverity()); + } + + // Detect frames that belong to the application. + if ($this->applicationPaths) { + /* @var \Whoops\Exception\Frame $frame */ + foreach ($frames as $frame) { + foreach ($this->applicationPaths as $path) { + if (substr($frame->getFile(), 0, strlen($path)) === $path) { + $frame->setApplication(TRUE); + break; + } + } + } + } + + // Nicely format the session object + $session = isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : []; + $session = ['session' => Util::prettyPrintArray($session)]; + + // List of variables that will be passed to the layout template. + $vars = [ + "page_title" => $this->getPageTitle(), + + // : Asset compiler + "stylesheet" => file_get_contents($cssFile), + "zepto" => file_get_contents($zeptoFile), + "clipboard" => file_get_contents($clipboard), + "javascript" => file_get_contents($jsFile), + + // Template paths: + "header" => $this->getResource("views/header.html.php"), + "header_outer" => $this->getResource("views/header_outer.html.php"), + "frame_list" => $this->getResource("views/frame_list.html.php"), + "frames_description" => $this->getResource("views/frames_description.html.php"), + "frames_container" => $this->getResource("views/frames_container.html.php"), + "panel_details" => $this->getResource("views/panel_details.html.php"), + "panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"), + "panel_left" => $this->getResource("views/panel_left.html.php"), + "panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"), + "frame_code" => $this->getResource("views/frame_code.html.php"), + "env_details" => $this->getResource("views/env_details.html.php"), + + "title" => $this->getPageTitle(), + "name" => explode("\\", $inspector->getExceptionName()), + "message" => $inspector->getException()->getMessage(), + "code" => $code, + "plain_exception" => Formatter::formatExceptionPlain($inspector), + "frames" => $frames, + "has_frames" => !!count($frames), + "handler" => $this, + "handlers" => [$this], + + "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all', + "has_frames_tabs" => $this->getApplicationPaths(), + + "tables" => [ + "GET Data" => $this->masked($_GET, '_GET'), + "POST Data" => $this->masked($_POST, '_POST'), + "Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [], + "Cookies" => $this->masked($_COOKIE, '_COOKIE'), + "Session" => $session, + "Server/Request Data" => $this->masked($_SERVER, '_SERVER'), + "Environment Variables" => $this->masked($_ENV, '_ENV'), + ], + ]; + + if (isset($customCssFile)) { + $vars["stylesheet"] .= file_get_contents($customCssFile); + } + + // Add extra entries list of data tables: + // : Consolidate addDataTable and addDataTableCallback + $extraTables = array_map(function ($table) use ($inspector) { + return $table instanceof \Closure ? $table($inspector) : $table; + }, $this->getDataTables()); + $vars["tables"] = array_merge($extraTables, $vars["tables"]); + + $plainTextHandler = new PlainTextHandler(); + $plainTextHandler->setException($this->getException()); + $plainTextHandler->setInspector($this->getInspector()); + $vars["preface"] = ""; + + $this->templateHelper->setVariables($vars); + + ob_start(); + $this->templateHelper->render($templateFile); + + $result = ob_get_clean(); + return $result; + } + + /** + * Adds an entry to the list of tables displayed in the template. + * The expected data is a simple associative array. Any nested arrays + * will be flattened with print_r + * @param string $label + * @param array $data + */ + public function addDataTable($label, array $data) { + $this->extraTables[$label] = $data; + } + + /** + * Lazily adds an entry to the list of tables displayed in the table. + * The supplied callback argument will be called when the error is rendered, + * it should produce a simple associative array. Any nested arrays will + * be flattened with print_r. + * + * @throws InvalidArgumentException If $callback is not callable + * @param string $label + * @param callable $callback Callable returning an associative array + */ + public function addDataTableCallback($label, /* callable */ + $callback) { + if (!is_callable($callback)) { + throw new InvalidArgumentException('Expecting callback argument to be callable'); + } + + $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = NULL) use ($callback) { + try { + $result = call_user_func($callback, $inspector); + + // Only return the result if it can be iterated over by foreach(). + return is_array($result) || $result instanceof \Traversable ? $result : []; + } catch (\Exception $e) { + // Don't allow failure to break the rendering of the original exception. + return []; + } + }; + } + + /** + * blacklist a sensitive value within one of the superglobal arrays. + * + * @param $superGlobalName string the name of the superglobal array, e.g. '_GET' + * @param $key string the key within the superglobal + */ + public function blacklist($superGlobalName, $key) { + $this->blacklist[$superGlobalName][] = $key; + } + + /** + * Returns all the extra data tables registered with this handler. + * Optionally accepts a 'label' parameter, to only return the data + * table under that label. + * @param string|null $label + * @return array[]|callable + */ + public function getDataTables($label = NULL) { + if ($label !== NULL) { + return isset($this->extraTables[$label]) ? + $this->extraTables[$label] : []; + } + + return $this->extraTables; + } + + /** + * Allows to disable all attempts to dynamically decide whether to + * handle or return prematurely. + * Set this to ensure that the handler will perform no matter what. + * @param bool|null $value + * @return bool|null + */ + public function handleUnconditionally($value = NULL) { + if (func_num_args() == 0) { + return $this->handleUnconditionally; + } + + $this->handleUnconditionally = (bool)$value; + } + + /** + * Adds an editor resolver, identified by a string + * name, and that may be a string path, or a callable + * resolver. If the callable returns a string, it will + * be set as the file reference's href attribute. + * + * @example + * $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line") + * @example + * $run->addEditor('remove-it', function($file, $line) { + * unlink($file); + * return "http://stackoverflow.com"; + * }); + * @param string $identifier + * @param string $resolver + */ + public function addEditor($identifier, $resolver) { + $this->editors[$identifier] = $resolver; + } + + /** + * Set the editor to use to open referenced files, by a string + * identifier, or a callable that will be executed for every + * file reference, with a $file and $line argument, and should + * return a string. + * + * @example + * $run->setEditor(function($file, $line) { return "file:///{$file}"; }); + * @example + * $run->setEditor('sublime'); + * + * @throws InvalidArgumentException If invalid argument identifier provided + * @param string|callable $editor + */ + public function setEditor($editor) { + if (!is_callable($editor) && !isset($this->editors[$editor])) { + throw new InvalidArgumentException( + "Unknown editor identifier: $editor. Known editors:" . + implode(",", array_keys($this->editors)) + ); + } + + $this->editor = $editor; + } + + /** + * Given a string file path, and an integer file line, + * executes the editor resolver and returns, if available, + * a string that may be used as the href property for that + * file reference. + * + * @throws InvalidArgumentException If editor resolver does not return a string + * @param string $filePath + * @param int $line + * @return string|bool + */ + public function getEditorHref($filePath, $line) { + $editor = $this->getEditor($filePath, $line); + + if (empty($editor)) { + return FALSE; + } + + // Check that the editor is a string, and replace the + // %line and %file placeholders: + if (!isset($editor['url']) || !is_string($editor['url'])) { + throw new UnexpectedValueException( + __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead." + ); + } + + $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']); + $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']); + + return $editor['url']; + } + + /** + * Given a boolean if the editor link should + * act as an Ajax request. The editor must be a + * valid callable function/closure + * + * @throws UnexpectedValueException If editor resolver does not return a boolean + * @param string $filePath + * @param int $line + * @return bool + */ + public function getEditorAjax($filePath, $line) { + $editor = $this->getEditor($filePath, $line); + + // Check that the ajax is a bool + if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) { + throw new UnexpectedValueException( + __METHOD__ . " should always resolve to a bool; got something else instead." + ); + } + return $editor['ajax']; + } + + /** + * @param string $title + * @return void + */ + public function setPageTitle($title) { + $this->pageTitle = (string)$title; + } + + /** + * @return string + */ + public function getPageTitle() { + return $this->pageTitle; + } + + /** + * Adds a path to the list of paths to be searched for + * resources. + * + * @throws InvalidArgumentException If $path is not a valid directory + * + * @param string $path + * @return void + */ + public function addResourcePath($path) { + if (!is_dir($path)) { + throw new InvalidArgumentException( + "'$path' is not a valid directory" + ); + } + + array_unshift($this->searchPaths, $path); + } + + /** + * Adds a custom css file to be loaded. + * + * @param string $name + * @return void + */ + public function addCustomCss($name) { + $this->customCss = $name; + } + + /** + * @return array + */ + public function getResourcePaths() { + return $this->searchPaths; + } + + /** + * @deprecated + * + * @return string + */ + public function getResourcesPath() { + $allPaths = $this->getResourcePaths(); + + // Compat: return only the first path added + return end($allPaths) ?: NULL; + } + + /** + * @deprecated + * + * @param string $resourcesPath + * @return void + */ + public function setResourcesPath($resourcesPath) { + $this->addResourcePath($resourcesPath); + } + + /** + * Return the application paths. + * + * @return array + */ + public function getApplicationPaths() { + return $this->applicationPaths; + } + + /** + * Set the application paths. + * + * @param array $applicationPaths + */ + public function setApplicationPaths($applicationPaths) { + $this->applicationPaths = $applicationPaths; + } + + /** + * Set the application root path. + * + * @param string $applicationRootPath + */ + public function setApplicationRootPath($applicationRootPath) { + $this->templateHelper->setApplicationRootPath($applicationRootPath); + } + + /** + * Given a boolean if the editor link should + * act as an Ajax request. The editor must be a + * valid callable function/closure + * + * @param string $filePath + * @param int $line + * @return array + */ + protected function getEditor($filePath, $line) { + if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) { + return []; + } + + if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) { + return [ + 'ajax' => FALSE, + 'url' => $this->editors[$this->editor], + ]; + } + + if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) { + if (is_callable($this->editor)) { + $callback = call_user_func($this->editor, $filePath, $line); + } else { + $callback = call_user_func($this->editors[$this->editor], $filePath, $line); + } + + if (is_string($callback)) { + return [ + 'ajax' => FALSE, + 'url' => $callback, + ]; + } + + return [ + 'ajax' => isset($callback['ajax']) ? $callback['ajax'] : FALSE, + 'url' => isset($callback['url']) ? $callback['url'] : $callback, + ]; + } + + return []; + } + + /** + * @return \Throwable + */ + protected function getException() { + return $this->exception; + } + + /** + * @return Inspector + */ + protected function getInspector() { + return $this->inspector; + } + + /** + * Finds a resource, by its relative path, in all available search paths. + * The search is performed starting at the last search path, and all the + * way back to the first, enabling a cascading-type system of overrides + * for all resources. + * + * @throws RuntimeException If resource cannot be found in any of the available paths + * + * @param string $resource + * @return string + */ + protected function getResource($resource) { + // If the resource was found before, we can speed things up + // by caching its absolute, resolved path: + if (isset($this->resourceCache[$resource])) { + return $this->resourceCache[$resource]; + } + + // Search through available search paths, until we find the + // resource we're after: + foreach ($this->searchPaths as $path) { + $fullPath = $path . "/$resource"; + + if (is_file($fullPath)) { + // Cache the result: + $this->resourceCache[$resource] = $fullPath; + return $fullPath; + } + } + + // If we got this far, nothing was found. + throw new RuntimeException( + "Could not find resource '$resource' in any resource paths." + . "(searched: " . join(", ", $this->searchPaths) . ")" + ); + } + + /** + * Checks all values within the given superGlobal array. + * Blacklisted values will be replaced by a equal length string cointaining only '*' characters. + * + * We intentionally dont rely on $GLOBALS as it depends on 'auto_globals_jit' php.ini setting. + * + * @param array $superGlobal One of the superglobal arrays + * @param string $superGlobalName the name of the superglobal array, e.g. '_GET' + * @return array $values without sensitive data + */ + private function masked(array $superGlobal, $superGlobalName) { + $blacklisted = $this->blacklist[$superGlobalName]; + + $values = $superGlobal; + foreach ($blacklisted as $key) { + if (isset($superGlobal[$key])) { + $values[$key] = str_repeat('*', strlen($superGlobal[$key])); + } + } + return $values; + } +} diff --git a/main/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php b/main/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php index 5c51d8d..8e17554 100644 --- a/main/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php +++ b/main/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php @@ -1,47 +1,47 @@ -exception; - $xml = "\n UserFrosting Application Error\n"; - if ($this->displayErrorDetails) { - do { - $xml .= " \n"; - $xml .= " " . get_class($e) . "\n"; - $xml .= " " . $e->getCode() . "\n"; - $xml .= " " . $this->createCdataSection($e->getMessage()) . "\n"; - $xml .= " " . $e->getFile() . "\n"; - $xml .= " " . $e->getLine() . "\n"; - $xml .= " \n"; - } while ($e = $e->getPrevious()); - } - $xml .= ""; - - return $xml; - } - - /** - * Returns a CDATA section with the given content. - * - * @param string $content - * @return string - */ - private function createCdataSection($content) { - return sprintf('', str_replace(']]>', ']]]]>', $content)); - } -} +exception; + $xml = "\n UserFrosting Application Error\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " \n"; + $xml .= " " . get_class($e) . "\n"; + $xml .= " " . $e->getCode() . "\n"; + $xml .= " " . $this->createCdataSection($e->getMessage()) . "\n"; + $xml .= " " . $e->getFile() . "\n"; + $xml .= " " . $e->getLine() . "\n"; + $xml .= " \n"; + } while ($e = $e->getPrevious()); + } + $xml .= ""; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) { + return sprintf('', str_replace(']]>', ']]]]>', $content)); + } +} diff --git a/main/app/sprinkles/core/src/Facades/Debug.php b/main/app/sprinkles/core/src/Facades/Debug.php index 40cc263..58e4302 100644 --- a/main/app/sprinkles/core/src/Facades/Debug.php +++ b/main/app/sprinkles/core/src/Facades/Debug.php @@ -1,28 +1,28 @@ -isXhr()) { - return 'text/html'; - } - - $acceptHeader = $request->getHeaderLine('Accept'); - $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); - $count = count($selectedContentTypes); - - if ($count) { - $current = current($selectedContentTypes); - - /** - * Ensure other supported content types take precedence over text/plain - * when multiple content types are provided via Accept header. - */ - if ($current === 'text/plain' && $count > 1) { - return next($selectedContentTypes); - } - - return $current; - } - - if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { - $mediaType = 'application/' . $matches[1]; - if (in_array($mediaType, $this->knownContentTypes)) { - return $mediaType; - } - } - - return 'text/html'; - } -} +isXhr()) { + return 'text/html'; + } + + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + $count = count($selectedContentTypes); + + if ($count) { + $current = current($selectedContentTypes); + + /** + * Ensure other supported content types take precedence over text/plain + * when multiple content types are provided via Accept header. + */ + if ($current === 'text/plain' && $count > 1) { + return next($selectedContentTypes); + } + + return $current; + } + + if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { + $mediaType = 'application/' . $matches[1]; + if (in_array($mediaType, $this->knownContentTypes)) { + return $mediaType; + } + } + + return 'text/html'; + } +} diff --git a/main/app/sprinkles/core/src/Log/DatabaseHandler.php b/main/app/sprinkles/core/src/Log/DatabaseHandler.php index d4c9fce..49eb5c2 100644 --- a/main/app/sprinkles/core/src/Log/DatabaseHandler.php +++ b/main/app/sprinkles/core/src/Log/DatabaseHandler.php @@ -1,52 +1,52 @@ -classMapper = $classMapper; - $this->modelName = $modelIdentifier; - parent::__construct($level, $bubble); - } - - /** - * {@inheritDoc} - */ - protected function write(array $record) { - $log = $this->classMapper->createInstance($this->modelName, $record['extra']); - $log->save(); - } -} +classMapper = $classMapper; + $this->modelName = $modelIdentifier; + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record) { + $log = $this->classMapper->createInstance($this->modelName, $record['extra']); + $log->save(); + } +} diff --git a/main/app/sprinkles/core/src/Log/MixedFormatter.php b/main/app/sprinkles/core/src/Log/MixedFormatter.php index ce21879..ef70268 100644 --- a/main/app/sprinkles/core/src/Log/MixedFormatter.php +++ b/main/app/sprinkles/core/src/Log/MixedFormatter.php @@ -1,58 +1,58 @@ -jsonEncodePretty($data); - } - - $json = $this->jsonEncodePretty($data); - - if ($json === FALSE) { - $json = $this->handleJsonError(json_last_error(), $data); - } - - return $json; - } - - /** - * @param mixed $data - * @return string JSON encoded data or null on failure - */ - private function jsonEncodePretty($data) { - if (version_compare(PHP_VERSION, '5.4.0', '>=')) { - return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - - return json_encode($data); - } -} +jsonEncodePretty($data); + } + + $json = $this->jsonEncodePretty($data); + + if ($json === FALSE) { + $json = $this->handleJsonError(json_last_error(), $data); + } + + return $json; + } + + /** + * @param mixed $data + * @return string JSON encoded data or null on failure + */ + private function jsonEncodePretty($data) { + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + + return json_encode($data); + } +} diff --git a/main/app/sprinkles/core/src/Mail/EmailRecipient.php b/main/app/sprinkles/core/src/Mail/EmailRecipient.php index 33b7db7..29b5de8 100644 --- a/main/app/sprinkles/core/src/Mail/EmailRecipient.php +++ b/main/app/sprinkles/core/src/Mail/EmailRecipient.php @@ -1,129 +1,129 @@ - value) to use when rendering an email template for this recipient. - */ - protected $params = []; - - /** - * @var array A list of CCs for this recipient. Each CC is an associative array with `email` and `name` properties. - */ - protected $cc = []; - - /** - * @var array A list of BCCs for this recipient. Each BCC is an associative array with `email` and `name` properties. - */ - protected $bcc = []; - - /** - * Create a new EmailRecipient instance. - * - * @param string $email The primary recipient email address. - * @param string $name The primary recipient name. - * @param array $params An array of template parameters to render the email message with for this particular recipient. - */ - public function __construct($email, $name = "", $params = []) { - $this->email = $email; - $this->name = $name; - $this->params = $params; - } - - /** - * Add a CC for this primary recipient. - * - * @param string $email The CC recipient email address. - * @param string $name The CC recipient name. - */ - public function cc($email, $name = "") { - $this->cc[] = [ - "email" => $email, - "name" => $name - ]; - } - - /** - * Add a BCC for this primary recipient. - * - * @param string $email The BCC recipient email address. - * @param string $name The BCC recipient name. - */ - public function bcc($email, $name = "") { - $this->bcc[] = [ - "email" => $email, - "name" => $name - ]; - } - - /** - * Get the primary recipient email address. - * - * @return string the primary recipient email address. - */ - public function getEmail() { - return $this->email; - } - - /** - * Get the primary recipient name. - * - * @return string the primary recipient name. - */ - public function getName() { - return $this->name; - } - - /** - * Get the parameters to use when rendering the template this recipient. - * - * @return array The parameters (name => value) to use when rendering an email template for this recipient. - */ - public function getParams() { - return $this->params; - } - - /** - * Get the list of CCs for this recipient. - * - * @return array A list of CCs for this recipient. Each CC is an associative array with `email` and `name` properties. - */ - public function getCCs() { - return $this->cc; - } - - /** - * Get the list of BCCs for this recipient. - * - * @return array A list of BCCs for this recipient. Each BCC is an associative array with `email` and `name` properties. - */ - public function getBCCs() { - return $this->bcc; - } -} + value) to use when rendering an email template for this recipient. + */ + protected $params = []; + + /** + * @var array A list of CCs for this recipient. Each CC is an associative array with `email` and `name` properties. + */ + protected $cc = []; + + /** + * @var array A list of BCCs for this recipient. Each BCC is an associative array with `email` and `name` properties. + */ + protected $bcc = []; + + /** + * Create a new EmailRecipient instance. + * + * @param string $email The primary recipient email address. + * @param string $name The primary recipient name. + * @param array $params An array of template parameters to render the email message with for this particular recipient. + */ + public function __construct($email, $name = "", $params = []) { + $this->email = $email; + $this->name = $name; + $this->params = $params; + } + + /** + * Add a CC for this primary recipient. + * + * @param string $email The CC recipient email address. + * @param string $name The CC recipient name. + */ + public function cc($email, $name = "") { + $this->cc[] = [ + "email" => $email, + "name" => $name + ]; + } + + /** + * Add a BCC for this primary recipient. + * + * @param string $email The BCC recipient email address. + * @param string $name The BCC recipient name. + */ + public function bcc($email, $name = "") { + $this->bcc[] = [ + "email" => $email, + "name" => $name + ]; + } + + /** + * Get the primary recipient email address. + * + * @return string the primary recipient email address. + */ + public function getEmail() { + return $this->email; + } + + /** + * Get the primary recipient name. + * + * @return string the primary recipient name. + */ + public function getName() { + return $this->name; + } + + /** + * Get the parameters to use when rendering the template this recipient. + * + * @return array The parameters (name => value) to use when rendering an email template for this recipient. + */ + public function getParams() { + return $this->params; + } + + /** + * Get the list of CCs for this recipient. + * + * @return array A list of CCs for this recipient. Each CC is an associative array with `email` and `name` properties. + */ + public function getCCs() { + return $this->cc; + } + + /** + * Get the list of BCCs for this recipient. + * + * @return array A list of BCCs for this recipient. Each BCC is an associative array with `email` and `name` properties. + */ + public function getBCCs() { + return $this->bcc; + } +} diff --git a/main/app/sprinkles/core/src/Mail/MailMessage.php b/main/app/sprinkles/core/src/Mail/MailMessage.php index 6aea56d..c16d1ad 100644 --- a/main/app/sprinkles/core/src/Mail/MailMessage.php +++ b/main/app/sprinkles/core/src/Mail/MailMessage.php @@ -1,175 +1,175 @@ -recipients[] = $recipient; - return $this; - } - - /** - * Clears out all recipients for this message. - */ - public function clearRecipients() { - $this->recipients = array(); - } - - /** - * Set sender information for this message. - * - * This is a shortcut for calling setFromEmail, setFromName, setReplyEmail, and setReplyName. - * @param string $fromInfo An array containing 'email', 'name', 'reply_email', and 'reply_name'. - */ - public function from($fromInfo = []) { - $this->setFromEmail(isset($fromInfo['email']) ? $fromInfo['email'] : ""); - $this->setFromName(isset($fromInfo['name']) ? $fromInfo['name'] : NULL); - $this->setReplyEmail(isset($fromInfo['reply_email']) ? $fromInfo['reply_email'] : NULL); - $this->setReplyName(isset($fromInfo['reply_name']) ? $fromInfo['reply_name'] : NULL); - - return $this; - } - - /** - * Get the sender email address. - * - * @return string - */ - public function getFromEmail() { - return $this->fromEmail; - } - - /** - * Get the sender name. Defaults to the email address if name is not set. - * - * @return string - */ - public function getFromName() { - return isset($this->fromName) ? $this->fromName : $this->getFromEmail(); - } - - /** - * Get the list of recipients for this message. - * - * @return EmailRecipient[] - */ - public function getRecipients() { - return $this->recipients; - } - - /** - * Get the 'reply-to' address for this message. Defaults to the sender email. - * - * @return string - */ - public function getReplyEmail() { - return isset($this->replyEmail) ? $this->replyEmail : $this->getFromEmail(); - } - - /** - * Get the 'reply-to' name for this message. Defaults to the sender name. - * - * @return string - */ - public function getReplyName() { - return isset($this->replyName) ? $this->replyName : $this->getFromName(); - } - - /** - * Set the sender email address. - * - * @param string $fromEmail - */ - public function setFromEmail($fromEmail) { - $this->fromEmail = $fromEmail; - return $this; - } - - /** - * Set the sender name. - * - * @param string $fromName - */ - public function setFromName($fromName) { - $this->fromName = $fromName; - return $this; - } - - /** - * Set the sender 'reply-to' address. - * - * @param string $replyEmail - */ - public function setReplyEmail($replyEmail) { - $this->replyEmail = $replyEmail; - return $this; - } - - /** - * Set the sender 'reply-to' name. - * - * @param string $replyName - */ - public function setReplyName($replyName) { - $this->replyName = $replyName; - return $this; - } -} +recipients[] = $recipient; + return $this; + } + + /** + * Clears out all recipients for this message. + */ + public function clearRecipients() { + $this->recipients = array(); + } + + /** + * Set sender information for this message. + * + * This is a shortcut for calling setFromEmail, setFromName, setReplyEmail, and setReplyName. + * @param string $fromInfo An array containing 'email', 'name', 'reply_email', and 'reply_name'. + */ + public function from($fromInfo = []) { + $this->setFromEmail(isset($fromInfo['email']) ? $fromInfo['email'] : ""); + $this->setFromName(isset($fromInfo['name']) ? $fromInfo['name'] : NULL); + $this->setReplyEmail(isset($fromInfo['reply_email']) ? $fromInfo['reply_email'] : NULL); + $this->setReplyName(isset($fromInfo['reply_name']) ? $fromInfo['reply_name'] : NULL); + + return $this; + } + + /** + * Get the sender email address. + * + * @return string + */ + public function getFromEmail() { + return $this->fromEmail; + } + + /** + * Get the sender name. Defaults to the email address if name is not set. + * + * @return string + */ + public function getFromName() { + return isset($this->fromName) ? $this->fromName : $this->getFromEmail(); + } + + /** + * Get the list of recipients for this message. + * + * @return EmailRecipient[] + */ + public function getRecipients() { + return $this->recipients; + } + + /** + * Get the 'reply-to' address for this message. Defaults to the sender email. + * + * @return string + */ + public function getReplyEmail() { + return isset($this->replyEmail) ? $this->replyEmail : $this->getFromEmail(); + } + + /** + * Get the 'reply-to' name for this message. Defaults to the sender name. + * + * @return string + */ + public function getReplyName() { + return isset($this->replyName) ? $this->replyName : $this->getFromName(); + } + + /** + * Set the sender email address. + * + * @param string $fromEmail + */ + public function setFromEmail($fromEmail) { + $this->fromEmail = $fromEmail; + return $this; + } + + /** + * Set the sender name. + * + * @param string $fromName + */ + public function setFromName($fromName) { + $this->fromName = $fromName; + return $this; + } + + /** + * Set the sender 'reply-to' address. + * + * @param string $replyEmail + */ + public function setReplyEmail($replyEmail) { + $this->replyEmail = $replyEmail; + return $this; + } + + /** + * Set the sender 'reply-to' name. + * + * @param string $replyName + */ + public function setReplyName($replyName) { + $this->replyName = $replyName; + return $this; + } +} diff --git a/main/app/sprinkles/core/src/Mail/Mailer.php b/main/app/sprinkles/core/src/Mail/Mailer.php index 761d15a..5331107 100644 --- a/main/app/sprinkles/core/src/Mail/Mailer.php +++ b/main/app/sprinkles/core/src/Mail/Mailer.php @@ -1,200 +1,200 @@ -logger = $logger; - - // 'true' tells PHPMailer to use exceptions instead of error codes - $this->phpMailer = new \PHPMailer(TRUE); - - // Configuration options - if (isset($config['mailer'])) { - if (!in_array($config['mailer'], ['smtp', 'mail', 'qmail', 'sendmail'])) { - throw new \phpmailerException("'mailer' must be one of 'smtp', 'mail', 'qmail', or 'sendmail'."); - } - - if ($config['mailer'] == 'smtp') { - $this->phpMailer->isSMTP(TRUE); - $this->phpMailer->Host = $config['host']; - $this->phpMailer->Port = $config['port']; - $this->phpMailer->SMTPAuth = $config['auth']; - $this->phpMailer->SMTPSecure = $config['secure']; - $this->phpMailer->Username = $config['username']; - $this->phpMailer->Password = $config['password']; - $this->phpMailer->SMTPDebug = $config['smtp_debug']; - - if (isset($config['smtp_options'])) { - $this->phpMailer->SMTPOptions = $config['smtp_options']; - } - } - - // Set any additional message-specific options - // enforce which options can be set through this subarray - if (isset($config['message_options'])) { - $this->setOptions($config['message_options']); - } - } - - // Pass logger into phpMailer object - $this->phpMailer->Debugoutput = function ($message, $level) { - $this->logger->debug($message); - }; - } - - /** - * Get the underlying PHPMailer object. - * - * @return \PHPMailer - */ - public function getPhpMailer() { - return $this->phpMailer; - } - - /** - * Send a MailMessage message. - * - * Sends a single email to all recipients, as well as their CCs and BCCs. - * Since it is a single-header message, recipient-specific template data will not be included. - * @param MailMessage $message - * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. - * @throws \phpmailerException The message could not be sent. - */ - public function send(MailMessage $message, $clearRecipients = TRUE) { - $this->phpMailer->From = $message->getFromEmail(); - $this->phpMailer->FromName = $message->getFromName(); - $this->phpMailer->addReplyTo($message->getReplyEmail(), $message->getReplyName()); - - // Add all email recipients, as well as their CCs and BCCs - foreach ($message->getRecipients() as $recipient) { - $this->phpMailer->addAddress($recipient->getEmail(), $recipient->getName()); - - // Add any CCs and BCCs - if ($recipient->getCCs()) { - foreach ($recipient->getCCs() as $cc) { - $this->phpMailer->addCC($cc['email'], $cc['name']); - } - } - - if ($recipient->getBCCs()) { - foreach ($recipient->getBCCs() as $bcc) { - $this->phpMailer->addBCC($bcc['email'], $bcc['name']); - } - } - } - - $this->phpMailer->Subject = $message->renderSubject(); - $this->phpMailer->Body = $message->renderBody(); - - // Try to send the mail. Will throw an exception on failure. - $this->phpMailer->send(); - - // Clear recipients from the PHPMailer object for this iteration, - // so that we can use the same object for other emails. - $this->phpMailer->clearAllRecipients(); - - // Clear out the MailMessage's internal recipient list - if ($clearRecipients) { - $message->clearRecipients(); - } - } - - /** - * Send a MailMessage message, sending a separate email to each recipient. - * - * If the message object supports message templates, this will render the template with the corresponding placeholder values for each recipient. - * @param MailMessage $message - * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. - * @throws \phpmailerException The message could not be sent. - */ - public function sendDistinct(MailMessage $message, $clearRecipients = TRUE) { - $this->phpMailer->From = $message->getFromEmail(); - $this->phpMailer->FromName = $message->getFromName(); - $this->phpMailer->addReplyTo($message->getReplyEmail(), $message->getReplyName()); - - // Loop through email recipients, sending customized content to each one - foreach ($message->getRecipients() as $recipient) { - $this->phpMailer->addAddress($recipient->getEmail(), $recipient->getName()); - - // Add any CCs and BCCs - if ($recipient->getCCs()) { - foreach ($recipient->getCCs() as $cc) { - $this->phpMailer->addCC($cc['email'], $cc['name']); - } - } - - if ($recipient->getBCCs()) { - foreach ($recipient->getBCCs() as $bcc) { - $this->phpMailer->addBCC($bcc['email'], $bcc['name']); - } - } - - $this->phpMailer->Subject = $message->renderSubject($recipient->getParams()); - $this->phpMailer->Body = $message->renderBody($recipient->getParams()); - - // Try to send the mail. Will throw an exception on failure. - $this->phpMailer->send(); - - // Clear recipients from the PHPMailer object for this iteration, - // so that we can send a separate email to the next recipient. - $this->phpMailer->clearAllRecipients(); - } - - // Clear out the MailMessage's internal recipient list - if ($clearRecipients) { - $message->clearRecipients(); - } - } - - /** - * Set option(s) on the underlying phpMailer object. - * - * @param mixed[] $options - * @return Mailer - */ - public function setOptions($options) { - if (isset($options['isHtml'])) { - $this->phpMailer->isHTML($options['isHtml']); - } - - foreach ($options as $name => $value) { - $this->phpMailer->set($name, $value); - } - - return $this; - } -} +logger = $logger; + + // 'true' tells PHPMailer to use exceptions instead of error codes + $this->phpMailer = new \PHPMailer(TRUE); + + // Configuration options + if (isset($config['mailer'])) { + if (!in_array($config['mailer'], ['smtp', 'mail', 'qmail', 'sendmail'])) { + throw new \phpmailerException("'mailer' must be one of 'smtp', 'mail', 'qmail', or 'sendmail'."); + } + + if ($config['mailer'] == 'smtp') { + $this->phpMailer->isSMTP(TRUE); + $this->phpMailer->Host = $config['host']; + $this->phpMailer->Port = $config['port']; + $this->phpMailer->SMTPAuth = $config['auth']; + $this->phpMailer->SMTPSecure = $config['secure']; + $this->phpMailer->Username = $config['username']; + $this->phpMailer->Password = $config['password']; + $this->phpMailer->SMTPDebug = $config['smtp_debug']; + + if (isset($config['smtp_options'])) { + $this->phpMailer->SMTPOptions = $config['smtp_options']; + } + } + + // Set any additional message-specific options + // enforce which options can be set through this subarray + if (isset($config['message_options'])) { + $this->setOptions($config['message_options']); + } + } + + // Pass logger into phpMailer object + $this->phpMailer->Debugoutput = function ($message, $level) { + $this->logger->debug($message); + }; + } + + /** + * Get the underlying PHPMailer object. + * + * @return \PHPMailer + */ + public function getPhpMailer() { + return $this->phpMailer; + } + + /** + * Send a MailMessage message. + * + * Sends a single email to all recipients, as well as their CCs and BCCs. + * Since it is a single-header message, recipient-specific template data will not be included. + * @param MailMessage $message + * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. + * @throws \phpmailerException The message could not be sent. + */ + public function send(MailMessage $message, $clearRecipients = TRUE) { + $this->phpMailer->From = $message->getFromEmail(); + $this->phpMailer->FromName = $message->getFromName(); + $this->phpMailer->addReplyTo($message->getReplyEmail(), $message->getReplyName()); + + // Add all email recipients, as well as their CCs and BCCs + foreach ($message->getRecipients() as $recipient) { + $this->phpMailer->addAddress($recipient->getEmail(), $recipient->getName()); + + // Add any CCs and BCCs + if ($recipient->getCCs()) { + foreach ($recipient->getCCs() as $cc) { + $this->phpMailer->addCC($cc['email'], $cc['name']); + } + } + + if ($recipient->getBCCs()) { + foreach ($recipient->getBCCs() as $bcc) { + $this->phpMailer->addBCC($bcc['email'], $bcc['name']); + } + } + } + + $this->phpMailer->Subject = $message->renderSubject(); + $this->phpMailer->Body = $message->renderBody(); + + // Try to send the mail. Will throw an exception on failure. + $this->phpMailer->send(); + + // Clear recipients from the PHPMailer object for this iteration, + // so that we can use the same object for other emails. + $this->phpMailer->clearAllRecipients(); + + // Clear out the MailMessage's internal recipient list + if ($clearRecipients) { + $message->clearRecipients(); + } + } + + /** + * Send a MailMessage message, sending a separate email to each recipient. + * + * If the message object supports message templates, this will render the template with the corresponding placeholder values for each recipient. + * @param MailMessage $message + * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. + * @throws \phpmailerException The message could not be sent. + */ + public function sendDistinct(MailMessage $message, $clearRecipients = TRUE) { + $this->phpMailer->From = $message->getFromEmail(); + $this->phpMailer->FromName = $message->getFromName(); + $this->phpMailer->addReplyTo($message->getReplyEmail(), $message->getReplyName()); + + // Loop through email recipients, sending customized content to each one + foreach ($message->getRecipients() as $recipient) { + $this->phpMailer->addAddress($recipient->getEmail(), $recipient->getName()); + + // Add any CCs and BCCs + if ($recipient->getCCs()) { + foreach ($recipient->getCCs() as $cc) { + $this->phpMailer->addCC($cc['email'], $cc['name']); + } + } + + if ($recipient->getBCCs()) { + foreach ($recipient->getBCCs() as $bcc) { + $this->phpMailer->addBCC($bcc['email'], $bcc['name']); + } + } + + $this->phpMailer->Subject = $message->renderSubject($recipient->getParams()); + $this->phpMailer->Body = $message->renderBody($recipient->getParams()); + + // Try to send the mail. Will throw an exception on failure. + $this->phpMailer->send(); + + // Clear recipients from the PHPMailer object for this iteration, + // so that we can send a separate email to the next recipient. + $this->phpMailer->clearAllRecipients(); + } + + // Clear out the MailMessage's internal recipient list + if ($clearRecipients) { + $message->clearRecipients(); + } + } + + /** + * Set option(s) on the underlying phpMailer object. + * + * @param mixed[] $options + * @return Mailer + */ + public function setOptions($options) { + if (isset($options['isHtml'])) { + $this->phpMailer->isHTML($options['isHtml']); + } + + foreach ($options as $name => $value) { + $this->phpMailer->set($name, $value); + } + + return $this; + } +} diff --git a/main/app/sprinkles/core/src/Mail/StaticMailMessage.php b/main/app/sprinkles/core/src/Mail/StaticMailMessage.php index 482226c..17758db 100644 --- a/main/app/sprinkles/core/src/Mail/StaticMailMessage.php +++ b/main/app/sprinkles/core/src/Mail/StaticMailMessage.php @@ -1,74 +1,74 @@ -subject = $subject; - $this->body = $body; - } - - /** - * {@inheritDoc} - */ - public function renderBody($params = []) { - return $this->body; - } - - /** - * {@inheritDoc} - */ - public function renderSubject($params = []) { - return $this->subject; - } - - /** - * Set the text of the message subject. - * - * @param string $subject - */ - public function setSubject($subject) { - $this->subject = $subject; - return $this; - } - - /** - * Set the text of the message body. - * - * @param string $body - */ - public function setBody($body) { - $this->body = $body; - return $this; - } -} +subject = $subject; + $this->body = $body; + } + + /** + * {@inheritDoc} + */ + public function renderBody($params = []) { + return $this->body; + } + + /** + * {@inheritDoc} + */ + public function renderSubject($params = []) { + return $this->subject; + } + + /** + * Set the text of the message subject. + * + * @param string $subject + */ + public function setSubject($subject) { + $this->subject = $subject; + return $this; + } + + /** + * Set the text of the message body. + * + * @param string $body + */ + public function setBody($body) { + $this->body = $body; + return $this; + } +} diff --git a/main/app/sprinkles/core/src/Mail/TwigMailMessage.php b/main/app/sprinkles/core/src/Mail/TwigMailMessage.php index aa4daea..7197f75 100644 --- a/main/app/sprinkles/core/src/Mail/TwigMailMessage.php +++ b/main/app/sprinkles/core/src/Mail/TwigMailMessage.php @@ -1,89 +1,89 @@ -view = $view; - - $twig = $this->view->getEnvironment(); - // Must manually merge in global variables for block rendering - // should we keep this separate from the local parameters? - $this->params = $twig->getGlobals(); - - if ($filename !== NULL) { - $this->template = $twig->loadTemplate($filename); - } - } - - /** - * Merge in any additional global Twig variables to use when rendering this message. - * - * @param mixed[] $params - */ - public function addParams($params = []) { - $this->params = array_replace_recursive($this->params, $params); - return $this; - } - - /** - * {@inheritDoc} - */ - public function renderSubject($params = []) { - $params = array_replace_recursive($this->params, $params); - return $this->template->renderBlock('subject', $params); - } - - /** - * {@inheritDoc} - */ - public function renderBody($params = []) { - $params = array_replace_recursive($this->params, $params); - return $this->template->renderBlock('body', $params); - } - - /** - * Sets the Twig template object for this message. - * - * @param Twig_Template $template The Twig template object, to source the content for this message. - */ - public function setTemplate($template) { - $this->template = $template; - return $this; - } -} +view = $view; + + $twig = $this->view->getEnvironment(); + // Must manually merge in global variables for block rendering + // should we keep this separate from the local parameters? + $this->params = $twig->getGlobals(); + + if ($filename !== NULL) { + $this->template = $twig->loadTemplate($filename); + } + } + + /** + * Merge in any additional global Twig variables to use when rendering this message. + * + * @param mixed[] $params + */ + public function addParams($params = []) { + $this->params = array_replace_recursive($this->params, $params); + return $this; + } + + /** + * {@inheritDoc} + */ + public function renderSubject($params = []) { + $params = array_replace_recursive($this->params, $params); + return $this->template->renderBlock('subject', $params); + } + + /** + * {@inheritDoc} + */ + public function renderBody($params = []) { + $params = array_replace_recursive($this->params, $params); + return $this->template->renderBlock('body', $params); + } + + /** + * Sets the Twig template object for this message. + * + * @param Twig_Template $template The Twig template object, to source the content for this message. + */ + public function setTemplate($template) { + $this->template = $template; + return $this; + } +} diff --git a/main/app/sprinkles/core/src/Model/UFModel.php b/main/app/sprinkles/core/src/Model/UFModel.php index d852606..fb01357 100644 --- a/main/app/sprinkles/core/src/Model/UFModel.php +++ b/main/app/sprinkles/core/src/Model/UFModel.php @@ -1,27 +1,27 @@ -routeGroups) { - $pattern = $this->processGroups() . $pattern; - } - - // According to RFC methods are defined in uppercase (See RFC 7231) - $methods = array_map("strtoupper", $methods); - - // Determine route signature - $signature = implode('-', $methods) . '-' . $pattern; - - // If a route with the same signature already exists, then we must replace it - if (isset($this->identifiers[$signature])) { - $route = new \Slim\Route($methods, $pattern, $handler, $this->routeGroups, str_replace('route', '', $this->identifiers[$signature])); - } else { - $route = new \Slim\Route($methods, $pattern, $handler, $this->routeGroups, $this->routeCounter); - } - - $this->routes[$route->getIdentifier()] = $route; - - // Record identifier in reverse lookup array - $this->identifiers[$signature] = $route->getIdentifier(); - - $this->routeCounter++; - - return $route; - } - - /** - * Delete the cache file - * - * @access public - * @return bool true/false if operation is successfull - */ - public function clearCache() { - // Get Filesystem instance - $fs = new FileSystem; - - // Make sure file exist and delete it - if ($fs->exists($this->cacheFile)) { - return $fs->delete($this->cacheFile); - } - - // It's still considered a success if file doesn't exist - return TRUE; - } -} +routeGroups) { + $pattern = $this->processGroups() . $pattern; + } + + // According to RFC methods are defined in uppercase (See RFC 7231) + $methods = array_map("strtoupper", $methods); + + // Determine route signature + $signature = implode('-', $methods) . '-' . $pattern; + + // If a route with the same signature already exists, then we must replace it + if (isset($this->identifiers[$signature])) { + $route = new \Slim\Route($methods, $pattern, $handler, $this->routeGroups, str_replace('route', '', $this->identifiers[$signature])); + } else { + $route = new \Slim\Route($methods, $pattern, $handler, $this->routeGroups, $this->routeCounter); + } + + $this->routes[$route->getIdentifier()] = $route; + + // Record identifier in reverse lookup array + $this->identifiers[$signature] = $route->getIdentifier(); + + $this->routeCounter++; + + return $route; + } + + /** + * Delete the cache file + * + * @access public + * @return bool true/false if operation is successfull + */ + public function clearCache() { + // Get Filesystem instance + $fs = new FileSystem; + + // Make sure file exist and delete it + if ($fs->exists($this->cacheFile)) { + return $fs->delete($this->cacheFile); + } + + // It's still considered a success if file doesn't exist + return TRUE; + } +} diff --git a/main/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php b/main/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php index 6ac8c41..0adc75a 100644 --- a/main/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php +++ b/main/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php @@ -1,621 +1,621 @@ -config; - - if ($config['alert.storage'] == 'cache') { - return new CacheAlertStream($config['alert.key'], $c->translator, $c->cache, $c->config); - } else if ($config['alert.storage'] == 'session') { - return new SessionAlertStream($config['alert.key'], $c->translator, $c->session); - } else { - throw new \Exception("Bad alert storage handler type '{$config['alert.storage']}' specified in configuration file."); - } - }; - - /** - * Asset loader service. - * - * Loads assets from a specified relative location. - * Assets are Javascript, CSS, image, and other files used by your site. - */ - $container['assetLoader'] = function ($c) { - $basePath = \UserFrosting\SPRINKLES_DIR; - $pattern = "/^[A-Za-z0-9_\-]+\/assets\//"; - - $al = new AssetLoader($basePath, $pattern); - return $al; - }; - - /** - * Asset manager service. - * - * Loads raw or compiled asset information from your bundle.config.json schema file. - * Assets are Javascript, CSS, image, and other files used by your site. - */ - $container['assets'] = function ($c) { - $config = $c->config; - $locator = $c->locator; - - // Load asset schema - if ($config['assets.use_raw']) { - $baseUrl = $config['site.uri.public'] . '/' . $config['assets.raw.path']; - $removePrefix = \UserFrosting\APP_DIR_NAME . \UserFrosting\DS . \UserFrosting\SPRINKLES_DIR_NAME; - $aub = new AssetUrlBuilder($locator, $baseUrl, $removePrefix, 'assets'); - - $as = new AssetBundleSchema($aub); - - // Load Sprinkle assets - $sprinkles = $c->sprinkleManager->getSprinkleNames(); - - // move this out into PathBuilder and Loader classes in userfrosting/assets - // This would also allow us to define and load bundles in themes - $bundleSchemas = array_reverse($locator->findResources('sprinkles://' . $config['assets.raw.schema'], TRUE, TRUE)); - - foreach ($bundleSchemas as $schema) { - if (file_exists($schema)) { - $as->loadRawSchemaFile($schema); - } - } - } else { - $baseUrl = $config['site.uri.public'] . '/' . $config['assets.compiled.path']; - $aub = new CompiledAssetUrlBuilder($baseUrl); - - $as = new AssetBundleSchema($aub); - $as->loadCompiledSchemaFile($locator->findResource("build://" . $config['assets.compiled.schema'], TRUE, TRUE)); - } - - $am = new AssetManager($aub, $as); - - return $am; - }; - - /** - * Cache service. - * - * @return \Illuminate\Cache\Repository - */ - $container['cache'] = function ($c) { - - $config = $c->config; - - if ($config['cache.driver'] == 'file') { - $path = $c->locator->findResource('cache://', TRUE, TRUE); - $cacheStore = new TaggableFileStore($path); - } else if ($config['cache.driver'] == 'memcached') { - // We need to inject the prefix in the memcached config - $config = array_merge($config['cache.memcached'], ['prefix' => $config['cache.prefix']]); - $cacheStore = new MemcachedStore($config); - } else if ($config['cache.driver'] == 'redis') { - // We need to inject the prefix in the redis config - $config = array_merge($config['cache.redis'], ['prefix' => $config['cache.prefix']]); - $cacheStore = new RedisStore($config); - } else { - throw new \Exception("Bad cache store type '{$config['cache.driver']}' specified in configuration file."); - } - - return $cacheStore->instance(); - }; - - /** - * Middleware to check environment. - * - * We should cache the results of this, the first time that it succeeds. - */ - $container['checkEnvironment'] = function ($c) { - $checkEnvironment = new CheckEnvironment($c->view, $c->locator, $c->cache); - return $checkEnvironment; - }; - - /** - * Class mapper. - * - * Creates an abstraction on top of class names to allow extending them in sprinkles. - */ - $container['classMapper'] = function ($c) { - $classMapper = new ClassMapper(); - $classMapper->setClassMapping('query_builder', 'UserFrosting\Sprinkle\Core\Database\Builder'); - $classMapper->setClassMapping('throttle', 'UserFrosting\Sprinkle\Core\Database\Models\Throttle'); - return $classMapper; - }; - - /** - * Site config service (separate from Slim settings). - * - * Will attempt to automatically determine which config file(s) to use based on the value of the UF_MODE environment variable. - */ - $container['config'] = function ($c) { - // Grab any relevant dotenv variables from the .env file - try { - $dotenv = new Dotenv(\UserFrosting\APP_DIR); - $dotenv->load(); - } catch (InvalidPathException $e) { - // Skip loading the environment config file if it doesn't exist. - } - - // Get configuration mode from environment - $mode = getenv('UF_MODE') ?: ''; - - // Construct and load config repository - $builder = new ConfigPathBuilder($c->locator, 'config://'); - $loader = new ArrayFileLoader($builder->buildPaths($mode)); - $config = new Repository($loader->load()); - - // Construct base url from components, if not explicitly specified - if (!isset($config['site.uri.public'])) { - $base_uri = $config['site.uri.base']; - - $public = new Uri( - $base_uri['scheme'], - $base_uri['host'], - $base_uri['port'], - $base_uri['path'] - ); - - // Slim\Http\Uri likes to add trailing slashes when the path is empty, so this fixes that. - $config['site.uri.public'] = trim($public, '/'); - } - - // Hacky fix to prevent sessions from being hit too much: ignore CSRF middleware for requests for raw assets ;-) - // See https://github.com/laravel/framework/issues/8172#issuecomment-99112012 for more information on why it's bad to hit Laravel sessions multiple times in rapid succession. - $csrfBlacklist = $config['csrf.blacklist']; - $csrfBlacklist['^/' . $config['assets.raw.path']] = [ - 'GET' - ]; - $csrfBlacklist['^/wormhole'] = [ - 'POST' - ]; - - $config->set('csrf.blacklist', $csrfBlacklist); - - return $config; - }; - - /** - * Initialize CSRF guard middleware. - * - * @see https://github.com/slimphp/Slim-Csrf - */ - $container['csrf'] = function ($c) { - $csrfKey = $c->config['session.keys.csrf']; - - // Workaround so that we can pass storage into CSRF guard. - // If we tried to directly pass the indexed portion of `session` (for example, $c->session['site.csrf']), - // we would get an 'Indirect modification of overloaded element of UserFrosting\Session\Session' error. - // If we tried to assign an array and use that, PHP would only modify the local variable, and not the session. - // Since ArrayObject is an object, PHP will modify the object itself, allowing it to persist in the session. - if (!$c->session->has($csrfKey)) { - $c->session[$csrfKey] = new \ArrayObject(); - } - $csrfStorage = $c->session[$csrfKey]; - - $onFailure = function ($request, $response, $next) { - $e = new BadRequestException("The CSRF code was invalid or not provided."); - $e->addUserMessage('CSRF_MISSING'); - throw $e; - - return $next($request, $response); - }; - - return new Guard($c->config['csrf.name'], $csrfStorage, $onFailure, $c->config['csrf.storage_limit'], $c->config['csrf.strength'], $c->config['csrf.persistent_token']); - }; - - /** - * Initialize Eloquent Capsule, which provides the database layer for UF. - * - * construct the individual objects rather than using the facade - */ - $container['db'] = function ($c) { - $config = $c->config; - - $capsule = new Capsule; - - foreach ($config['db'] as $name => $dbConfig) { - $capsule->addConnection($dbConfig, $name); - } - - $queryEventDispatcher = new Dispatcher(new Container); - - $capsule->setEventDispatcher($queryEventDispatcher); - - // Register as global connection - $capsule->setAsGlobal(); - - // Start Eloquent - $capsule->bootEloquent(); - - if ($config['debug.queries']) { - $logger = $c->queryLogger; - - foreach ($config['db'] as $name => $dbConfig) { - $capsule->connection($name)->enableQueryLog(); - } - - // Register listener - $queryEventDispatcher->listen(QueryExecuted::class, function ($query) use ($logger) { - $logger->debug("Query executed on database [{$query->connectionName}]:", [ - 'query' => $query->sql, - 'bindings' => $query->bindings, - 'time' => $query->time . ' ms' - ]); - }); - } - - return $capsule; - }; - - /** - * Debug logging with Monolog. - * - * Extend this service to push additional handlers onto the 'debug' log stack. - */ - $container['debugLogger'] = function ($c) { - $logger = new Logger('debug'); - - $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); - - $handler = new StreamHandler($logFile); - - $formatter = new MixedFormatter(NULL, NULL, TRUE); - - $handler->setFormatter($formatter); - $logger->pushHandler($handler); - - return $logger; - }; - - /** - * Custom error-handler for recoverable errors. - */ - $container['errorHandler'] = function ($c) { - $settings = $c->settings; - - $handler = new ExceptionHandlerManager($c, $settings['displayErrorDetails']); - - // Register the base HttpExceptionHandler. - $handler->registerHandler('\UserFrosting\Support\Exception\HttpException', '\UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler'); - - // Register the NotFoundExceptionHandler. - $handler->registerHandler('\UserFrosting\Support\Exception\NotFoundException', '\UserFrosting\Sprinkle\Core\Error\Handler\NotFoundExceptionHandler'); - - // Register the PhpMailerExceptionHandler. - $handler->registerHandler('\phpmailerException', '\UserFrosting\Sprinkle\Core\Error\Handler\PhpMailerExceptionHandler'); - - return $handler; - }; - - /** - * Error logging with Monolog. - * - * Extend this service to push additional handlers onto the 'error' log stack. - */ - $container['errorLogger'] = function ($c) { - $log = new Logger('errors'); - - $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); - - $handler = new StreamHandler($logFile, Logger::WARNING); - - $formatter = new LineFormatter(NULL, NULL, TRUE); - - $handler->setFormatter($formatter); - $log->pushHandler($handler); - - return $log; - }; - - /** - * Factory service with FactoryMuffin. - * - * Provide access to factories for the rapid creation of objects for the purpose of testing - */ - $container['factory'] = function ($c) { - - // Get the path of all of the sprinkle's factories - $factoriesPath = $c->locator->findResources('factories://', TRUE, TRUE); - - // Create a new Factory Muffin instance - $fm = new FactoryMuffin(); - - // Load all of the model definitions - $fm->loadFactories($factoriesPath); - - // Set the locale. Could be the config one, but for testing English should do - Faker::setLocale('en_EN'); - - return $fm; - }; - - /** - * Builds search paths for locales in all Sprinkles. - */ - $container['localePathBuilder'] = function ($c) { - $config = $c->config; - - // Make sure the locale config is a valid string - if (!is_string($config['site.locales.default']) || $config['site.locales.default'] == '') { - throw new \UnexpectedValueException('The locale config is not a valid string.'); - } - - // Load the base locale file(s) as specified in the configuration - $locales = explode(',', $config['site.locales.default']); - - return new LocalePathBuilder($c->locator, 'locale://', $locales); - }; - - /** - * Mail service. - */ - $container['mailer'] = function ($c) { - $mailer = new Mailer($c->mailLogger, $c->config['mail']); - - // Use UF debug settings to override any service-specific log settings. - if (!$c->config['debug.smtp']) { - $mailer->getPhpMailer()->SMTPDebug = 0; - } - - return $mailer; - }; - - /** - * Mail logging service. - * - * PHPMailer will use this to log SMTP activity. - * Extend this service to push additional handlers onto the 'mail' log stack. - */ - $container['mailLogger'] = function ($c) { - $log = new Logger('mail'); - - $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); - - $handler = new StreamHandler($logFile); - $formatter = new LineFormatter(NULL, NULL, TRUE); - - $handler->setFormatter($formatter); - $log->pushHandler($handler); - - return $log; - }; - - /** - * Error-handler for 404 errors. Notice that we manually create a UserFrosting NotFoundException, - * and a NotFoundExceptionHandler. This lets us pass through to the UF error handling system. - */ - $container['notFoundHandler'] = function ($c) { - return function ($request, $response) use ($c) { - $exception = new NotFoundException; - $handler = new NotFoundExceptionHandler($c, $request, $response, $exception, $c->settings['displayErrorDetails']); - return $handler->handle(); - }; - }; - - /** - * Error-handler for PHP runtime errors. Notice that we just pass this through to our general-purpose - * error-handling service. - */ - $container['phpErrorHandler'] = function ($c) { - return $c->errorHandler; - }; - - /** - * Laravel query logging with Monolog. - * - * Extend this service to push additional handlers onto the 'query' log stack. - */ - $container['queryLogger'] = function ($c) { - $logger = new Logger('query'); - - $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); - - $handler = new StreamHandler($logFile); - - $formatter = new MixedFormatter(NULL, NULL, TRUE); - - $handler->setFormatter($formatter); - $logger->pushHandler($handler); - - return $logger; - }; - - /** - * Override Slim's default router with the UF router. - */ - $container['router'] = function ($c) { - $routerCacheFile = FALSE; - if (isset($c->config['settings.routerCacheFile'])) { - $routerCacheFile = $c->config['settings.routerCacheFile']; - } - - return (new Router)->setCacheFile($routerCacheFile); - }; - - /** - * Start the PHP session, with the name and parameters specified in the configuration file. - */ - $container['session'] = function ($c) { - $config = $c->config; - - // Create appropriate handler based on config - if ($config['session.handler'] == 'file') { - $fs = new FileSystem; - $handler = new FileSessionHandler($fs, $c->locator->findResource('session://'), $config['session.minutes']); - } else if ($config['session.handler'] == 'database') { - $connection = $c->db->connection(); - // Table must exist, otherwise an exception will be thrown - $handler = new DatabaseSessionHandler($connection, $config['session.database.table'], $config['session.minutes']); - } else { - throw new \Exception("Bad session handler type '{$config['session.handler']}' specified in configuration file."); - } - - // Create, start and return a new wrapper for $_SESSION - $session = new Session($handler, $config['session']); - $session->start(); - - return $session; - }; - - /** - * Request throttler. - * - * Throttles (rate-limits) requests of a predefined type, with rules defined in site config. - */ - $container['throttler'] = function ($c) { - $throttler = new Throttler($c->classMapper); - - $config = $c->config; - - if ($config->has('throttles') && ($config['throttles'] !== NULL)) { - foreach ($config['throttles'] as $type => $rule) { - if ($rule) { - $throttleRule = new ThrottleRule($rule['method'], $rule['interval'], $rule['delays']); - $throttler->addThrottleRule($type, $throttleRule); - } else { - $throttler->addThrottleRule($type, NULL); - } - } - } - - return $throttler; - }; - - /** - * Translation service, for translating message tokens. - */ - $container['translator'] = function ($c) { - // Load the translations - $paths = $c->localePathBuilder->buildPaths(); - $loader = new ArrayFileLoader($paths); - - // Create the $translator object - $translator = new MessageTranslator($loader->load()); - - return $translator; - }; - - /** - * Set up Twig as the view, adding template paths for all sprinkles and the Slim Twig extension. - * - * Also adds the UserFrosting core Twig extension, which provides additional functions, filters, global variables, etc. - */ - $container['view'] = function ($c) { - $templatePaths = $c->locator->findResources('templates://', TRUE, TRUE); - - $view = new Twig($templatePaths); - - $loader = $view->getLoader(); - - $sprinkles = $c->sprinkleManager->getSprinkleNames(); - - // Add Sprinkles' templates namespaces - foreach ($sprinkles as $sprinkle) { - $path = \UserFrosting\SPRINKLES_DIR . \UserFrosting\DS . - $sprinkle . \UserFrosting\DS . - \UserFrosting\TEMPLATE_DIR_NAME . \UserFrosting\DS; - - if (is_dir($path)) { - $loader->addPath($path, $sprinkle); - } - } - - $twig = $view->getEnvironment(); - - if ($c->config['cache.twig']) { - $twig->setCache($c->locator->findResource('cache://twig', TRUE, TRUE)); - } - - if ($c->config['debug.twig']) { - $twig->enableDebug(); - $view->addExtension(new \Twig_Extension_Debug()); - } - - // Register the Slim extension with Twig - $slimExtension = new TwigExtension( - $c->router, - $c->request->getUri() - ); - $view->addExtension($slimExtension); - - // Register the core UF extension with Twig - $coreExtension = new CoreExtension($c); - $view->addExtension($coreExtension); - - return $view; - }; - } -} +config; + + if ($config['alert.storage'] == 'cache') { + return new CacheAlertStream($config['alert.key'], $c->translator, $c->cache, $c->config); + } else if ($config['alert.storage'] == 'session') { + return new SessionAlertStream($config['alert.key'], $c->translator, $c->session); + } else { + throw new \Exception("Bad alert storage handler type '{$config['alert.storage']}' specified in configuration file."); + } + }; + + /** + * Asset loader service. + * + * Loads assets from a specified relative location. + * Assets are Javascript, CSS, image, and other files used by your site. + */ + $container['assetLoader'] = function ($c) { + $basePath = \UserFrosting\SPRINKLES_DIR; + $pattern = "/^[A-Za-z0-9_\-]+\/assets\//"; + + $al = new AssetLoader($basePath, $pattern); + return $al; + }; + + /** + * Asset manager service. + * + * Loads raw or compiled asset information from your bundle.config.json schema file. + * Assets are Javascript, CSS, image, and other files used by your site. + */ + $container['assets'] = function ($c) { + $config = $c->config; + $locator = $c->locator; + + // Load asset schema + if ($config['assets.use_raw']) { + $baseUrl = $config['site.uri.public'] . '/' . $config['assets.raw.path']; + $removePrefix = \UserFrosting\APP_DIR_NAME . \UserFrosting\DS . \UserFrosting\SPRINKLES_DIR_NAME; + $aub = new AssetUrlBuilder($locator, $baseUrl, $removePrefix, 'assets'); + + $as = new AssetBundleSchema($aub); + + // Load Sprinkle assets + $sprinkles = $c->sprinkleManager->getSprinkleNames(); + + // move this out into PathBuilder and Loader classes in userfrosting/assets + // This would also allow us to define and load bundles in themes + $bundleSchemas = array_reverse($locator->findResources('sprinkles://' . $config['assets.raw.schema'], TRUE, TRUE)); + + foreach ($bundleSchemas as $schema) { + if (file_exists($schema)) { + $as->loadRawSchemaFile($schema); + } + } + } else { + $baseUrl = $config['site.uri.public'] . '/' . $config['assets.compiled.path']; + $aub = new CompiledAssetUrlBuilder($baseUrl); + + $as = new AssetBundleSchema($aub); + $as->loadCompiledSchemaFile($locator->findResource("build://" . $config['assets.compiled.schema'], TRUE, TRUE)); + } + + $am = new AssetManager($aub, $as); + + return $am; + }; + + /** + * Cache service. + * + * @return \Illuminate\Cache\Repository + */ + $container['cache'] = function ($c) { + + $config = $c->config; + + if ($config['cache.driver'] == 'file') { + $path = $c->locator->findResource('cache://', TRUE, TRUE); + $cacheStore = new TaggableFileStore($path); + } else if ($config['cache.driver'] == 'memcached') { + // We need to inject the prefix in the memcached config + $config = array_merge($config['cache.memcached'], ['prefix' => $config['cache.prefix']]); + $cacheStore = new MemcachedStore($config); + } else if ($config['cache.driver'] == 'redis') { + // We need to inject the prefix in the redis config + $config = array_merge($config['cache.redis'], ['prefix' => $config['cache.prefix']]); + $cacheStore = new RedisStore($config); + } else { + throw new \Exception("Bad cache store type '{$config['cache.driver']}' specified in configuration file."); + } + + return $cacheStore->instance(); + }; + + /** + * Middleware to check environment. + * + * We should cache the results of this, the first time that it succeeds. + */ + $container['checkEnvironment'] = function ($c) { + $checkEnvironment = new CheckEnvironment($c->view, $c->locator, $c->cache); + return $checkEnvironment; + }; + + /** + * Class mapper. + * + * Creates an abstraction on top of class names to allow extending them in sprinkles. + */ + $container['classMapper'] = function ($c) { + $classMapper = new ClassMapper(); + $classMapper->setClassMapping('query_builder', 'UserFrosting\Sprinkle\Core\Database\Builder'); + $classMapper->setClassMapping('throttle', 'UserFrosting\Sprinkle\Core\Database\Models\Throttle'); + return $classMapper; + }; + + /** + * Site config service (separate from Slim settings). + * + * Will attempt to automatically determine which config file(s) to use based on the value of the UF_MODE environment variable. + */ + $container['config'] = function ($c) { + // Grab any relevant dotenv variables from the .env file + try { + $dotenv = new Dotenv(\UserFrosting\APP_DIR); + $dotenv->load(); + } catch (InvalidPathException $e) { + // Skip loading the environment config file if it doesn't exist. + } + + // Get configuration mode from environment + $mode = getenv('UF_MODE') ?: ''; + + // Construct and load config repository + $builder = new ConfigPathBuilder($c->locator, 'config://'); + $loader = new ArrayFileLoader($builder->buildPaths($mode)); + $config = new Repository($loader->load()); + + // Construct base url from components, if not explicitly specified + if (!isset($config['site.uri.public'])) { + $base_uri = $config['site.uri.base']; + + $public = new Uri( + $base_uri['scheme'], + $base_uri['host'], + $base_uri['port'], + $base_uri['path'] + ); + + // Slim\Http\Uri likes to add trailing slashes when the path is empty, so this fixes that. + $config['site.uri.public'] = trim($public, '/'); + } + + // Hacky fix to prevent sessions from being hit too much: ignore CSRF middleware for requests for raw assets ;-) + // See https://github.com/laravel/framework/issues/8172#issuecomment-99112012 for more information on why it's bad to hit Laravel sessions multiple times in rapid succession. + $csrfBlacklist = $config['csrf.blacklist']; + $csrfBlacklist['^/' . $config['assets.raw.path']] = [ + 'GET' + ]; + $csrfBlacklist['^/wormhole'] = [ + 'POST' + ]; + + $config->set('csrf.blacklist', $csrfBlacklist); + + return $config; + }; + + /** + * Initialize CSRF guard middleware. + * + * @see https://github.com/slimphp/Slim-Csrf + */ + $container['csrf'] = function ($c) { + $csrfKey = $c->config['session.keys.csrf']; + + // Workaround so that we can pass storage into CSRF guard. + // If we tried to directly pass the indexed portion of `session` (for example, $c->session['site.csrf']), + // we would get an 'Indirect modification of overloaded element of UserFrosting\Session\Session' error. + // If we tried to assign an array and use that, PHP would only modify the local variable, and not the session. + // Since ArrayObject is an object, PHP will modify the object itself, allowing it to persist in the session. + if (!$c->session->has($csrfKey)) { + $c->session[$csrfKey] = new \ArrayObject(); + } + $csrfStorage = $c->session[$csrfKey]; + + $onFailure = function ($request, $response, $next) { + $e = new BadRequestException("The CSRF code was invalid or not provided."); + $e->addUserMessage('CSRF_MISSING'); + throw $e; + + return $next($request, $response); + }; + + return new Guard($c->config['csrf.name'], $csrfStorage, $onFailure, $c->config['csrf.storage_limit'], $c->config['csrf.strength'], $c->config['csrf.persistent_token']); + }; + + /** + * Initialize Eloquent Capsule, which provides the database layer for UF. + * + * construct the individual objects rather than using the facade + */ + $container['db'] = function ($c) { + $config = $c->config; + + $capsule = new Capsule; + + foreach ($config['db'] as $name => $dbConfig) { + $capsule->addConnection($dbConfig, $name); + } + + $queryEventDispatcher = new Dispatcher(new Container); + + $capsule->setEventDispatcher($queryEventDispatcher); + + // Register as global connection + $capsule->setAsGlobal(); + + // Start Eloquent + $capsule->bootEloquent(); + + if ($config['debug.queries']) { + $logger = $c->queryLogger; + + foreach ($config['db'] as $name => $dbConfig) { + $capsule->connection($name)->enableQueryLog(); + } + + // Register listener + $queryEventDispatcher->listen(QueryExecuted::class, function ($query) use ($logger) { + $logger->debug("Query executed on database [{$query->connectionName}]:", [ + 'query' => $query->sql, + 'bindings' => $query->bindings, + 'time' => $query->time . ' ms' + ]); + }); + } + + return $capsule; + }; + + /** + * Debug logging with Monolog. + * + * Extend this service to push additional handlers onto the 'debug' log stack. + */ + $container['debugLogger'] = function ($c) { + $logger = new Logger('debug'); + + $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); + + $handler = new StreamHandler($logFile); + + $formatter = new MixedFormatter(NULL, NULL, TRUE); + + $handler->setFormatter($formatter); + $logger->pushHandler($handler); + + return $logger; + }; + + /** + * Custom error-handler for recoverable errors. + */ + $container['errorHandler'] = function ($c) { + $settings = $c->settings; + + $handler = new ExceptionHandlerManager($c, $settings['displayErrorDetails']); + + // Register the base HttpExceptionHandler. + $handler->registerHandler('\UserFrosting\Support\Exception\HttpException', '\UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler'); + + // Register the NotFoundExceptionHandler. + $handler->registerHandler('\UserFrosting\Support\Exception\NotFoundException', '\UserFrosting\Sprinkle\Core\Error\Handler\NotFoundExceptionHandler'); + + // Register the PhpMailerExceptionHandler. + $handler->registerHandler('\phpmailerException', '\UserFrosting\Sprinkle\Core\Error\Handler\PhpMailerExceptionHandler'); + + return $handler; + }; + + /** + * Error logging with Monolog. + * + * Extend this service to push additional handlers onto the 'error' log stack. + */ + $container['errorLogger'] = function ($c) { + $log = new Logger('errors'); + + $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); + + $handler = new StreamHandler($logFile, Logger::WARNING); + + $formatter = new LineFormatter(NULL, NULL, TRUE); + + $handler->setFormatter($formatter); + $log->pushHandler($handler); + + return $log; + }; + + /** + * Factory service with FactoryMuffin. + * + * Provide access to factories for the rapid creation of objects for the purpose of testing + */ + $container['factory'] = function ($c) { + + // Get the path of all of the sprinkle's factories + $factoriesPath = $c->locator->findResources('factories://', TRUE, TRUE); + + // Create a new Factory Muffin instance + $fm = new FactoryMuffin(); + + // Load all of the model definitions + $fm->loadFactories($factoriesPath); + + // Set the locale. Could be the config one, but for testing English should do + Faker::setLocale('en_EN'); + + return $fm; + }; + + /** + * Builds search paths for locales in all Sprinkles. + */ + $container['localePathBuilder'] = function ($c) { + $config = $c->config; + + // Make sure the locale config is a valid string + if (!is_string($config['site.locales.default']) || $config['site.locales.default'] == '') { + throw new \UnexpectedValueException('The locale config is not a valid string.'); + } + + // Load the base locale file(s) as specified in the configuration + $locales = explode(',', $config['site.locales.default']); + + return new LocalePathBuilder($c->locator, 'locale://', $locales); + }; + + /** + * Mail service. + */ + $container['mailer'] = function ($c) { + $mailer = new Mailer($c->mailLogger, $c->config['mail']); + + // Use UF debug settings to override any service-specific log settings. + if (!$c->config['debug.smtp']) { + $mailer->getPhpMailer()->SMTPDebug = 0; + } + + return $mailer; + }; + + /** + * Mail logging service. + * + * PHPMailer will use this to log SMTP activity. + * Extend this service to push additional handlers onto the 'mail' log stack. + */ + $container['mailLogger'] = function ($c) { + $log = new Logger('mail'); + + $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); + + $handler = new StreamHandler($logFile); + $formatter = new LineFormatter(NULL, NULL, TRUE); + + $handler->setFormatter($formatter); + $log->pushHandler($handler); + + return $log; + }; + + /** + * Error-handler for 404 errors. Notice that we manually create a UserFrosting NotFoundException, + * and a NotFoundExceptionHandler. This lets us pass through to the UF error handling system. + */ + $container['notFoundHandler'] = function ($c) { + return function ($request, $response) use ($c) { + $exception = new NotFoundException; + $handler = new NotFoundExceptionHandler($c, $request, $response, $exception, $c->settings['displayErrorDetails']); + return $handler->handle(); + }; + }; + + /** + * Error-handler for PHP runtime errors. Notice that we just pass this through to our general-purpose + * error-handling service. + */ + $container['phpErrorHandler'] = function ($c) { + return $c->errorHandler; + }; + + /** + * Laravel query logging with Monolog. + * + * Extend this service to push additional handlers onto the 'query' log stack. + */ + $container['queryLogger'] = function ($c) { + $logger = new Logger('query'); + + $logFile = $c->locator->findResource('log://userfrosting.log', TRUE, TRUE); + + $handler = new StreamHandler($logFile); + + $formatter = new MixedFormatter(NULL, NULL, TRUE); + + $handler->setFormatter($formatter); + $logger->pushHandler($handler); + + return $logger; + }; + + /** + * Override Slim's default router with the UF router. + */ + $container['router'] = function ($c) { + $routerCacheFile = FALSE; + if (isset($c->config['settings.routerCacheFile'])) { + $routerCacheFile = $c->config['settings.routerCacheFile']; + } + + return (new Router)->setCacheFile($routerCacheFile); + }; + + /** + * Start the PHP session, with the name and parameters specified in the configuration file. + */ + $container['session'] = function ($c) { + $config = $c->config; + + // Create appropriate handler based on config + if ($config['session.handler'] == 'file') { + $fs = new FileSystem; + $handler = new FileSessionHandler($fs, $c->locator->findResource('session://'), $config['session.minutes']); + } else if ($config['session.handler'] == 'database') { + $connection = $c->db->connection(); + // Table must exist, otherwise an exception will be thrown + $handler = new DatabaseSessionHandler($connection, $config['session.database.table'], $config['session.minutes']); + } else { + throw new \Exception("Bad session handler type '{$config['session.handler']}' specified in configuration file."); + } + + // Create, start and return a new wrapper for $_SESSION + $session = new Session($handler, $config['session']); + $session->start(); + + return $session; + }; + + /** + * Request throttler. + * + * Throttles (rate-limits) requests of a predefined type, with rules defined in site config. + */ + $container['throttler'] = function ($c) { + $throttler = new Throttler($c->classMapper); + + $config = $c->config; + + if ($config->has('throttles') && ($config['throttles'] !== NULL)) { + foreach ($config['throttles'] as $type => $rule) { + if ($rule) { + $throttleRule = new ThrottleRule($rule['method'], $rule['interval'], $rule['delays']); + $throttler->addThrottleRule($type, $throttleRule); + } else { + $throttler->addThrottleRule($type, NULL); + } + } + } + + return $throttler; + }; + + /** + * Translation service, for translating message tokens. + */ + $container['translator'] = function ($c) { + // Load the translations + $paths = $c->localePathBuilder->buildPaths(); + $loader = new ArrayFileLoader($paths); + + // Create the $translator object + $translator = new MessageTranslator($loader->load()); + + return $translator; + }; + + /** + * Set up Twig as the view, adding template paths for all sprinkles and the Slim Twig extension. + * + * Also adds the UserFrosting core Twig extension, which provides additional functions, filters, global variables, etc. + */ + $container['view'] = function ($c) { + $templatePaths = $c->locator->findResources('templates://', TRUE, TRUE); + + $view = new Twig($templatePaths); + + $loader = $view->getLoader(); + + $sprinkles = $c->sprinkleManager->getSprinkleNames(); + + // Add Sprinkles' templates namespaces + foreach ($sprinkles as $sprinkle) { + $path = \UserFrosting\SPRINKLES_DIR . \UserFrosting\DS . + $sprinkle . \UserFrosting\DS . + \UserFrosting\TEMPLATE_DIR_NAME . \UserFrosting\DS; + + if (is_dir($path)) { + $loader->addPath($path, $sprinkle); + } + } + + $twig = $view->getEnvironment(); + + if ($c->config['cache.twig']) { + $twig->setCache($c->locator->findResource('cache://twig', TRUE, TRUE)); + } + + if ($c->config['debug.twig']) { + $twig->enableDebug(); + $view->addExtension(new \Twig_Extension_Debug()); + } + + // Register the Slim extension with Twig + $slimExtension = new TwigExtension( + $c->router, + $c->request->getUri() + ); + $view->addExtension($slimExtension); + + // Register the core UF extension with Twig + $coreExtension = new CoreExtension($c); + $view->addExtension($coreExtension); + + return $view; + }; + } +} diff --git a/main/app/sprinkles/core/src/Sprunje/Sprunje.php b/main/app/sprinkles/core/src/Sprunje/Sprunje.php index ea066a3..b81d266 100644 --- a/main/app/sprinkles/core/src/Sprunje/Sprunje.php +++ b/main/app/sprinkles/core/src/Sprunje/Sprunje.php @@ -1,547 +1,547 @@ - [], - 'filters' => [], - 'lists' => [], - 'size' => 'all', - 'page' => NULL, - 'format' => 'json' - ]; - - /** - * Fields to allow filtering upon. - * - * @var array[string] - */ - protected $filterable = []; - - /** - * Fields to allow listing (enumeration) upon. - * - * @var array[string] - */ - protected $listable = []; - - /** - * Fields to allow sorting upon. - * - * @var array[string] - */ - protected $sortable = []; - - /** - * List of fields to exclude when processing an "_all" filter. - * - * @var array[string] - */ - protected $excludeForAll = []; - - /** - * Separator to use when splitting filter values to treat them as ORs. - * - * @var string - */ - protected $orSeparator = '||'; - - /** - * Array key for the total unfiltered object count. - * - * @var string - */ - protected $countKey = 'count'; - - /** - * Array key for the filtered object count. - * - * @var string - */ - protected $countFilteredKey = 'count_filtered'; - - /** - * Array key for the actual result set. - * - * @var string - */ - protected $rowsKey = 'rows'; - - /** - * Array key for the list of enumerated columns and their enumerations. - * - * @var string - */ - protected $listableKey = 'listable'; - - /** - * Constructor. - * - * @param ClassMapper $classMapper - * @param mixed[] $options - */ - public function __construct(ClassMapper $classMapper, array $options) { - $this->classMapper = $classMapper; - - // Validation on input data - $v = new Validator($options); - $v->rule('array', ['sorts', 'filters', 'lists']); - $v->rule('regex', 'sorts.*', '/asc|desc/i'); - $v->rule('regex', 'size', '/all|[0-9]+/i'); - $v->rule('integer', 'page'); - $v->rule('regex', 'format', '/json|csv/i'); - - // translated rules - if (!$v->validate()) { - $e = new BadRequestException(); - foreach ($v->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $e->addUserMessage($error); - } - } - throw $e; - } - - $this->options = array_replace_recursive($this->options, $options); - - $this->query = $this->baseQuery(); - - // Start a new query on any Model instances - if (is_a($this->baseQuery(), '\Illuminate\Database\Eloquent\Model')) { - $this->query = $this->baseQuery()->newQuery(); - } - } - - /** - * Extend the query by providing a callback. - * - * @param callable $callback A callback which accepts and returns a Builder instance. - * @return $this - */ - public function extendQuery(callable $callback) { - $this->query = $callback($this->query); - return $this; - } - - /** - * Execute the query and build the results, and append them in the appropriate format to the response. - * - * @param ResponseInterface $response - * @return ResponseInterface - */ - public function toResponse(Response $response) { - $format = $this->options['format']; - - if ($format == 'csv') { - $result = $this->getCsv(); - - // Prepare response - $settings = http_build_query($this->options); - $date = Carbon::now()->format('Ymd'); - $response = $response->withAddedHeader('Content-Disposition', "attachment;filename=$date-{$this->name}-$settings.csv"); - $response = $response->withAddedHeader('Content-Type', 'text/csv; charset=utf-8'); - return $response->write($result); - // Default to JSON - } else { - $result = $this->getArray(); - return $response->withJson($result, 200, JSON_PRETTY_PRINT); - } - } - - /** - * Executes the sprunje query, applying all sorts, filters, and pagination. - * - * Returns an array containing `count` (the total number of rows, before filtering), `count_filtered` (the total number of rows after filtering), - * and `rows` (the filtered result set). - * @return mixed[] - */ - public function getArray() { - list($count, $countFiltered, $rows) = $this->getModels(); - - // Return sprunjed results - return [ - $this->countKey => $count, - $this->countFilteredKey => $countFiltered, - $this->rowsKey => $rows->values()->toArray(), - $this->listableKey => $this->getListable() - ]; - } - - /** - * Run the query and build a CSV object by flattening the resulting collection. Ignores any pagination. - * - * @return SplTempFileObject - */ - public function getCsv() { - $filteredQuery = clone $this->query; - - // Apply filters - $this->applyFilters($filteredQuery); - - // Apply sorts - $this->applySorts($filteredQuery); - - $collection = collect($filteredQuery->get()); - - // Perform any additional transformations on the dataset - $this->applyTransformations($collection); - - $csv = Writer::createFromFileObject(new \SplTempFileObject()); - - $columnNames = []; - - // Flatten collection while simultaneously building the column names from the union of each element's keys - $collection->transform(function ($item, $key) use (&$columnNames) { - $item = array_dot($item->toArray()); - foreach ($item as $itemKey => $itemValue) { - if (!in_array($itemKey, $columnNames)) { - $columnNames[] = $itemKey; - } - } - return $item; - }); - - $csv->insertOne($columnNames); - - // Insert the data as rows in the CSV document - $collection->each(function ($item) use ($csv, $columnNames) { - $row = []; - foreach ($columnNames as $itemKey) { - // Only add the value if it is set and not an array. Laravel's array_dot sometimes creates empty child arrays :( - // See https://github.com/laravel/framework/pull/13009 - if (isset($item[$itemKey]) && !is_array($item[$itemKey])) { - $row[] = $item[$itemKey]; - } else { - $row[] = ''; - } - } - - $csv->insertOne($row); - }); - - return $csv; - } - - /** - * Executes the sprunje query, applying all sorts, filters, and pagination. - * - * Returns the filtered, paginated result set and the counts. - * @return mixed[] - */ - public function getModels() { - // Count unfiltered total - $count = $this->count($this->query); - - // Clone the Query\Builder, Eloquent\Builder, or Relation - $filteredQuery = clone $this->query; - - // Apply filters - $this->applyFilters($filteredQuery); - - // Count filtered total - $countFiltered = $this->countFiltered($filteredQuery); - - // Apply sorts - $this->applySorts($filteredQuery); - - // Paginate - $this->applyPagination($filteredQuery); - - $collection = collect($filteredQuery->get()); - - // Perform any additional transformations on the dataset - $this->applyTransformations($collection); - - return [$count, $countFiltered, $collection]; - } - - /** - * Get lists of values for specified fields in 'lists' option, calling a custom lister callback when appropriate. - * - * @return array - */ - public function getListable() { - $result = []; - foreach ($this->listable as $name) { - - // Determine if a custom filter method has been defined - $methodName = 'list' . studly_case($name); - - if (method_exists($this, $methodName)) { - $result[$name] = $this->$methodName(); - } else { - $result[$name] = $this->getColumnValues($name); - } - } - - return $result; - } - - /** - * Get the underlying queriable object in its current state. - * - * @return Builder - */ - public function getQuery() { - return $this->query; - } - - /** - * Set the underlying QueryBuilder object. - * - * @param Builder $query - * @return $this - */ - public function setQuery($query) { - $this->query = $query; - return $this; - } - - /** - * Apply any filters from the options, calling a custom filter callback when appropriate. - * - * @param Builder $query - * @return $this - */ - public function applyFilters($query) { - foreach ($this->options['filters'] as $name => $value) { - // Check that this filter is allowed - if (($name != '_all') && !in_array($name, $this->filterable)) { - $e = new BadRequestException(); - $e->addUserMessage('VALIDATE.SPRUNJE.BAD_FILTER', ['name' => $name]); - throw $e; - } - // Since we want to match _all_ of the fields, we wrap the field callback in a 'where' callback - $query->where(function ($fieldQuery) use ($name, $value) { - $this->buildFilterQuery($fieldQuery, $name, $value); - }); - } - - return $this; - } - - /** - * Apply any sorts from the options, calling a custom sorter callback when appropriate. - * - * @param Builder $query - * @return $this - */ - public function applySorts($query) { - foreach ($this->options['sorts'] as $name => $direction) { - // Check that this sort is allowed - if (!in_array($name, $this->sortable)) { - $e = new BadRequestException(); - $e->addUserMessage('VALIDATE.SPRUNJE.BAD_SORT', ['name' => $name]); - throw $e; - } - - // Determine if a custom sort method has been defined - $methodName = 'sort' . studly_case($name); - - if (method_exists($this, $methodName)) { - $this->$methodName($query, $direction); - } else { - $query->orderBy($name, $direction); - } - } - - return $this; - } - - /** - * Apply pagination based on the `page` and `size` options. - * - * @param Builder $query - * @return $this - */ - public function applyPagination($query) { - if ( - ($this->options['page'] !== NULL) && - ($this->options['size'] !== NULL) && - ($this->options['size'] != 'all') - ) { - $offset = $this->options['size'] * $this->options['page']; - $query->skip($offset) - ->take($this->options['size']); - } - - return $this; - } - - /** - * Match any filter in `filterable`. - * - * @param Builder $query - * @param mixed $value - * @return $this - */ - protected function filterAll($query, $value) { - foreach ($this->filterable as $name) { - if (studly_case($name) != 'all' && !in_array($name, $this->excludeForAll)) { - // Since we want to match _any_ of the fields, we wrap the field callback in a 'orWhere' callback - $query->orWhere(function ($fieldQuery) use ($name, $value) { - $this->buildFilterQuery($fieldQuery, $name, $value); - }); - } - } - - return $this; - } - - /** - * Build the filter query for a single field. - * - * @param Builder $query - * @param string $name - * @param mixed $value - * @return $this - */ - protected function buildFilterQuery($query, $name, $value) { - $methodName = 'filter' . studly_case($name); - - // Determine if a custom filter method has been defined - if (method_exists($this, $methodName)) { - $this->$methodName($query, $value); - } else { - $this->buildFilterDefaultFieldQuery($query, $name, $value); - } - - return $this; - } - - /** - * Perform a 'like' query on a single field, separating the value string on the or separator and - * matching any of the supplied values. - * - * @param Builder $query - * @param string $name - * @param mixed $value - * @return $this - */ - protected function buildFilterDefaultFieldQuery($query, $name, $value) { - // Default filter - split value on separator for OR queries - // and search by column name - $values = explode($this->orSeparator, $value); - foreach ($values as $value) { - $query->orLike($name, $value); - } - - return $this; - } - - /** - * Set any transformations you wish to apply to the collection, after the query is executed. - * - * @param \Illuminate\Database\Eloquent\Collection $collection - * @return \Illuminate\Database\Eloquent\Collection - */ - protected function applyTransformations($collection) { - return $collection; - } - - /** - * Set the initial query used by your Sprunje. - * - * @return Builder|Relation|Model - */ - abstract protected function baseQuery(); - - /** - * Returns a list of distinct values for a specified column. - * Formats results to have a "value" and "text" attribute. - * - * @param string $column - * @return array - */ - protected function getColumnValues($column) { - $rawValues = $this->query->select($column)->distinct()->orderBy($column, 'asc')->get(); - $values = []; - foreach ($rawValues as $raw) { - $values[] = [ - 'value' => $raw[$column], - 'text' => $raw[$column] - ]; - } - return $values; - } - - /** - * Get the unpaginated count of items (before filtering) in this query. - * - * @param Builder $query - * @return int - */ - protected function count($query) { - return $query->count(); - } - - /** - * Get the unpaginated count of items (after filtering) in this query. - * - * @param Builder $query - * @return int - */ - protected function countFiltered($query) { - return $query->count(); - } - - /** - * Executes the sprunje query, applying all sorts, filters, and pagination. - * - * Returns an array containing `count` (the total number of rows, before filtering), `count_filtered` (the total number of rows after filtering), - * and `rows` (the filtered result set). - * @deprecated since 4.1.7 Use getArray() instead. - * @return mixed[] - */ - public function getResults() { - return $this->getArray(); - } -} + [], + 'filters' => [], + 'lists' => [], + 'size' => 'all', + 'page' => NULL, + 'format' => 'json' + ]; + + /** + * Fields to allow filtering upon. + * + * @var array[string] + */ + protected $filterable = []; + + /** + * Fields to allow listing (enumeration) upon. + * + * @var array[string] + */ + protected $listable = []; + + /** + * Fields to allow sorting upon. + * + * @var array[string] + */ + protected $sortable = []; + + /** + * List of fields to exclude when processing an "_all" filter. + * + * @var array[string] + */ + protected $excludeForAll = []; + + /** + * Separator to use when splitting filter values to treat them as ORs. + * + * @var string + */ + protected $orSeparator = '||'; + + /** + * Array key for the total unfiltered object count. + * + * @var string + */ + protected $countKey = 'count'; + + /** + * Array key for the filtered object count. + * + * @var string + */ + protected $countFilteredKey = 'count_filtered'; + + /** + * Array key for the actual result set. + * + * @var string + */ + protected $rowsKey = 'rows'; + + /** + * Array key for the list of enumerated columns and their enumerations. + * + * @var string + */ + protected $listableKey = 'listable'; + + /** + * Constructor. + * + * @param ClassMapper $classMapper + * @param mixed[] $options + */ + public function __construct(ClassMapper $classMapper, array $options) { + $this->classMapper = $classMapper; + + // Validation on input data + $v = new Validator($options); + $v->rule('array', ['sorts', 'filters', 'lists']); + $v->rule('regex', 'sorts.*', '/asc|desc/i'); + $v->rule('regex', 'size', '/all|[0-9]+/i'); + $v->rule('integer', 'page'); + $v->rule('regex', 'format', '/json|csv/i'); + + // translated rules + if (!$v->validate()) { + $e = new BadRequestException(); + foreach ($v->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + $this->options = array_replace_recursive($this->options, $options); + + $this->query = $this->baseQuery(); + + // Start a new query on any Model instances + if (is_a($this->baseQuery(), '\Illuminate\Database\Eloquent\Model')) { + $this->query = $this->baseQuery()->newQuery(); + } + } + + /** + * Extend the query by providing a callback. + * + * @param callable $callback A callback which accepts and returns a Builder instance. + * @return $this + */ + public function extendQuery(callable $callback) { + $this->query = $callback($this->query); + return $this; + } + + /** + * Execute the query and build the results, and append them in the appropriate format to the response. + * + * @param ResponseInterface $response + * @return ResponseInterface + */ + public function toResponse(Response $response) { + $format = $this->options['format']; + + if ($format == 'csv') { + $result = $this->getCsv(); + + // Prepare response + $settings = http_build_query($this->options); + $date = Carbon::now()->format('Ymd'); + $response = $response->withAddedHeader('Content-Disposition', "attachment;filename=$date-{$this->name}-$settings.csv"); + $response = $response->withAddedHeader('Content-Type', 'text/csv; charset=utf-8'); + return $response->write($result); + // Default to JSON + } else { + $result = $this->getArray(); + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + } + + /** + * Executes the sprunje query, applying all sorts, filters, and pagination. + * + * Returns an array containing `count` (the total number of rows, before filtering), `count_filtered` (the total number of rows after filtering), + * and `rows` (the filtered result set). + * @return mixed[] + */ + public function getArray() { + list($count, $countFiltered, $rows) = $this->getModels(); + + // Return sprunjed results + return [ + $this->countKey => $count, + $this->countFilteredKey => $countFiltered, + $this->rowsKey => $rows->values()->toArray(), + $this->listableKey => $this->getListable() + ]; + } + + /** + * Run the query and build a CSV object by flattening the resulting collection. Ignores any pagination. + * + * @return SplTempFileObject + */ + public function getCsv() { + $filteredQuery = clone $this->query; + + // Apply filters + $this->applyFilters($filteredQuery); + + // Apply sorts + $this->applySorts($filteredQuery); + + $collection = collect($filteredQuery->get()); + + // Perform any additional transformations on the dataset + $this->applyTransformations($collection); + + $csv = Writer::createFromFileObject(new \SplTempFileObject()); + + $columnNames = []; + + // Flatten collection while simultaneously building the column names from the union of each element's keys + $collection->transform(function ($item, $key) use (&$columnNames) { + $item = array_dot($item->toArray()); + foreach ($item as $itemKey => $itemValue) { + if (!in_array($itemKey, $columnNames)) { + $columnNames[] = $itemKey; + } + } + return $item; + }); + + $csv->insertOne($columnNames); + + // Insert the data as rows in the CSV document + $collection->each(function ($item) use ($csv, $columnNames) { + $row = []; + foreach ($columnNames as $itemKey) { + // Only add the value if it is set and not an array. Laravel's array_dot sometimes creates empty child arrays :( + // See https://github.com/laravel/framework/pull/13009 + if (isset($item[$itemKey]) && !is_array($item[$itemKey])) { + $row[] = $item[$itemKey]; + } else { + $row[] = ''; + } + } + + $csv->insertOne($row); + }); + + return $csv; + } + + /** + * Executes the sprunje query, applying all sorts, filters, and pagination. + * + * Returns the filtered, paginated result set and the counts. + * @return mixed[] + */ + public function getModels() { + // Count unfiltered total + $count = $this->count($this->query); + + // Clone the Query\Builder, Eloquent\Builder, or Relation + $filteredQuery = clone $this->query; + + // Apply filters + $this->applyFilters($filteredQuery); + + // Count filtered total + $countFiltered = $this->countFiltered($filteredQuery); + + // Apply sorts + $this->applySorts($filteredQuery); + + // Paginate + $this->applyPagination($filteredQuery); + + $collection = collect($filteredQuery->get()); + + // Perform any additional transformations on the dataset + $this->applyTransformations($collection); + + return [$count, $countFiltered, $collection]; + } + + /** + * Get lists of values for specified fields in 'lists' option, calling a custom lister callback when appropriate. + * + * @return array + */ + public function getListable() { + $result = []; + foreach ($this->listable as $name) { + + // Determine if a custom filter method has been defined + $methodName = 'list' . studly_case($name); + + if (method_exists($this, $methodName)) { + $result[$name] = $this->$methodName(); + } else { + $result[$name] = $this->getColumnValues($name); + } + } + + return $result; + } + + /** + * Get the underlying queriable object in its current state. + * + * @return Builder + */ + public function getQuery() { + return $this->query; + } + + /** + * Set the underlying QueryBuilder object. + * + * @param Builder $query + * @return $this + */ + public function setQuery($query) { + $this->query = $query; + return $this; + } + + /** + * Apply any filters from the options, calling a custom filter callback when appropriate. + * + * @param Builder $query + * @return $this + */ + public function applyFilters($query) { + foreach ($this->options['filters'] as $name => $value) { + // Check that this filter is allowed + if (($name != '_all') && !in_array($name, $this->filterable)) { + $e = new BadRequestException(); + $e->addUserMessage('VALIDATE.SPRUNJE.BAD_FILTER', ['name' => $name]); + throw $e; + } + // Since we want to match _all_ of the fields, we wrap the field callback in a 'where' callback + $query->where(function ($fieldQuery) use ($name, $value) { + $this->buildFilterQuery($fieldQuery, $name, $value); + }); + } + + return $this; + } + + /** + * Apply any sorts from the options, calling a custom sorter callback when appropriate. + * + * @param Builder $query + * @return $this + */ + public function applySorts($query) { + foreach ($this->options['sorts'] as $name => $direction) { + // Check that this sort is allowed + if (!in_array($name, $this->sortable)) { + $e = new BadRequestException(); + $e->addUserMessage('VALIDATE.SPRUNJE.BAD_SORT', ['name' => $name]); + throw $e; + } + + // Determine if a custom sort method has been defined + $methodName = 'sort' . studly_case($name); + + if (method_exists($this, $methodName)) { + $this->$methodName($query, $direction); + } else { + $query->orderBy($name, $direction); + } + } + + return $this; + } + + /** + * Apply pagination based on the `page` and `size` options. + * + * @param Builder $query + * @return $this + */ + public function applyPagination($query) { + if ( + ($this->options['page'] !== NULL) && + ($this->options['size'] !== NULL) && + ($this->options['size'] != 'all') + ) { + $offset = $this->options['size'] * $this->options['page']; + $query->skip($offset) + ->take($this->options['size']); + } + + return $this; + } + + /** + * Match any filter in `filterable`. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterAll($query, $value) { + foreach ($this->filterable as $name) { + if (studly_case($name) != 'all' && !in_array($name, $this->excludeForAll)) { + // Since we want to match _any_ of the fields, we wrap the field callback in a 'orWhere' callback + $query->orWhere(function ($fieldQuery) use ($name, $value) { + $this->buildFilterQuery($fieldQuery, $name, $value); + }); + } + } + + return $this; + } + + /** + * Build the filter query for a single field. + * + * @param Builder $query + * @param string $name + * @param mixed $value + * @return $this + */ + protected function buildFilterQuery($query, $name, $value) { + $methodName = 'filter' . studly_case($name); + + // Determine if a custom filter method has been defined + if (method_exists($this, $methodName)) { + $this->$methodName($query, $value); + } else { + $this->buildFilterDefaultFieldQuery($query, $name, $value); + } + + return $this; + } + + /** + * Perform a 'like' query on a single field, separating the value string on the or separator and + * matching any of the supplied values. + * + * @param Builder $query + * @param string $name + * @param mixed $value + * @return $this + */ + protected function buildFilterDefaultFieldQuery($query, $name, $value) { + // Default filter - split value on separator for OR queries + // and search by column name + $values = explode($this->orSeparator, $value); + foreach ($values as $value) { + $query->orLike($name, $value); + } + + return $this; + } + + /** + * Set any transformations you wish to apply to the collection, after the query is executed. + * + * @param \Illuminate\Database\Eloquent\Collection $collection + * @return \Illuminate\Database\Eloquent\Collection + */ + protected function applyTransformations($collection) { + return $collection; + } + + /** + * Set the initial query used by your Sprunje. + * + * @return Builder|Relation|Model + */ + abstract protected function baseQuery(); + + /** + * Returns a list of distinct values for a specified column. + * Formats results to have a "value" and "text" attribute. + * + * @param string $column + * @return array + */ + protected function getColumnValues($column) { + $rawValues = $this->query->select($column)->distinct()->orderBy($column, 'asc')->get(); + $values = []; + foreach ($rawValues as $raw) { + $values[] = [ + 'value' => $raw[$column], + 'text' => $raw[$column] + ]; + } + return $values; + } + + /** + * Get the unpaginated count of items (before filtering) in this query. + * + * @param Builder $query + * @return int + */ + protected function count($query) { + return $query->count(); + } + + /** + * Get the unpaginated count of items (after filtering) in this query. + * + * @param Builder $query + * @return int + */ + protected function countFiltered($query) { + return $query->count(); + } + + /** + * Executes the sprunje query, applying all sorts, filters, and pagination. + * + * Returns an array containing `count` (the total number of rows, before filtering), `count_filtered` (the total number of rows after filtering), + * and `rows` (the filtered result set). + * @deprecated since 4.1.7 Use getArray() instead. + * @return mixed[] + */ + public function getResults() { + return $this->getArray(); + } +} diff --git a/main/app/sprinkles/core/src/Throttle/ThrottleRule.php b/main/app/sprinkles/core/src/Throttle/ThrottleRule.php index c5e0c82..5840027 100644 --- a/main/app/sprinkles/core/src/Throttle/ThrottleRule.php +++ b/main/app/sprinkles/core/src/Throttle/ThrottleRule.php @@ -1,133 +1,133 @@ -setMethod($method); - $this->setInterval($interval); - $this->setDelays($delays); - } - - /** - * Get the current delay on this rule for a particular number of event counts. - * - * @param Carbon\Carbon $lastEventTime The timestamp for the last countable event. - * @param int $count The total number of events which have occurred in an interval. - */ - public function getDelay($lastEventTime, $count) { - // Zero occurrences always maps to a delay of 0 seconds. - if ($count == 0) { - return 0; - } - - foreach ($this->delays as $observations => $delay) { - // Skip any delay rules for which we haven't met the requisite number of observations - if ($count < $observations) { - continue; - } - - // If this rule meets the observed number of events, and violates the required delay, then return the remaining time left - if ($lastEventTime->diffInSeconds() < $delay) { - return $lastEventTime->addSeconds($delay)->diffInSeconds(); - } - } - - return 0; - } - - /** - * Gets the current mapping of attempts (int) to delays (seconds). - * - * @return int[] - */ - public function getDelays() { - return $this->delays; - } - - /** - * Gets the current throttling interval (seconds). - * - * @return int - */ - public function getInterval() { - return $this->interval; - } - - /** - * Gets the current throttling method ('ip' or 'data'). - * - * @return string - */ - public function getMethod() { - return $this->method; - } - - /** - * Sets the current mapping of attempts (int) to delays (seconds). - * - * @param int[] A mapping of minimum observation counts (x) to delays (y), in seconds. - */ - public function setDelays($delays) { - // Sort the array by key, from highest to lowest value - $this->delays = $delays; - krsort($this->delays); - - return $this; - } - - /** - * Sets the current throttling interval (seconds). - * - * @param int The amount of time, in seconds, to look back in determining attempts to consider. - */ - public function setInterval($interval) { - $this->interval = $interval; - - return $this; - } - - /** - * Sets the current throttling method ('ip' or 'data'). - * - * @param string Set to 'ip' for ip-based throttling, 'data' for request-data-based throttling. - */ - public function setMethod($method) { - $this->method = $method; - - return $this; - } -} +setMethod($method); + $this->setInterval($interval); + $this->setDelays($delays); + } + + /** + * Get the current delay on this rule for a particular number of event counts. + * + * @param Carbon\Carbon $lastEventTime The timestamp for the last countable event. + * @param int $count The total number of events which have occurred in an interval. + */ + public function getDelay($lastEventTime, $count) { + // Zero occurrences always maps to a delay of 0 seconds. + if ($count == 0) { + return 0; + } + + foreach ($this->delays as $observations => $delay) { + // Skip any delay rules for which we haven't met the requisite number of observations + if ($count < $observations) { + continue; + } + + // If this rule meets the observed number of events, and violates the required delay, then return the remaining time left + if ($lastEventTime->diffInSeconds() < $delay) { + return $lastEventTime->addSeconds($delay)->diffInSeconds(); + } + } + + return 0; + } + + /** + * Gets the current mapping of attempts (int) to delays (seconds). + * + * @return int[] + */ + public function getDelays() { + return $this->delays; + } + + /** + * Gets the current throttling interval (seconds). + * + * @return int + */ + public function getInterval() { + return $this->interval; + } + + /** + * Gets the current throttling method ('ip' or 'data'). + * + * @return string + */ + public function getMethod() { + return $this->method; + } + + /** + * Sets the current mapping of attempts (int) to delays (seconds). + * + * @param int[] A mapping of minimum observation counts (x) to delays (y), in seconds. + */ + public function setDelays($delays) { + // Sort the array by key, from highest to lowest value + $this->delays = $delays; + krsort($this->delays); + + return $this; + } + + /** + * Sets the current throttling interval (seconds). + * + * @param int The amount of time, in seconds, to look back in determining attempts to consider. + */ + public function setInterval($interval) { + $this->interval = $interval; + + return $this; + } + + /** + * Sets the current throttling method ('ip' or 'data'). + * + * @param string Set to 'ip' for ip-based throttling, 'data' for request-data-based throttling. + */ + public function setMethod($method) { + $this->method = $method; + + return $this; + } +} diff --git a/main/app/sprinkles/core/src/Throttle/Throttler.php b/main/app/sprinkles/core/src/Throttle/Throttler.php index 4ab9dd6..f7d1cc7 100644 --- a/main/app/sprinkles/core/src/Throttle/Throttler.php +++ b/main/app/sprinkles/core/src/Throttle/Throttler.php @@ -1,172 +1,172 @@ -classMapper = $classMapper; - $this->throttleRules = []; - } - - /** - * Add a throttling rule for a particular throttle event type. - * - * @param string $type The type of throttle event to check against. - * @param ThrottleRule $rule The rule to use when throttling this type of event. - */ - public function addThrottleRule($type, $rule) { - if (!($rule instanceof ThrottleRule || ($rule === NULL))) { - throw new ThrottlerException('$rule must be of type ThrottleRule (or null).'); - } - - $this->throttleRules[$type] = $rule; - - return $this; - } - - /** - * Check the current request against a specified throttle rule. - * - * @param string $type The type of throttle event to check against. - * @param mixed[] $requestData Any additional request parameters to use in checking the throttle. - * @return bool - */ - public function getDelay($type, $requestData = []) { - $throttleRule = $this->getRule($type); - - if (is_null($throttleRule)) { - return 0; - } - - // Get earliest time to start looking for throttleable events - $startTime = Carbon::now() - ->subSeconds($throttleRule->getInterval()); - - // Fetch all throttle events of the specified type, that match the specified rule - if ($throttleRule->getMethod() == 'ip') { - $events = $this->classMapper->staticMethod('throttle', 'where', 'type', $type) - ->where('created_at', '>', $startTime) - ->where('ip', $_SERVER['REMOTE_ADDR']) - ->get(); - } else { - $events = $this->classMapper->staticMethod('throttle', 'where', 'type', $type) - ->where('created_at', '>', $startTime) - ->get(); - - // Filter out only events that match the required JSON data - $events = $events->filter(function ($item, $key) use ($requestData) { - $data = json_decode($item->request_data); - - // If a field is not specified in the logged data, or it doesn't match the value we're searching for, - // then filter out this event from the collection. - foreach ($requestData as $name => $value) { - if (!isset($data->$name) || ($data->$name != $value)) { - return FALSE; - } - } - - return TRUE; - }); - } - - // Check the collection of events against the specified throttle rule. - return $this->computeDelay($events, $throttleRule); - } - - /** - * Get a registered rule of a particular type. - * - * @param string $type - * @throws ThrottlerException - * @return ThrottleRule[] - */ - public function getRule($type) { - if (!array_key_exists($type, $this->throttleRules)) { - throw new ThrottlerException("The throttling rule for '$type' could not be found."); - } - - return $this->throttleRules[$type]; - } - - /** - * Get the current throttling rules. - * - * @return ThrottleRule[] - */ - public function getThrottleRules() { - return $this->throttleRules; - } - - /** - * Log a throttleable event to the database. - * - * @param string $type the type of event - * @param string[] $requestData an array of field names => values that are relevant to throttling for this event (e.g. username, email, etc). - */ - public function logEvent($type, $requestData = []) { - // Just a check to make sure the rule exists - $throttleRule = $this->getRule($type); - - if (is_null($throttleRule)) { - return $this; - } - - $event = $this->classMapper->createInstance('throttle', [ - 'type' => $type, - 'ip' => $_SERVER['REMOTE_ADDR'], - 'request_data' => json_encode($requestData) - ]); - - $event->save(); - - return $this; - } - - /** - * Returns the current delay for a specified throttle rule. - * - * @param Throttle[] $events a Collection of throttle events. - * @param ThrottleRule $throttleRule a rule representing the strategy to use for throttling a particular type of event. - * @return int seconds remaining until a particular event is permitted to be attempted again. - */ - protected function computeDelay($events, $throttleRule) { - // If no matching events found, then there is no delay - if (!$events->count()) { - return 0; - } - - // Great, now we compare our delay against the most recent attempt - $lastEvent = $events->last(); - return $throttleRule->getDelay($lastEvent->created_at, $events->count()); - } -} +classMapper = $classMapper; + $this->throttleRules = []; + } + + /** + * Add a throttling rule for a particular throttle event type. + * + * @param string $type The type of throttle event to check against. + * @param ThrottleRule $rule The rule to use when throttling this type of event. + */ + public function addThrottleRule($type, $rule) { + if (!($rule instanceof ThrottleRule || ($rule === NULL))) { + throw new ThrottlerException('$rule must be of type ThrottleRule (or null).'); + } + + $this->throttleRules[$type] = $rule; + + return $this; + } + + /** + * Check the current request against a specified throttle rule. + * + * @param string $type The type of throttle event to check against. + * @param mixed[] $requestData Any additional request parameters to use in checking the throttle. + * @return bool + */ + public function getDelay($type, $requestData = []) { + $throttleRule = $this->getRule($type); + + if (is_null($throttleRule)) { + return 0; + } + + // Get earliest time to start looking for throttleable events + $startTime = Carbon::now() + ->subSeconds($throttleRule->getInterval()); + + // Fetch all throttle events of the specified type, that match the specified rule + if ($throttleRule->getMethod() == 'ip') { + $events = $this->classMapper->staticMethod('throttle', 'where', 'type', $type) + ->where('created_at', '>', $startTime) + ->where('ip', $_SERVER['REMOTE_ADDR']) + ->get(); + } else { + $events = $this->classMapper->staticMethod('throttle', 'where', 'type', $type) + ->where('created_at', '>', $startTime) + ->get(); + + // Filter out only events that match the required JSON data + $events = $events->filter(function ($item, $key) use ($requestData) { + $data = json_decode($item->request_data); + + // If a field is not specified in the logged data, or it doesn't match the value we're searching for, + // then filter out this event from the collection. + foreach ($requestData as $name => $value) { + if (!isset($data->$name) || ($data->$name != $value)) { + return FALSE; + } + } + + return TRUE; + }); + } + + // Check the collection of events against the specified throttle rule. + return $this->computeDelay($events, $throttleRule); + } + + /** + * Get a registered rule of a particular type. + * + * @param string $type + * @throws ThrottlerException + * @return ThrottleRule[] + */ + public function getRule($type) { + if (!array_key_exists($type, $this->throttleRules)) { + throw new ThrottlerException("The throttling rule for '$type' could not be found."); + } + + return $this->throttleRules[$type]; + } + + /** + * Get the current throttling rules. + * + * @return ThrottleRule[] + */ + public function getThrottleRules() { + return $this->throttleRules; + } + + /** + * Log a throttleable event to the database. + * + * @param string $type the type of event + * @param string[] $requestData an array of field names => values that are relevant to throttling for this event (e.g. username, email, etc). + */ + public function logEvent($type, $requestData = []) { + // Just a check to make sure the rule exists + $throttleRule = $this->getRule($type); + + if (is_null($throttleRule)) { + return $this; + } + + $event = $this->classMapper->createInstance('throttle', [ + 'type' => $type, + 'ip' => $_SERVER['REMOTE_ADDR'], + 'request_data' => json_encode($requestData) + ]); + + $event->save(); + + return $this; + } + + /** + * Returns the current delay for a specified throttle rule. + * + * @param Throttle[] $events a Collection of throttle events. + * @param ThrottleRule $throttleRule a rule representing the strategy to use for throttling a particular type of event. + * @return int seconds remaining until a particular event is permitted to be attempted again. + */ + protected function computeDelay($events, $throttleRule) { + // If no matching events found, then there is no delay + if (!$events->count()) { + return 0; + } + + // Great, now we compare our delay against the most recent attempt + $lastEvent = $events->last(); + return $throttleRule->getDelay($lastEvent->created_at, $events->count()); + } +} diff --git a/main/app/sprinkles/core/src/Throttle/ThrottlerException.php b/main/app/sprinkles/core/src/Throttle/ThrottlerException.php index 08f2919..af29bc8 100644 --- a/main/app/sprinkles/core/src/Throttle/ThrottlerException.php +++ b/main/app/sprinkles/core/src/Throttle/ThrottlerException.php @@ -1,19 +1,19 @@ -ci = $ci; - } - - /** - * Function that delete the Twig cache directory content - * - * @access public - * @return bool true/false if operation is successfull - */ - public function clearCache() { - // Get location - $path = $this->ci->locator->findResource('cache://twig', TRUE, TRUE); - - // Get Filesystem instance - $fs = new FileSystem; - - // Make sure directory exist and delete it - if ($fs->exists($path)) { - return $fs->deleteDirectory($path, TRUE); - } - - // It's still considered a success if directory doesn't exist yet - return TRUE; - } -} +ci = $ci; + } + + /** + * Function that delete the Twig cache directory content + * + * @access public + * @return bool true/false if operation is successfull + */ + public function clearCache() { + // Get location + $path = $this->ci->locator->findResource('cache://twig', TRUE, TRUE); + + // Get Filesystem instance + $fs = new FileSystem; + + // Make sure directory exist and delete it + if ($fs->exists($path)) { + return $fs->deleteDirectory($path, TRUE); + } + + // It's still considered a success if directory doesn't exist yet + return TRUE; + } +} diff --git a/main/app/sprinkles/core/src/Twig/CoreExtension.php b/main/app/sprinkles/core/src/Twig/CoreExtension.php index 2837e84..fccd542 100644 --- a/main/app/sprinkles/core/src/Twig/CoreExtension.php +++ b/main/app/sprinkles/core/src/Twig/CoreExtension.php @@ -1,120 +1,120 @@ -services = $services; - } - - /** - * Get the name of this extension. - * - * @return string - */ - public function getName() { - return 'userfrosting/core'; - } - - /** - * Adds Twig functions `getAlerts` and `translate`. - * - * @return array[\Twig_SimpleFunction] - */ - public function getFunctions() { - return array( - // Add Twig function for fetching alerts - new \Twig_SimpleFunction('getAlerts', function ($clear = TRUE) { - if ($clear) { - return $this->services['alerts']->getAndClearMessages(); - } else { - return $this->services['alerts']->messages(); - } - }), - new \Twig_SimpleFunction('translate', function ($hook, $params = array()) { - return $this->services['translator']->translate($hook, $params); - }, [ - 'is_safe' => ['html'] - ]) - ); - } - - /** - * Adds Twig filters `unescape`. - * - * @return array[\Twig_SimpleFilter] - */ - public function getFilters() { - return array( - /** - * Converts phone numbers to a standard format. - * - * @param String $num A unformatted phone number - * @return String Returns the formatted phone number - */ - new \Twig_SimpleFilter('phone', function ($num) { - return Util::formatPhoneNumber($num); - }), - new \Twig_SimpleFilter('unescape', function ($string) { - return html_entity_decode($string); - }) - ); - } - - /** - * Adds Twig global variables `site` and `assets`. - * - * @return array[mixed] - */ - public function getGlobals() { - // CSRF token name and value - $csrfNameKey = $this->services->csrf->getTokenNameKey(); - $csrfValueKey = $this->services->csrf->getTokenValueKey(); - $csrfName = $this->services->csrf->getTokenName(); - $csrfValue = $this->services->csrf->getTokenValue(); - - $csrf = [ - 'csrf' => [ - 'keys' => [ - 'name' => $csrfNameKey, - 'value' => $csrfValueKey - ], - 'name' => $csrfName, - 'value' => $csrfValue - ] - ]; - - $site = array_replace_recursive($this->services->config['site'], $csrf); - - return [ - 'site' => $site, - 'assets' => $this->services->assets - ]; - } -} +services = $services; + } + + /** + * Get the name of this extension. + * + * @return string + */ + public function getName() { + return 'userfrosting/core'; + } + + /** + * Adds Twig functions `getAlerts` and `translate`. + * + * @return array[\Twig_SimpleFunction] + */ + public function getFunctions() { + return array( + // Add Twig function for fetching alerts + new \Twig_SimpleFunction('getAlerts', function ($clear = TRUE) { + if ($clear) { + return $this->services['alerts']->getAndClearMessages(); + } else { + return $this->services['alerts']->messages(); + } + }), + new \Twig_SimpleFunction('translate', function ($hook, $params = array()) { + return $this->services['translator']->translate($hook, $params); + }, [ + 'is_safe' => ['html'] + ]) + ); + } + + /** + * Adds Twig filters `unescape`. + * + * @return array[\Twig_SimpleFilter] + */ + public function getFilters() { + return array( + /** + * Converts phone numbers to a standard format. + * + * @param String $num A unformatted phone number + * @return String Returns the formatted phone number + */ + new \Twig_SimpleFilter('phone', function ($num) { + return Util::formatPhoneNumber($num); + }), + new \Twig_SimpleFilter('unescape', function ($string) { + return html_entity_decode($string); + }) + ); + } + + /** + * Adds Twig global variables `site` and `assets`. + * + * @return array[mixed] + */ + public function getGlobals() { + // CSRF token name and value + $csrfNameKey = $this->services->csrf->getTokenNameKey(); + $csrfValueKey = $this->services->csrf->getTokenValueKey(); + $csrfName = $this->services->csrf->getTokenName(); + $csrfValue = $this->services->csrf->getTokenValue(); + + $csrf = [ + 'csrf' => [ + 'keys' => [ + 'name' => $csrfNameKey, + 'value' => $csrfValueKey + ], + 'name' => $csrfName, + 'value' => $csrfValue + ] + ]; + + $site = array_replace_recursive($this->services->config['site'], $csrf); + + return [ + 'site' => $site, + 'assets' => $this->services->assets + ]; + } +} diff --git a/main/app/sprinkles/core/src/Util/BadClassNameException.php b/main/app/sprinkles/core/src/Util/BadClassNameException.php index 1cd6f4e..1271c44 100644 --- a/main/app/sprinkles/core/src/Util/BadClassNameException.php +++ b/main/app/sprinkles/core/src/Util/BadClassNameException.php @@ -1,19 +1,19 @@ -session = $session; - $this->key = $key; - - if (!$this->session->has($key)) { - $this->session[$key] = array(); - } - } - - /** - * Generates a new captcha for the user registration form. - * - * This generates a random 5-character captcha and stores it in the session with an md5 hash. - * Also, generates the corresponding captcha image. - */ - public function generateRandomCode() { - $md5_hash = md5(rand(0, 99999)); - $this->code = substr($md5_hash, 25, 5); - $enc = md5($this->code); - - // Store the generated captcha value to the session - $this->session[$this->key] = $enc; - - $this->generateImage(); - } - - /** - * Returns the captcha code. - */ - public function getCaptcha() { - return $this->code; - } - - /** - * Returns the captcha image. - */ - public function getImage() { - return $this->image; - } - - /** - * Check that the specified code, when hashed, matches the code in the session. - * - * Also, stores the specified code in the session with an md5 hash. - * @param string - * @return bool - */ - public function verifyCode($code) { - return (md5($code) == $this->session[$this->key]); - } - - /** - * Generate the image for the current captcha. - * - * This generates an image as a binary string. - */ - protected function generateImage() { - $width = 150; - $height = 30; - - $image = imagecreatetruecolor(150, 30); - - //color pallette - $white = imagecolorallocate($image, 255, 255, 255); - $black = imagecolorallocate($image, 0, 0, 0); - $red = imagecolorallocate($image, 255, 0, 0); - $yellow = imagecolorallocate($image, 255, 255, 0); - $dark_grey = imagecolorallocate($image, 64, 64, 64); - $blue = imagecolorallocate($image, 0, 0, 255); - - //create white rectangle - imagefilledrectangle($image, 0, 0, 150, 30, $white); - - //add some lines - for ($i = 0; $i < 2; $i++) { - imageline($image, 0, rand() % 10, 10, rand() % 30, $dark_grey); - imageline($image, 0, rand() % 30, 150, rand() % 30, $red); - imageline($image, 0, rand() % 30, 150, rand() % 30, $yellow); - } - - // RandTab color pallette - $randc[0] = imagecolorallocate($image, 0, 0, 0); - $randc[1] = imagecolorallocate($image, 255, 0, 0); - $randc[2] = imagecolorallocate($image, 255, 255, 0); - $randc[3] = imagecolorallocate($image, 64, 64, 64); - $randc[4] = imagecolorallocate($image, 0, 0, 255); - - //add some dots - for ($i = 0; $i < 1000; $i++) { - imagesetpixel($image, rand() % 200, rand() % 50, $randc[rand() % 5]); - } - - //calculate center of text - $x = (150 - 0 - imagefontwidth(5) * strlen($this->code)) / 2 + 0 + 5; - - //write string twice - imagestring($image, 5, $x, 7, $this->code, $black); - imagestring($image, 5, $x, 7, $this->code, $black); - //start ob - ob_start(); - imagepng($image); - - //get binary image data - $this->image = ob_get_clean(); - - return $this->image; - } -} +session = $session; + $this->key = $key; + + if (!$this->session->has($key)) { + $this->session[$key] = array(); + } + } + + /** + * Generates a new captcha for the user registration form. + * + * This generates a random 5-character captcha and stores it in the session with an md5 hash. + * Also, generates the corresponding captcha image. + */ + public function generateRandomCode() { + $md5_hash = md5(rand(0, 99999)); + $this->code = substr($md5_hash, 25, 5); + $enc = md5($this->code); + + // Store the generated captcha value to the session + $this->session[$this->key] = $enc; + + $this->generateImage(); + } + + /** + * Returns the captcha code. + */ + public function getCaptcha() { + return $this->code; + } + + /** + * Returns the captcha image. + */ + public function getImage() { + return $this->image; + } + + /** + * Check that the specified code, when hashed, matches the code in the session. + * + * Also, stores the specified code in the session with an md5 hash. + * @param string + * @return bool + */ + public function verifyCode($code) { + return (md5($code) == $this->session[$this->key]); + } + + /** + * Generate the image for the current captcha. + * + * This generates an image as a binary string. + */ + protected function generateImage() { + $width = 150; + $height = 30; + + $image = imagecreatetruecolor(150, 30); + + //color pallette + $white = imagecolorallocate($image, 255, 255, 255); + $black = imagecolorallocate($image, 0, 0, 0); + $red = imagecolorallocate($image, 255, 0, 0); + $yellow = imagecolorallocate($image, 255, 255, 0); + $dark_grey = imagecolorallocate($image, 64, 64, 64); + $blue = imagecolorallocate($image, 0, 0, 255); + + //create white rectangle + imagefilledrectangle($image, 0, 0, 150, 30, $white); + + //add some lines + for ($i = 0; $i < 2; $i++) { + imageline($image, 0, rand() % 10, 10, rand() % 30, $dark_grey); + imageline($image, 0, rand() % 30, 150, rand() % 30, $red); + imageline($image, 0, rand() % 30, 150, rand() % 30, $yellow); + } + + // RandTab color pallette + $randc[0] = imagecolorallocate($image, 0, 0, 0); + $randc[1] = imagecolorallocate($image, 255, 0, 0); + $randc[2] = imagecolorallocate($image, 255, 255, 0); + $randc[3] = imagecolorallocate($image, 64, 64, 64); + $randc[4] = imagecolorallocate($image, 0, 0, 255); + + //add some dots + for ($i = 0; $i < 1000; $i++) { + imagesetpixel($image, rand() % 200, rand() % 50, $randc[rand() % 5]); + } + + //calculate center of text + $x = (150 - 0 - imagefontwidth(5) * strlen($this->code)) / 2 + 0 + 5; + + //write string twice + imagestring($image, 5, $x, 7, $this->code, $black); + imagestring($image, 5, $x, 7, $this->code, $black); + //start ob + ob_start(); + imagepng($image); + + //get binary image data + $this->image = ob_get_clean(); + + return $this->image; + } +} diff --git a/main/app/sprinkles/core/src/Util/CheckEnvironment.php b/main/app/sprinkles/core/src/Util/CheckEnvironment.php index 05b555f..b8e5ec8 100644 --- a/main/app/sprinkles/core/src/Util/CheckEnvironment.php +++ b/main/app/sprinkles/core/src/Util/CheckEnvironment.php @@ -1,331 +1,331 @@ -view = $view; - $this->locator = $locator; - $this->cache = $cache; - } - - /** - * Invoke the CheckEnvironment middleware, performing all pre-flight checks and returning an error page if problems were found. - * - * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request - * @param \Psr\Http\Message\ResponseInterface $response PSR7 response - * @param callable $next Next middleware - * - * @return \Psr\Http\Message\ResponseInterface - */ - public function __invoke($request, $response, $next) { - $problemsFound = FALSE; - - // If production environment and no cached checks, perform environment checks - if ($this->isProduction() && $this->cache->get('checkEnvironment') != 'pass') { - $problemsFound = $this->checkAll(); - - // Cache if checks passed - if (!$problemsFound) { - $this->cache->forever('checkEnvironment', 'pass'); - } - } else if (!$this->isProduction()) { - $problemsFound = $this->checkAll(); - } - - if ($problemsFound) { - $results = array_merge($this->resultsFailed, $this->resultsSuccess); - - $response = $this->view->render($response, 'pages/error/config-errors.html.twig', [ - "messages" => $results - ]); - } else { - $response = $next($request, $response); - } - - return $response; - } - - /** - * Run through all pre-flight checks. - */ - public function checkAll() { - $problemsFound = FALSE; - - if ($this->checkApache()) $problemsFound = TRUE; - - if ($this->checkPhp()) $problemsFound = TRUE; - - if ($this->checkPdo()) $problemsFound = TRUE; - - if ($this->checkGd()) $problemsFound = TRUE; - - if ($this->checkImageFunctions()) $problemsFound = TRUE; - - if ($this->checkPermissions()) $problemsFound = TRUE; - - return $problemsFound; - } - - /** - * For Apache environments, check that required Apache modules are installed. - */ - public function checkApache() { - $problemsFound = FALSE; - - // Perform some Apache checks. We may also need to do this before any routing takes place. - if (strpos(php_sapi_name(), 'apache') !== FALSE) { - - $require_apache_modules = ['mod_rewrite']; - $apache_modules = apache_get_modules(); - - $apache_status = []; - - foreach ($require_apache_modules as $module) { - if (!in_array($module, $apache_modules)) { - $problemsFound = TRUE; - $this->resultsFailed['apache-' . $module] = [ - "title" => " Missing Apache module $module.", - "message" => "Please make sure that the $module Apache module is installed and enabled. If you use shared hosting, you will need to ask your web host to do this for you.", - "success" => FALSE - ]; - } else { - $this->resultsSuccess['apache-' . $module] = [ - "title" => " Apache module $module is installed and enabled.", - "message" => "Great, we found the $module Apache module!", - "success" => TRUE - ]; - } - } - } - - return $problemsFound; - } - - /** - * Check for GD library (required for Captcha). - */ - public function checkGd() { - $problemsFound = FALSE; - - if (!(extension_loaded('gd') && function_exists('gd_info'))) { - $problemsFound = TRUE; - $this->resultsFailed['gd'] = [ - "title" => " GD library not installed", - "message" => "We could not confirm that the GD library is installed and enabled. GD is an image processing library that UserFrosting uses to generate captcha codes for user account registration.", - "success" => FALSE - ]; - } else { - $this->resultsSuccess['gd'] = [ - "title" => " GD library installed!", - "message" => "Great, you have GD installed and enabled.", - "success" => TRUE - ]; - } - - return $problemsFound; - } - - /** - * Check that all image* functions used by Captcha exist. - * - * Some versions of GD are missing one or more of these functions, thus why we check for them explicitly. - */ - public function checkImageFunctions() { - $problemsFound = FALSE; - - $funcs = [ - 'imagepng', - 'imagecreatetruecolor', - 'imagecolorallocate', - 'imagefilledrectangle', - 'imageline', - 'imagesetpixel', - 'imagefontwidth', - 'imagestring' - ]; - - foreach ($funcs as $func) { - if (!function_exists($func)) { - $problemsFound = TRUE; - $this->resultsFailed['function-' . $func] = [ - "title" => " Missing image manipulation function.", - "message" => "It appears that function $func is not available. UserFrosting needs this to render captchas.", - "success" => FALSE - ]; - } else { - $this->resultsSuccess['function-' . $func] = [ - "title" => " Function $func is available!", - "message" => "Sweet!", - "success" => TRUE - ]; - } - } - - return $problemsFound; - } - - /** - * Check that PDO is installed and enabled. - */ - public function checkPdo() { - $problemsFound = FALSE; - - if (!class_exists('PDO')) { - $problemsFound = TRUE; - $this->resultsFailed['pdo'] = [ - "title" => " PDO is not installed.", - "message" => "I'm sorry, you must have PDO installed and enabled in order for UserFrosting to access the database. If you don't know what PDO is, please see http://php.net/manual/en/book.pdo.php.", - "success" => FALSE - ]; - } else { - $this->resultsSuccess['pdo'] = [ - "title" => " PDO is installed!", - "message" => "You've got PDO installed. Good job!", - "success" => TRUE - ]; - } - - return $problemsFound; - } - - /** - * Check that log, cache, and session directories are writable, and that other directories are set appropriately for the environment. - */ - function checkPermissions() { - $problemsFound = FALSE; - - $shouldBeWriteable = [ - $this->locator->findResource('log://') => TRUE, - $this->locator->findResource('cache://') => TRUE, - $this->locator->findResource('session://') => TRUE - ]; - - if ($this->isProduction()) { - // Should be write-protected in production! - $shouldBeWriteable = array_merge($shouldBeWriteable, [ - \UserFrosting\SPRINKLES_DIR => FALSE, - \UserFrosting\VENDOR_DIR => FALSE - ]); - } - - // Check for essential files & perms - foreach ($shouldBeWriteable as $file => $assertWriteable) { - $is_dir = FALSE; - if (!file_exists($file)) { - $problemsFound = TRUE; - $this->resultsFailed['file-' . $file] = [ - "title" => " File or directory does not exist.", - "message" => "We could not find the file or directory $file.", - "success" => FALSE - ]; - } else { - $writeable = is_writable($file); - if ($assertWriteable !== $writeable) { - $problemsFound = TRUE; - $this->resultsFailed['file-' . $file] = [ - "title" => " Incorrect permissions for file or directory.", - "message" => "$file is " - . ($writeable ? "writeable" : "not writeable") - . ", but it should " - . ($assertWriteable ? "be writeable" : "not be writeable") - . ". Please modify the OS user or group permissions so that user " - . exec('whoami') . " " - . ($assertWriteable ? "has" : "does not have") . " write permissions for this directory.", - "success" => FALSE - ]; - } else { - $this->resultsSuccess['file-' . $file] = [ - "title" => " File/directory check passed!", - "message" => "$file exists and is correctly set as " - . ($writeable ? "writeable" : "not writeable") - . ".", - "success" => TRUE - ]; - } - } - } - return $problemsFound; - } - - /** - * Check that PHP meets the minimum required version. - */ - public function checkPhp() { - $problemsFound = FALSE; - - // Check PHP version - if (version_compare(phpversion(), \UserFrosting\PHP_MIN_VERSION, '<')) { - $problemsFound = TRUE; - $this->resultsFailed['phpVersion'] = [ - "title" => " You need to upgrade your PHP installation.", - "message" => "I'm sorry, UserFrosting requires version " . \UserFrosting\PHP_MIN_VERSION . " or greater. Please upgrade your version of PHP, or contact your web hosting service and ask them to upgrade it for you.", - "success" => FALSE - ]; - } else { - $this->resultsSuccess['phpVersion'] = [ - "title" => " PHP version checks out!", - "message" => "You're using PHP " . \UserFrosting\PHP_MIN_VERSION . "or higher. Great!", - "success" => TRUE - ]; - } - - return $problemsFound; - } - - /** - * Determine whether or not we are running in production mode. - * - * @return bool - */ - public function isProduction() { - return (getenv('UF_MODE') == 'production'); - } -} +view = $view; + $this->locator = $locator; + $this->cache = $cache; + } + + /** + * Invoke the CheckEnvironment middleware, performing all pre-flight checks and returning an error page if problems were found. + * + * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request + * @param \Psr\Http\Message\ResponseInterface $response PSR7 response + * @param callable $next Next middleware + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke($request, $response, $next) { + $problemsFound = FALSE; + + // If production environment and no cached checks, perform environment checks + if ($this->isProduction() && $this->cache->get('checkEnvironment') != 'pass') { + $problemsFound = $this->checkAll(); + + // Cache if checks passed + if (!$problemsFound) { + $this->cache->forever('checkEnvironment', 'pass'); + } + } else if (!$this->isProduction()) { + $problemsFound = $this->checkAll(); + } + + if ($problemsFound) { + $results = array_merge($this->resultsFailed, $this->resultsSuccess); + + $response = $this->view->render($response, 'pages/error/config-errors.html.twig', [ + "messages" => $results + ]); + } else { + $response = $next($request, $response); + } + + return $response; + } + + /** + * Run through all pre-flight checks. + */ + public function checkAll() { + $problemsFound = FALSE; + + if ($this->checkApache()) $problemsFound = TRUE; + + if ($this->checkPhp()) $problemsFound = TRUE; + + if ($this->checkPdo()) $problemsFound = TRUE; + + if ($this->checkGd()) $problemsFound = TRUE; + + if ($this->checkImageFunctions()) $problemsFound = TRUE; + + if ($this->checkPermissions()) $problemsFound = TRUE; + + return $problemsFound; + } + + /** + * For Apache environments, check that required Apache modules are installed. + */ + public function checkApache() { + $problemsFound = FALSE; + + // Perform some Apache checks. We may also need to do this before any routing takes place. + if (strpos(php_sapi_name(), 'apache') !== FALSE) { + + $require_apache_modules = ['mod_rewrite']; + $apache_modules = apache_get_modules(); + + $apache_status = []; + + foreach ($require_apache_modules as $module) { + if (!in_array($module, $apache_modules)) { + $problemsFound = TRUE; + $this->resultsFailed['apache-' . $module] = [ + "title" => " Missing Apache module $module.", + "message" => "Please make sure that the $module Apache module is installed and enabled. If you use shared hosting, you will need to ask your web host to do this for you.", + "success" => FALSE + ]; + } else { + $this->resultsSuccess['apache-' . $module] = [ + "title" => " Apache module $module is installed and enabled.", + "message" => "Great, we found the $module Apache module!", + "success" => TRUE + ]; + } + } + } + + return $problemsFound; + } + + /** + * Check for GD library (required for Captcha). + */ + public function checkGd() { + $problemsFound = FALSE; + + if (!(extension_loaded('gd') && function_exists('gd_info'))) { + $problemsFound = TRUE; + $this->resultsFailed['gd'] = [ + "title" => " GD library not installed", + "message" => "We could not confirm that the GD library is installed and enabled. GD is an image processing library that UserFrosting uses to generate captcha codes for user account registration.", + "success" => FALSE + ]; + } else { + $this->resultsSuccess['gd'] = [ + "title" => " GD library installed!", + "message" => "Great, you have GD installed and enabled.", + "success" => TRUE + ]; + } + + return $problemsFound; + } + + /** + * Check that all image* functions used by Captcha exist. + * + * Some versions of GD are missing one or more of these functions, thus why we check for them explicitly. + */ + public function checkImageFunctions() { + $problemsFound = FALSE; + + $funcs = [ + 'imagepng', + 'imagecreatetruecolor', + 'imagecolorallocate', + 'imagefilledrectangle', + 'imageline', + 'imagesetpixel', + 'imagefontwidth', + 'imagestring' + ]; + + foreach ($funcs as $func) { + if (!function_exists($func)) { + $problemsFound = TRUE; + $this->resultsFailed['function-' . $func] = [ + "title" => " Missing image manipulation function.", + "message" => "It appears that function $func is not available. UserFrosting needs this to render captchas.", + "success" => FALSE + ]; + } else { + $this->resultsSuccess['function-' . $func] = [ + "title" => " Function $func is available!", + "message" => "Sweet!", + "success" => TRUE + ]; + } + } + + return $problemsFound; + } + + /** + * Check that PDO is installed and enabled. + */ + public function checkPdo() { + $problemsFound = FALSE; + + if (!class_exists('PDO')) { + $problemsFound = TRUE; + $this->resultsFailed['pdo'] = [ + "title" => " PDO is not installed.", + "message" => "I'm sorry, you must have PDO installed and enabled in order for UserFrosting to access the database. If you don't know what PDO is, please see http://php.net/manual/en/book.pdo.php.", + "success" => FALSE + ]; + } else { + $this->resultsSuccess['pdo'] = [ + "title" => " PDO is installed!", + "message" => "You've got PDO installed. Good job!", + "success" => TRUE + ]; + } + + return $problemsFound; + } + + /** + * Check that log, cache, and session directories are writable, and that other directories are set appropriately for the environment. + */ + function checkPermissions() { + $problemsFound = FALSE; + + $shouldBeWriteable = [ + $this->locator->findResource('log://') => TRUE, + $this->locator->findResource('cache://') => TRUE, + $this->locator->findResource('session://') => TRUE + ]; + + if ($this->isProduction()) { + // Should be write-protected in production! + $shouldBeWriteable = array_merge($shouldBeWriteable, [ + \UserFrosting\SPRINKLES_DIR => FALSE, + \UserFrosting\VENDOR_DIR => FALSE + ]); + } + + // Check for essential files & perms + foreach ($shouldBeWriteable as $file => $assertWriteable) { + $is_dir = FALSE; + if (!file_exists($file)) { + $problemsFound = TRUE; + $this->resultsFailed['file-' . $file] = [ + "title" => " File or directory does not exist.", + "message" => "We could not find the file or directory $file.", + "success" => FALSE + ]; + } else { + $writeable = is_writable($file); + if ($assertWriteable !== $writeable) { + $problemsFound = TRUE; + $this->resultsFailed['file-' . $file] = [ + "title" => " Incorrect permissions for file or directory.", + "message" => "$file is " + . ($writeable ? "writeable" : "not writeable") + . ", but it should " + . ($assertWriteable ? "be writeable" : "not be writeable") + . ". Please modify the OS user or group permissions so that user " + . exec('whoami') . " " + . ($assertWriteable ? "has" : "does not have") . " write permissions for this directory.", + "success" => FALSE + ]; + } else { + $this->resultsSuccess['file-' . $file] = [ + "title" => " File/directory check passed!", + "message" => "$file exists and is correctly set as " + . ($writeable ? "writeable" : "not writeable") + . ".", + "success" => TRUE + ]; + } + } + } + return $problemsFound; + } + + /** + * Check that PHP meets the minimum required version. + */ + public function checkPhp() { + $problemsFound = FALSE; + + // Check PHP version + if (version_compare(phpversion(), \UserFrosting\PHP_MIN_VERSION, '<')) { + $problemsFound = TRUE; + $this->resultsFailed['phpVersion'] = [ + "title" => " You need to upgrade your PHP installation.", + "message" => "I'm sorry, UserFrosting requires version " . \UserFrosting\PHP_MIN_VERSION . " or greater. Please upgrade your version of PHP, or contact your web hosting service and ask them to upgrade it for you.", + "success" => FALSE + ]; + } else { + $this->resultsSuccess['phpVersion'] = [ + "title" => " PHP version checks out!", + "message" => "You're using PHP " . \UserFrosting\PHP_MIN_VERSION . "or higher. Great!", + "success" => TRUE + ]; + } + + return $problemsFound; + } + + /** + * Determine whether or not we are running in production mode. + * + * @return bool + */ + public function isProduction() { + return (getenv('UF_MODE') == 'production'); + } +} diff --git a/main/app/sprinkles/core/src/Util/ClassMapper.php b/main/app/sprinkles/core/src/Util/ClassMapper.php index 11720f6..e29c524 100644 --- a/main/app/sprinkles/core/src/Util/ClassMapper.php +++ b/main/app/sprinkles/core/src/Util/ClassMapper.php @@ -1,90 +1,90 @@ -getClassMapping($identifier); - - $params = array_slice(func_get_args(), 1); - - // We must use reflection in PHP < 5.6. See http://stackoverflow.com/questions/8734522/dynamically-call-class-with-variable-number-of-parameters-in-the-constructor - $reflection = new \ReflectionClass($className); - - return $reflection->newInstanceArgs($params); - } - - /** - * Gets the fully qualified class name for a specified class identifier. - * - * @param string $identifier - * @return string - */ - public function getClassMapping($identifier) { - if (isset($this->classMappings[$identifier])) { - return $this->classMappings[$identifier]; - } else { - throw new \OutOfBoundsException("There is no class mapped to the identifier '$identifier'."); - } - } - - /** - * Assigns a fully qualified class name to a specified class identifier. - * - * @param string $identifier - * @param string $className - * @return ClassMapper - */ - public function setClassMapping($identifier, $className) { - // Check that class exists - if (!class_exists($className)) { - throw new BadClassNameException("Unable to find the class '$className'."); - } - - $this->classMappings[$identifier] = $className; - - return $this; - } - - /** - * Call a static method for a specified class. - * - * @param string $identifier The identifier for the class, e.g. 'user' - * @param string $methodName The method to be invoked. - * @param mixed ...$arg Whatever needs to be passed to the method. - */ - public function staticMethod($identifier, $methodName) { - $className = $this->getClassMapping($identifier); - - $params = array_slice(func_get_args(), 2); - - return call_user_func_array("$className::$methodName", $params); - } -} +getClassMapping($identifier); + + $params = array_slice(func_get_args(), 1); + + // We must use reflection in PHP < 5.6. See http://stackoverflow.com/questions/8734522/dynamically-call-class-with-variable-number-of-parameters-in-the-constructor + $reflection = new \ReflectionClass($className); + + return $reflection->newInstanceArgs($params); + } + + /** + * Gets the fully qualified class name for a specified class identifier. + * + * @param string $identifier + * @return string + */ + public function getClassMapping($identifier) { + if (isset($this->classMappings[$identifier])) { + return $this->classMappings[$identifier]; + } else { + throw new \OutOfBoundsException("There is no class mapped to the identifier '$identifier'."); + } + } + + /** + * Assigns a fully qualified class name to a specified class identifier. + * + * @param string $identifier + * @param string $className + * @return ClassMapper + */ + public function setClassMapping($identifier, $className) { + // Check that class exists + if (!class_exists($className)) { + throw new BadClassNameException("Unable to find the class '$className'."); + } + + $this->classMappings[$identifier] = $className; + + return $this; + } + + /** + * Call a static method for a specified class. + * + * @param string $identifier The identifier for the class, e.g. 'user' + * @param string $methodName The method to be invoked. + * @param mixed ...$arg Whatever needs to be passed to the method. + */ + public function staticMethod($identifier, $methodName) { + $className = $this->getClassMapping($identifier); + + $params = array_slice(func_get_args(), 2); + + return call_user_func_array("$className::$methodName", $params); + } +} diff --git a/main/app/sprinkles/core/src/Util/EnvironmentInfo.php b/main/app/sprinkles/core/src/Util/EnvironmentInfo.php index 116a59e..e0e9d49 100644 --- a/main/app/sprinkles/core/src/Util/EnvironmentInfo.php +++ b/main/app/sprinkles/core/src/Util/EnvironmentInfo.php @@ -1,67 +1,67 @@ -getPdo(); - $results = []; - - try { - $results['type'] = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); - } catch (Exception $e) { - $results['type'] = "Unknown"; - } - - try { - $results['version'] = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); - } catch (Exception $e) { - $results['version'] = ""; - } - - return $results; - } - - /** - * Test whether a DB connection can be established. - * - * @return bool true if the connection can be established, false otherwise. - */ - public static function canConnectToDatabase() { - try { - Capsule::connection()->getPdo(); - } catch (\PDOException $e) { - return FALSE; - } - - return TRUE; - } -} +getPdo(); + $results = []; + + try { + $results['type'] = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } catch (Exception $e) { + $results['type'] = "Unknown"; + } + + try { + $results['version'] = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); + } catch (Exception $e) { + $results['version'] = ""; + } + + return $results; + } + + /** + * Test whether a DB connection can be established. + * + * @return bool true if the connection can be established, false otherwise. + */ + public static function canConnectToDatabase() { + try { + Capsule::connection()->getPdo(); + } catch (\PDOException $e) { + return FALSE; + } + + return TRUE; + } +} diff --git a/main/app/sprinkles/core/src/Util/ShutdownHandler.php b/main/app/sprinkles/core/src/Util/ShutdownHandler.php index 5447c8f..18e60ec 100644 --- a/main/app/sprinkles/core/src/Util/ShutdownHandler.php +++ b/main/app/sprinkles/core/src/Util/ShutdownHandler.php @@ -1,162 +1,162 @@ -ci = $ci; - $this->displayErrorInfo = $displayErrorInfo; - } - - /** - * Register this class with the shutdown handler. - * - * @return void - */ - public function register() { - register_shutdown_function([$this, 'fatalHandler']); - } - - /** - * Set up the fatal error handler, so that we get a clean error message and alert instead of a WSOD. - */ - public function fatalHandler() { - $error = error_get_last(); - $fatalErrorTypes = [ - E_ERROR, - E_PARSE, - E_CORE_ERROR, - E_COMPILE_ERROR, - E_RECOVERABLE_ERROR - ]; - - // Handle fatal errors and parse errors - if ($error !== NULL && in_array($error['type'], $fatalErrorTypes)) { - - // Build the appropriate error message (debug or client) - if ($this->displayErrorInfo) { - $errorMessage = $this->buildErrorInfoMessage($error); - } else { - $errorMessage = "Oops, looks like our server might have goofed. If you're an admin, please ensure that php.log_errors is enabled, and then check the PHP error log."; - } - - // For CLI, just print the message and exit. - if (php_sapi_name() === 'cli') { - exit($errorMessage . PHP_EOL); - } - - // For all other environments, print a debug response for the requested data type - echo $this->buildErrorPage($errorMessage); - - // If this is an AJAX request and AJAX debugging is turned off, write message to the alert stream - if ($this->ci->request->isXhr() && !$this->ci->config['site.debug.ajax']) { - if ($this->ci->alerts && is_object($this->ci->alerts)) { - $this->ci->alerts->addMessageTranslated('danger', $errorMessage); - } - } - - header('HTTP/1.1 500 Internal Server Error'); - exit(); - } - } - - /** - * Build the error message string. - * - * @param array $error - * @return string - */ - protected function buildErrorInfoMessage(array $error) { - $errfile = $error['file']; - $errline = (string)$error['line']; - $errstr = $error['message']; - - $errorTypes = [ - E_ERROR => 'Fatal error', - E_PARSE => 'Parse error', - E_CORE_ERROR => 'PHP core error', - E_COMPILE_ERROR => 'Zend compile error', - E_RECOVERABLE_ERROR => 'Catchable fatal error' - ]; - - return "" . $errorTypes[$error['type']] . ": $errstr in $errfile on line $errline"; - } - - /** - * Build an error response of the appropriate type as determined by the request's Accept header. - * - * @param string $message - * @return string - */ - protected function buildErrorPage($message) { - $contentType = $this->determineContentType($this->ci->request, $this->ci->config['site.debug.ajax']); - - switch ($contentType) { - case 'application/json': - $error = ['message' => $message]; - return json_encode($error, JSON_PRETTY_PRINT); - - case 'text/html': - return $this->buildHtmlErrorPage($message); - - default: - case 'text/plain': - return $message; - } - } - - /** - * Build an HTML error page from an error string. - * - * @param string $errorMessage - * @return string - */ - protected function buildHtmlErrorPage($message) { - $title = 'UserFrosting Application Error'; - $html = "

$message

"; - - return sprintf( - "" . - "%s

%s

%s", - $title, - $title, - $html - ); - } -} +ci = $ci; + $this->displayErrorInfo = $displayErrorInfo; + } + + /** + * Register this class with the shutdown handler. + * + * @return void + */ + public function register() { + register_shutdown_function([$this, 'fatalHandler']); + } + + /** + * Set up the fatal error handler, so that we get a clean error message and alert instead of a WSOD. + */ + public function fatalHandler() { + $error = error_get_last(); + $fatalErrorTypes = [ + E_ERROR, + E_PARSE, + E_CORE_ERROR, + E_COMPILE_ERROR, + E_RECOVERABLE_ERROR + ]; + + // Handle fatal errors and parse errors + if ($error !== NULL && in_array($error['type'], $fatalErrorTypes)) { + + // Build the appropriate error message (debug or client) + if ($this->displayErrorInfo) { + $errorMessage = $this->buildErrorInfoMessage($error); + } else { + $errorMessage = "Oops, looks like our server might have goofed. If you're an admin, please ensure that php.log_errors is enabled, and then check the PHP error log."; + } + + // For CLI, just print the message and exit. + if (php_sapi_name() === 'cli') { + exit($errorMessage . PHP_EOL); + } + + // For all other environments, print a debug response for the requested data type + echo $this->buildErrorPage($errorMessage); + + // If this is an AJAX request and AJAX debugging is turned off, write message to the alert stream + if ($this->ci->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + if ($this->ci->alerts && is_object($this->ci->alerts)) { + $this->ci->alerts->addMessageTranslated('danger', $errorMessage); + } + } + + header('HTTP/1.1 500 Internal Server Error'); + exit(); + } + } + + /** + * Build the error message string. + * + * @param array $error + * @return string + */ + protected function buildErrorInfoMessage(array $error) { + $errfile = $error['file']; + $errline = (string)$error['line']; + $errstr = $error['message']; + + $errorTypes = [ + E_ERROR => 'Fatal error', + E_PARSE => 'Parse error', + E_CORE_ERROR => 'PHP core error', + E_COMPILE_ERROR => 'Zend compile error', + E_RECOVERABLE_ERROR => 'Catchable fatal error' + ]; + + return "" . $errorTypes[$error['type']] . ": $errstr in $errfile on line $errline"; + } + + /** + * Build an error response of the appropriate type as determined by the request's Accept header. + * + * @param string $message + * @return string + */ + protected function buildErrorPage($message) { + $contentType = $this->determineContentType($this->ci->request, $this->ci->config['site.debug.ajax']); + + switch ($contentType) { + case 'application/json': + $error = ['message' => $message]; + return json_encode($error, JSON_PRETTY_PRINT); + + case 'text/html': + return $this->buildHtmlErrorPage($message); + + default: + case 'text/plain': + return $message; + } + } + + /** + * Build an HTML error page from an error string. + * + * @param string $errorMessage + * @return string + */ + protected function buildHtmlErrorPage($message) { + $title = 'UserFrosting Application Error'; + $html = "

$message

"; + + return sprintf( + "" . + "%s

%s

%s", + $title, + $title, + $html + ); + } +} diff --git a/main/app/sprinkles/core/src/Util/Util.php b/main/app/sprinkles/core/src/Util/Util.php index 0db3b72..0cf3a56 100644 --- a/main/app/sprinkles/core/src/Util/Util.php +++ b/main/app/sprinkles/core/src/Util/Util.php @@ -1,174 +1,174 @@ -' . str_repeat('  ', $newLineLevel); - } - - $result .= $char . $post; - } - - return $result; - } - - /** - * Generate a random phrase, consisting of a specified number of adjectives, followed by a noun. - * - * @param int $numAdjectives - * @param int $maxLength - * @param int $maxTries - * @param string $separator - * @return string - */ - static public function randomPhrase($numAdjectives, $maxLength = 9999999, $maxTries = 10, $separator = '-') { - $adjectives = include('extra://adjectives.php'); - $nouns = include('extra://nouns.php'); - - for ($n = 0; $n < $maxTries; $n++) { - $keys = array_rand($adjectives, $numAdjectives); - $matches = array_only($adjectives, $keys); - - $result = implode($separator, $matches); - $result .= $separator . $nouns[array_rand($nouns)]; - $result = str_slug($result, $separator); - if (strlen($result) < $maxLength) { - return $result; - } - } - - return ''; - } -} +' . str_repeat('  ', $newLineLevel); + } + + $result .= $char . $post; + } + + return $result; + } + + /** + * Generate a random phrase, consisting of a specified number of adjectives, followed by a noun. + * + * @param int $numAdjectives + * @param int $maxLength + * @param int $maxTries + * @param string $separator + * @return string + */ + static public function randomPhrase($numAdjectives, $maxLength = 9999999, $maxTries = 10, $separator = '-') { + $adjectives = include('extra://adjectives.php'); + $nouns = include('extra://nouns.php'); + + for ($n = 0; $n < $maxTries; $n++) { + $keys = array_rand($adjectives, $numAdjectives); + $matches = array_only($adjectives, $keys); + + $result = implode($separator, $matches); + $result .= $separator . $nouns[array_rand($nouns)]; + $result = str_slug($result, $separator); + if (strlen($result) < $maxLength) { + return $result; + } + } + + return ''; + } +} -- cgit v1.2.3