diff --git a/.gitignore b/.gitignore index 6be000bfbf..32c14d01b5 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,10 @@ writable/uploads/* writable/debugbar/* +application/Database/Migrations/2* + +php_errors.log + #------------------------- # User Guide Temp Files #------------------------- @@ -122,3 +126,4 @@ nb-configuration.xml .vscode/ /results/ +/phpunit.xml diff --git a/.travis.yml b/.travis.yml index 9535ab4ec5..b724db7167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ php: matrix: fast_finish: true allow_failures: - - php: 7.2 - php: nightly global: @@ -43,4 +42,4 @@ before_script: - composer install --prefer-source after_success: - - travis_retry php tests/bin/coveralls.phar + - travis_retry php tests/bin/coveralls.phar -v diff --git a/application/Config/App.php b/application/Config/App.php index 8c7e6041ed..94a2bab44a 100644 --- a/application/Config/App.php +++ b/application/Config/App.php @@ -270,6 +270,9 @@ class App extends BaseConfig | and state of your application during that page display. By default it will | NOT be displayed under production environments, and will only display if | CI_DEBUG is true, since if it's not, there's not much to display anyway. + | + | toolbarMaxHistory = Number of history files, 0 for none or -1 for unlimited + | */ public $toolbarCollectors = [ 'CodeIgniter\Debug\Toolbar\Collectors\Timers', @@ -281,6 +284,7 @@ class App extends BaseConfig 'CodeIgniter\Debug\Toolbar\Collectors\Routes', 'CodeIgniter\Debug\Toolbar\Collectors\Events', ]; + public $toolbarMaxHistory = 20; /* |-------------------------------------------------------------------------- diff --git a/application/Config/Autoload.php b/application/Config/Autoload.php index 0dab676ce6..7ac3add13e 100644 --- a/application/Config/Autoload.php +++ b/application/Config/Autoload.php @@ -1,6 +1,6 @@ \App\Filters\CSRF::class, 'toolbar' => \App\Filters\DebugToolbar::class, + 'honeypot' => \App\Filters\Honeypot::class ]; // Always applied before every request public $globals = [ 'before' => [ - // 'csrf' + //'honeypot' + // 'csrf', ], 'after' => [ - 'toolbar' + 'toolbar', + //'honeypot' ] ]; diff --git a/application/Config/Honeypot.php b/application/Config/Honeypot.php new file mode 100644 index 0000000000..d35b0c7ee8 --- /dev/null +++ b/application/Config/Honeypot.php @@ -0,0 +1,31 @@ +{label}'; +} \ No newline at end of file diff --git a/application/Config/Paths.php b/application/Config/Paths.php index c58f91a1ad..f679dca50d 100644 --- a/application/Config/Paths.php +++ b/application/Config/Paths.php @@ -6,7 +6,7 @@ * Modifying these allows you to re-structure your application, * share a system folder between multiple applications, and more. * - * All paths are relative to the application's front controller, index.php + * All paths are relative to the project's root folder. */ class Paths { @@ -19,7 +19,7 @@ class Paths * Include the path if the folder is not in the same directory * as this file. */ - public $systemDirectory = '../system'; + public $systemDirectory = 'system'; /* *--------------------------------------------------------------- @@ -34,7 +34,7 @@ class Paths * * NO TRAILING SLASH! */ - public $applicationDirectory = '../application'; + public $applicationDirectory = 'application'; /* * --------------------------------------------------------------- @@ -47,7 +47,7 @@ class Paths * for maximum security, keeping it out of the application and/or * system directories. */ - public $writableDirectory = '../writable'; + public $writableDirectory = 'writable'; /* * --------------------------------------------------------------- @@ -60,7 +60,7 @@ class Paths * for maximum security, keeping it out of the application and/or * system directories. */ - public $testsDirectory = '../tests'; + public $testsDirectory = 'tests'; /* * --------------------------------------------------------------- diff --git a/application/Config/Services.php b/application/Config/Services.php index 543c2c4aa6..58a93a0a4e 100644 --- a/application/Config/Services.php +++ b/application/Config/Services.php @@ -1,6 +1,7 @@ 'Windows 10', + 'windows nt 6.3' => 'Windows 8.1', + 'windows nt 6.2' => 'Windows 8', + 'windows nt 6.1' => 'Windows 7', + 'windows nt 6.0' => 'Windows Vista', + 'windows nt 5.2' => 'Windows 2003', + 'windows nt 5.1' => 'Windows XP', + 'windows nt 5.0' => 'Windows 2000', + 'windows nt 4.0' => 'Windows NT 4.0', + 'winnt4.0' => 'Windows NT 4.0', + 'winnt 4.0' => 'Windows NT', + 'winnt' => 'Windows NT', + 'windows 98' => 'Windows 98', + 'win98' => 'Windows 98', + 'windows 95' => 'Windows 95', + 'win95' => 'Windows 95', + 'windows phone' => 'Windows Phone', + 'windows' => 'Unknown Windows OS', + 'android' => 'Android', + 'blackberry' => 'BlackBerry', + 'iphone' => 'iOS', + 'ipad' => 'iOS', + 'ipod' => 'iOS', + 'os x' => 'Mac OS X', + 'ppc mac' => 'Power PC Mac', + 'freebsd' => 'FreeBSD', + 'ppc' => 'Macintosh', + 'linux' => 'Linux', + 'debian' => 'Debian', + 'sunos' => 'Sun Solaris', + 'beos' => 'BeOS', + 'apachebench' => 'ApacheBench', + 'aix' => 'AIX', + 'irix' => 'Irix', + 'osf' => 'DEC OSF', + 'hp-ux' => 'HP-UX', + 'netbsd' => 'NetBSD', + 'bsdi' => 'BSDi', + 'openbsd' => 'OpenBSD', + 'gnu' => 'GNU/Linux', + 'unix' => 'Unknown Unix OS', + 'symbian' => 'Symbian OS', + ]; + + + // The order of this array should NOT be changed. Many browsers return + // multiple browser types so we want to identify the sub-type first. + public $browsers = [ + 'OPR' => 'Opera', + 'Flock' => 'Flock', + 'Edge' => 'Spartan', + 'Chrome' => 'Chrome', + // Opera 10+ always reports Opera/9.80 and appends Version/ to the user agent string + 'Opera.*?Version' => 'Opera', + 'Opera' => 'Opera', + 'MSIE' => 'Internet Explorer', + 'Internet Explorer' => 'Internet Explorer', + 'Trident.* rv' => 'Internet Explorer', + 'Shiira' => 'Shiira', + 'Firefox' => 'Firefox', + 'Chimera' => 'Chimera', + 'Phoenix' => 'Phoenix', + 'Firebird' => 'Firebird', + 'Camino' => 'Camino', + 'Netscape' => 'Netscape', + 'OmniWeb' => 'OmniWeb', + 'Safari' => 'Safari', + 'Mozilla' => 'Mozilla', + 'Konqueror' => 'Konqueror', + 'icab' => 'iCab', + 'Lynx' => 'Lynx', + 'Links' => 'Links', + 'hotjava' => 'HotJava', + 'amaya' => 'Amaya', + 'IBrowse' => 'IBrowse', + 'Maxthon' => 'Maxthon', + 'Ubuntu' => 'Ubuntu Web Browser', + 'Vivaldi' => 'Vivaldi', + ]; + + public $mobiles = [ + // legacy array, old values commented out + 'mobileexplorer' => 'Mobile Explorer', + // 'openwave' => 'Open Wave', + // 'opera mini' => 'Opera Mini', + // 'operamini' => 'Opera Mini', + // 'elaine' => 'Palm', + 'palmsource' => 'Palm', + // 'digital paths' => 'Palm', + // 'avantgo' => 'Avantgo', + // 'xiino' => 'Xiino', + 'palmscape' => 'Palmscape', + // 'nokia' => 'Nokia', + // 'ericsson' => 'Ericsson', + // 'blackberry' => 'BlackBerry', + // 'motorola' => 'Motorola' + + // Phones and Manufacturers + 'motorola' => 'Motorola', + 'nokia' => 'Nokia', + 'palm' => 'Palm', + 'iphone' => 'Apple iPhone', + 'ipad' => 'iPad', + 'ipod' => 'Apple iPod Touch', + 'sony' => 'Sony Ericsson', + 'ericsson' => 'Sony Ericsson', + 'blackberry' => 'BlackBerry', + 'cocoon' => 'O2 Cocoon', + 'blazer' => 'Treo', + 'lg' => 'LG', + 'amoi' => 'Amoi', + 'xda' => 'XDA', + 'mda' => 'MDA', + 'vario' => 'Vario', + 'htc' => 'HTC', + 'samsung' => 'Samsung', + 'sharp' => 'Sharp', + 'sie-' => 'Siemens', + 'alcatel' => 'Alcatel', + 'benq' => 'BenQ', + 'ipaq' => 'HP iPaq', + 'mot-' => 'Motorola', + 'playstation portable' => 'PlayStation Portable', + 'playstation 3' => 'PlayStation 3', + 'playstation vita' => 'PlayStation Vita', + 'hiptop' => 'Danger Hiptop', + 'nec-' => 'NEC', + 'panasonic' => 'Panasonic', + 'philips' => 'Philips', + 'sagem' => 'Sagem', + 'sanyo' => 'Sanyo', + 'spv' => 'SPV', + 'zte' => 'ZTE', + 'sendo' => 'Sendo', + 'nintendo dsi' => 'Nintendo DSi', + 'nintendo ds' => 'Nintendo DS', + 'nintendo 3ds' => 'Nintendo 3DS', + 'wii' => 'Nintendo Wii', + 'open web' => 'Open Web', + 'openweb' => 'OpenWeb', + + // Operating Systems + 'android' => 'Android', + 'symbian' => 'Symbian', + 'SymbianOS' => 'SymbianOS', + 'elaine' => 'Palm', + 'series60' => 'Symbian S60', + 'windows ce' => 'Windows CE', + + // Browsers + 'obigo' => 'Obigo', + 'netfront' => 'Netfront Browser', + 'openwave' => 'Openwave Browser', + 'mobilexplorer' => 'Mobile Explorer', + 'operamini' => 'Opera Mini', + 'opera mini' => 'Opera Mini', + 'opera mobi' => 'Opera Mobile', + 'fennec' => 'Firefox Mobile', + + // Other + 'digital paths' => 'Digital Paths', + 'avantgo' => 'AvantGo', + 'xiino' => 'Xiino', + 'novarra' => 'Novarra Transcoder', + 'vodafone' => 'Vodafone', + 'docomo' => 'NTT DoCoMo', + 'o2' => 'O2', + + // Fallback + 'mobile' => 'Generic Mobile', + 'wireless' => 'Generic Mobile', + 'j2me' => 'Generic Mobile', + 'midp' => 'Generic Mobile', + 'cldc' => 'Generic Mobile', + 'up.link' => 'Generic Mobile', + 'up.browser' => 'Generic Mobile', + 'smartphone' => 'Generic Mobile', + 'cellphone' => 'Generic Mobile', + ]; + + // There are hundreds of bots but these are the most common. + public $robots = [ + 'googlebot' => 'Googlebot', + 'msnbot' => 'MSNBot', + 'baiduspider' => 'Baiduspider', + 'bingbot' => 'Bing', + 'slurp' => 'Inktomi Slurp', + 'yahoo' => 'Yahoo', + 'ask jeeves' => 'Ask Jeeves', + 'fastcrawler' => 'FastCrawler', + 'infoseek' => 'InfoSeek Robot 1.0', + 'lycos' => 'Lycos', + 'yandex' => 'YandexBot', + 'mediapartners-google' => 'MediaPartners Google', + 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler', + 'adsbot-google' => 'AdsBot Google', + 'feedfetcher-google' => 'Feedfetcher Google', + 'curious george' => 'Curious George', + 'ia_archiver' => 'Alexa Crawler', + 'MJ12bot' => 'Majestic-12', + 'Uptimebot' => 'Uptimebot', + ]; +} diff --git a/application/Filters/DebugToolbar.php b/application/Filters/DebugToolbar.php index 61bc520e55..945108013c 100644 --- a/application/Filters/DebugToolbar.php +++ b/application/Filters/DebugToolbar.php @@ -33,15 +33,13 @@ class DebugToolbar implements FilterInterface */ public function after(RequestInterface $request, ResponseInterface $response) { - $format = $response->getHeaderLine('content-type'); - - if ( ! is_cli() && CI_DEBUG && strpos($format, 'html') !== false) + if ( ! is_cli() && CI_DEBUG) { global $app; $toolbar = Services::toolbar(new App()); $stats = $app->getPerformanceStats(); - $output = $toolbar->run( + $data = $toolbar->run( $stats['startTime'], $stats['totalTime'], $stats['startMemory'], @@ -49,7 +47,7 @@ class DebugToolbar implements FilterInterface $response ); - helper(['filesystem', 'url']); + helper('filesystem'); // Updated to time() so we can get history $time = time(); @@ -59,7 +57,19 @@ class DebugToolbar implements FilterInterface mkdir(WRITEPATH.'debugbar', 0777); } - write_file(WRITEPATH .'debugbar/'.'debugbar_' . $time, $output, 'w+'); + write_file(WRITEPATH .'debugbar/'.'debugbar_' . $time, $data, 'w+'); + + $format = $response->getHeaderLine('content-type'); + + // Non-HTML formats should not include the debugbar + // then we send headers saying where to find the debug data + // for this response + if ($request->isAJAX() || strpos($format, 'html') === false) + { + return $response->setHeader('Debugbar-Time', (string)$time) + ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}")) + ->getBody(); + } $script = PHP_EOL . ''); + http_response_code(404); + exit(); // Exit here is needed to avoid load the index page } } } diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php index ae03ab287c..c30051dda6 100644 --- a/system/Debug/Toolbar/Collectors/BaseCollector.php +++ b/system/Debug/Toolbar/Collectors/BaseCollector.php @@ -229,14 +229,13 @@ class BaseCollector //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string + public function display(): array { - return ''; + return []; } //-------------------------------------------------------------------- diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php index 63ff8e5163..9e290100d3 100644 --- a/system/Debug/Toolbar/Collectors/Config.php +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -9,24 +9,17 @@ class Config public static function display() { $config = new App(); - $parser = \Config\Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null,false); - $data = [ - 'ciVersion' => CodeIgniter::CI_VERSION, - 'phpVersion' => phpversion(), - 'phpSAPI' => php_sapi_name(), + return [ + 'ciVersion' => CodeIgniter::CI_VERSION, + 'phpVersion' => phpversion(), + 'phpSAPI' => php_sapi_name(), 'environment' => ENVIRONMENT, - 'baseURL' => $config->baseURL, - 'timezone' => app_timezone(), - 'locale' => Services::request()->getLocale(), - 'cspEnabled' => $config->CSPEnabled, - 'salt' => $config->salt, + 'baseURL' => $config->baseURL, + 'timezone' => app_timezone(), + 'locale' => Services::request()->getLocale(), + 'cspEnabled' => $config->CSPEnabled, + 'salt' => $config->salt, ]; - - - $output = $parser->setData($data) - ->render('_config.tpl'); - - return $output; } } diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index a5a1bd42d1..21a81962ec 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -149,11 +149,11 @@ class Database extends BaseCollector //-------------------------------------------------------------------- /** - * Returns the HTML to fill the Database tab in the toolbar. + * Returns the data of this collector to be formatted in the toolbar * - * @return string The data formatted for the toolbar. + * @return array */ - public function display(): string + public function display(): array { // Key words we want bolded $highlight = ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'AND', 'LEFT JOIN', 'ORDER BY', 'GROUP BY', @@ -161,8 +161,6 @@ class Database extends BaseCollector 'IN', 'LIKE', 'NOT LIKE', 'COUNT', 'MAX', 'MIN', 'ON', 'AS', 'AVG', 'SUM', '(', ')' ]; - $parser = \Config\Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null,false); - $data = [ 'queries' => [] ]; @@ -182,10 +180,7 @@ class Database extends BaseCollector ]; } - $output = $parser->setData($data) - ->render('_database.tpl'); - - return $output; + return $data; } //-------------------------------------------------------------------- @@ -236,9 +231,7 @@ class Database extends BaseCollector */ public function icon(): string { - return << -EOD; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADMSURBVEhLY6A3YExLSwsA4nIycQDIDIhRWEBqamo/UNF/SjDQjF6ocZgAKPkRiFeEhoYyQ4WIBiA9QAuWAPEHqBAmgLqgHcolGQD1V4DMgHIxwbCxYD+QBqcKINseKo6eWrBioPrtQBq/BcgY5ht0cUIYbBg2AJKkRxCNWkDQgtFUNJwtABr+F6igE8olGQD114HMgHIxAVDyAhA/AlpSA8RYUwoeXAPVex5qHCbIyMgwBCkAuQJIY00huDBUz/mUlBQDqHGjgBjAwAAACexpph6oHSQAAAAASUVORK5CYII='; } diff --git a/system/Debug/Toolbar/Collectors/Events.php b/system/Debug/Toolbar/Collectors/Events.php index 682c10f5d4..e4125c39e8 100644 --- a/system/Debug/Toolbar/Collectors/Events.php +++ b/system/Debug/Toolbar/Collectors/Events.php @@ -122,14 +122,12 @@ class Events extends BaseCollector //-------------------------------------------------------------------- /** - * Returns the HTML to fill the Events tab in the toolbar. + * Returns the data of this collector to be formatted in the toolbar * - * @return string The data formatted for the toolbar. + * @return array */ - public function display(): string + public function display(): array { - $parser = \Config\Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null,false); - $data = [ 'events' => [] ]; @@ -153,10 +151,7 @@ class Events extends BaseCollector $data['events'][$key]['count']++; } - $output = $parser->setData($data) - ->render('_events.tpl'); - - return $output; + return $data; } //-------------------------------------------------------------------- @@ -180,9 +175,7 @@ class Events extends BaseCollector */ public function icon(): string { - return << -EOD; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEASURBVEhL7ZXNDcIwDIVTsRBH1uDQDdquUA6IM1xgCA6MwJUN2hk6AQzAz0vl0ETUxC5VT3zSU5w81/mRMGZysixbFEVR0jSKNt8geQU9aRpFmp/keX6AbjZ5oB74vsaN5lSzA4tLSjpBFxsjeSuRy4d2mDdQTWU7YLbXTNN05mKyovj5KL6B7q3hoy3KwdZxBlT+Ipz+jPHrBqOIynZgcZonoukb/0ckiTHqNvDXtXEAaygRbaB9FvUTjRUHsIYS0QaSp+Dw6wT4hiTmYHOcYZsdLQ2CbXa4ftuuYR4x9vYZgdb4vsFYUdmABMYeukK9/SUme3KMFQ77+Yfzh8eYF8+orDuDWU5LAAAAAElFTkSuQmCC'; } } diff --git a/system/Debug/Toolbar/Collectors/Files.php b/system/Debug/Toolbar/Collectors/Files.php index ed7f918210..3e565a5c6d 100644 --- a/system/Debug/Toolbar/Collectors/Files.php +++ b/system/Debug/Toolbar/Collectors/Files.php @@ -81,15 +81,12 @@ class Files extends BaseCollector //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string + public function display(): array { - $parser = \Config\Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null, false); - $rawFiles = get_included_files(); $coreFiles = []; $userFiles = []; @@ -117,11 +114,10 @@ class Files extends BaseCollector sort($userFiles); sort($coreFiles); - return $parser->setData([ - 'coreFiles' => $coreFiles, - 'userFiles' => $userFiles, - ]) - ->render('_files.tpl'); + return [ + 'coreFiles' => $coreFiles, + 'userFiles' => $userFiles, + ]; } //-------------------------------------------------------------------- @@ -147,9 +143,7 @@ class Files extends BaseCollector */ public function icon(): string { - return << -EOD; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVEhL7ZQ9S8NQGIVTBQUncfMfCO4uLgoKbuKQOWg+OkXERRE1IAXrIHbVDrqIDuLiJgj+gro7S3dnpfq88b1FMTE3VZx64HBzzvvZWxKnj15QCcPwCD5HUfSWR+JtzgmtsUcQBEva5IIm9SwSu+95CAWbUuy67qBa32ByZEDpIaZYZSZMjjQuPcQUq8yEyYEb8FSerYeQVGbAFzJkX1PyQWLhgCz0BxTCekC1Wp0hsa6yokzhed4oje6Iz6rlJEkyIKfUEFtITVtQdAibn5rMyaYsMS+a5wTv8qeXMhcU16QZbKgl3hbs+L4/pnpdc87MElZgq10p5DxGdq8I7xrvUWUKvG3NbSK7ubngYzdJwSsF7TiOh9VOgfcEz1UayNe3JUPM1RWC5GXYgTfc75B4NBmXJnAtTfpABX0iPvEd9ezALwkplCFXcr9styiNOKc1RRZpaPM9tcqBwlWzGY1qPL9wjqRBgF5BH6j8HWh2S7MHlX8PrmbK+k/8PzjOOzx1D3i1pKTTAAAAAElFTkSuQmCC'; } } diff --git a/system/Debug/Toolbar/Collectors/Logs.php b/system/Debug/Toolbar/Collectors/Logs.php index f28f330185..bd15bfef4d 100644 --- a/system/Debug/Toolbar/Collectors/Logs.php +++ b/system/Debug/Toolbar/Collectors/Logs.php @@ -77,26 +77,22 @@ class Logs extends BaseCollector //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string + public function display(): array { - $this->collectLogs(); - - $parser = \Config\Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null, false); + $logs = $this->collectLogs(); if (empty($logs) || ! is_array($logs)) { - return '

Nothing was logged. If you were expecting logged items, ensure that LoggerConfig file has the correct threshold set.

'; + $logs = []; } - return $parser->setData([ - 'logs' => $logs - ]) - ->render('_logs.tpl'); + return [ + 'logs' => $logs + ]; } //-------------------------------------------------------------------- @@ -122,9 +118,7 @@ class Logs extends BaseCollector */ public function icon(): string { - return << -EOD; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACYSURBVEhLYxgFJIHU1FSjtLS0i0D8AYj7gEKMEBkqAaAFF4D4ERCvAFrwH4gDoFIMKSkpFkB+OTEYqgUTACXfA/GqjIwMQyD9H2hRHlQKJFcBEiMGQ7VgAqCBvUgK32dmZspCpagGGNPT0/1BLqeF4bQHQJePpiIwhmrBBEADR1MRfgB0+WgqAmOoFkwANHA0FY0CUgEDAwCQ0PUpNB3kqwAAAABJRU5ErkJggg=='; } diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php index 285b70424e..3bb25fd285 100644 --- a/system/Debug/Toolbar/Collectors/Routes.php +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -70,15 +70,12 @@ class Routes extends BaseCollector //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string + public function display(): array { - $parser = \Config\Services::parser(); - $rawRoutes = Services::routes(true); $router = Services::router(null, true); @@ -126,11 +123,10 @@ class Routes extends BaseCollector ]; } - return $parser->setData([ - 'matchedRoute' => $matchedRoute, - 'routes' => $routes - ]) - ->render('CodeIgniter\Debug\Toolbar\Views\_routes.tpl'); + return [ + 'matchedRoute' => $matchedRoute, + 'routes' => $routes + ]; } //-------------------------------------------------------------------- @@ -158,9 +154,7 @@ class Routes extends BaseCollector */ public function icon(): string { - return << -EOD; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg=='; } } diff --git a/system/Debug/Toolbar/Collectors/Timers.php b/system/Debug/Toolbar/Collectors/Timers.php index 2492ef54ab..6e69ee74c6 100644 --- a/system/Debug/Toolbar/Collectors/Timers.php +++ b/system/Debug/Toolbar/Collectors/Timers.php @@ -98,5 +98,4 @@ class Timers extends BaseCollector return $data; } - //-------------------------------------------------------------------- } diff --git a/system/Debug/Toolbar/Collectors/Views.php b/system/Debug/Toolbar/Collectors/Views.php index 2d7f4119bd..711da495c5 100644 --- a/system/Debug/Toolbar/Collectors/Views.php +++ b/system/Debug/Toolbar/Collectors/Views.php @@ -182,9 +182,7 @@ class Views extends BaseCollector */ public function icon(): string { - return << -EOD; + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII='; } } diff --git a/system/Debug/Toolbar/Views/_history.tpl.php b/system/Debug/Toolbar/Views/_history.tpl.php new file mode 100644 index 0000000000..e777b89d2a --- /dev/null +++ b/system/Debug/Toolbar/Views/_history.tpl.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + {files} + + + + + + + + + + {/files} + +
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
+ + {datetime}{status}{method}{url}{contentType}{isAJAX}
diff --git a/system/Debug/Toolbar/Views/_logs.tpl.php b/system/Debug/Toolbar/Views/_logs.tpl.php index 66e7f71962..4708a02e2f 100644 --- a/system/Debug/Toolbar/Views/_logs.tpl.php +++ b/system/Debug/Toolbar/Views/_logs.tpl.php @@ -1,3 +1,6 @@ +{ if logs == [] } +

Nothing was logged. If you were expecting logged items, ensure that LoggerConfig file has the correct threshold set.

+{ else } @@ -14,3 +17,4 @@ {/logs}
+{ endif } diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index fefee2a48a..079157021d 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -163,6 +163,38 @@ margin-left: 0.5em; } +#debug-bar span.ci-label .badge.active { + background-color: red; +} + +#debug-bar button { + border: 1px solid #ddd; + background-color: #fff; + cursor: pointer; + border-radius: 4px; + color: #333; +} + +#debug-bar button:hover { + background-color: #eaeaea; +} + +#debug-bar tr[data-active="1"] { + background-color: #dff0d8; +} + +#debug-bar tr[data-active="1"]:hover { + background-color: #a7d499; +} + +#debug-bar tr.current { + background-color: #FDC894; +} + +#debug-bar tr.current:hover { + background-color: #DD4814; +} + #debug-bar table strong { font-weight: 500; color: rgba(0, 0, 0, 0.3); diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js index fd0afd151d..944893e10c 100644 --- a/system/Debug/Toolbar/Views/toolbar.js +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -17,11 +17,36 @@ var ciDebugBar = { ciDebugBar.createListeners(); ciDebugBar.setToolbarState(); ciDebugBar.setToolbarPosition(); - ciDebugBar.toogleViewsHints(); - - document.getElementById('debug-bar-link').addEventListener('click', ciDebugBar.toggleToolbar, true); - document.getElementById('debug-icon-link').addEventListener('click', ciDebugBar.toggleToolbar, true); - + ciDebugBar.toggleViewsHints(); + + document.getElementById('debug-bar-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + document.getElementById('debug-icon-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + + // Allows to highlight the row of the current history request + var btn = document.querySelector('button[data-time="'+localStorage.getItem('debugbar-time')+'"]'); + ciDebugBar.addClass(btn.parentNode.parentNode, 'current'); + + historyLoad = document.getElementsByClassName('ci-history-load'); + + for (var i = 0; i < historyLoad.length; i++) + { + historyLoad[i].addEventListener('click', function() { + loadDoc(this.getAttribute('data-time')); + }, true); + } + + // Display the active Tab on page load + var tab = ciDebugBar.readCookie('debug-bar-tab'); + if (document.getElementById(tab)) { + var el = document.getElementById(tab); + el.style.display = 'block'; + ciDebugBar.addClass(el, 'active'); + tab = document.querySelector('[data-tab='+tab+']'); + if (tab) { + ciDebugBar.addClass(tab.parentNode, 'active'); + } + } + }, //-------------------------------------------------------------------- @@ -48,6 +73,9 @@ var ciDebugBar = { return; } + // Remove debug-bar-tab cookie + ciDebugBar.createCookie('debug-bar-tab', '', -1); + // Check our current state. var state = tab.style.display; @@ -72,6 +100,8 @@ var ciDebugBar = { { tab.style.display = 'block'; ciDebugBar.addClass(this.parentNode, 'active'); + // Create debug-bar-tab cookie to persistent state + ciDebugBar.createCookie('debug-bar-tab', this.getAttribute('data-tab'), 365); } }, @@ -156,8 +186,16 @@ var ciDebugBar = { //-------------------------------------------------------------------- - toogleViewsHints: function() + toggleViewsHints: function() { + // Avoid toggle hints on history requests that are not the initial + if (localStorage.getItem('debugbar-time') != localStorage.getItem('debugbar-time-new')) + { + var a = document.querySelector('a[data-tab="ci-views"]'); + a.href = '#'; + return; + } + var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] var sortedComments = []; var comments = []; @@ -399,15 +437,7 @@ var ciDebugBar = { return; } - btn = btn.parentNode; - - // Determine Hints state on page load - if (ciDebugBar.readCookie('debug-view')) - { - showHints(); - } - - btn.onclick = function() { + btn.parentNode.onclick = function() { if (ciDebugBar.readCookie('debug-view')) { hideHints(); @@ -417,6 +447,12 @@ var ciDebugBar = { showHints(); } }; + + // Determine Hints state on page load + if (ciDebugBar.readCookie('debug-view')) + { + showHints(); + } }, //-------------------------------------------------------------------- diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 06fea8c7c7..e1423a752b 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -6,86 +6,86 @@
- + - - - ms   MB - - - collectors as $c) : ?> - isEmpty()) : ?> - hasTabContent() || $c->hasLabel()) : ?> - - - icon() ?> - - getTitle()) ?> - getBadgeValue())) : ?> - getBadgeValue() ?> - - - - - - - + + + ms   MB + + + + + + + + + + + + + + + + + + + - - - Vars - - + + + Vars + + -

- - - - - - - - - - -

+

+ + + + + + + + + + +

- - - - + + + +
@@ -96,107 +96,87 @@ NAME COMPONENT DURATION - + ms - + - renderTimeline($segmentCount, $segmentDuration, $totalTime) ?> +
- collectors as $c) : ?> - isEmpty()) : ?> - hasTabContent()) : ?> -
-

getTitle()) ?> getTitleDetails()) ?>

+ + + +
+

- display() ?> -
- - + setData($c['display'])->render("_{$c['titleSafe']}.tpl") ?> +
+ +
- $items) : ?> + + $items) : ?> - -

-
+ +

+
- + - - - $value) : ?> - - - - - - -
- -
+ + + $value) : ?> + + + + + + +
- -

No data to display.

- - + +

No data to display.

+ + +

Session User Data

- - + + - $value) : ?> + $value) : ?> - - + + - +
- -

No data to display.

- +

Session doesn't seem to be active.

- + -

Request ( isSecure() ? 'HTTPS' : 'HTTP').'/'.$request->getProtocolVersion() ?> )

+

Request ( )

- getGet()) : ?> +

$_GET

@@ -205,15 +185,15 @@ $value) : ?> - - + + - + - getPost()) : ?> +

$_POST

@@ -222,56 +202,51 @@ $value) : ?> - - + + - + - getHeaders()) : ?> +

Headers

- $value) : ?> - - - - - + + - - +
getName()) ?>getValueLine()) ?>
- getCookie()) : ?> +

Cookies

- $value) : ?> + $value) : ?> - - + + - + -

Response ( getStatusCode().' - '. esc($response->getReason()) ?> )

+

Response ( )

- getHeaders()) : ?> +

Headers

@@ -280,18 +255,20 @@ $value) : ?> - - getHeaderLine($header)) ?> + + - + - +
- -
-

System Configuration

+ +
+

System Configuration

- -
+ setData($config)->render('_config.tpl') ?> +
+ + diff --git a/system/Debug/Toolbar/toolbarloader.js.php b/system/Debug/Toolbar/toolbarloader.js.php index d77e96e82a..7aafb90ad6 100644 --- a/system/Debug/Toolbar/toolbarloader.js.php +++ b/system/Debug/Toolbar/toolbarloader.js.php @@ -1,24 +1,60 @@ + +document.addEventListener('DOMContentLoaded', loadDoc, false); -document.addEventListener('DOMContentLoaded', loadDoc, false ); +function loadDoc(time) { + if (isNaN(time)) { + time = document.getElementById("debugbar_loader").getAttribute("data-time"); + localStorage.setItem('debugbar-time', time); + } -function loadDoc() { - var time = document.getElementById("debugbar_loader").getAttribute("data-time"); - var url = ""; + localStorage.setItem('debugbar-time-new', time); - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function () { - if (this.readyState == 4 && this.status == 200) { - var toolbar = document.createElement( 'div' ); - toolbar.setAttribute( 'id', 'toolbarContainer' ); - toolbar.innerHTML = this.responseText; - document.body.appendChild( toolbar ); - eval(document.getElementById("toolbar_js").innerHTML); - if (typeof ciDebugBar === 'object') { - ciDebugBar.init(); - } - } - }; + var url = ""; - xhttp.open("GET", url + "?debugbar_time=" + time, true); - xhttp.send(); + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState === 4 && this.status === 200) { + var toolbar = document.getElementById("toolbarContainer"); + if (!toolbar) { + toolbar = document.createElement('div'); + toolbar.setAttribute('id', 'toolbarContainer'); + toolbar.innerHTML = this.responseText; + document.body.appendChild(toolbar); + } else { + toolbar.innerHTML = this.responseText; + } + eval(document.getElementById("toolbar_js").innerHTML); + if (typeof ciDebugBar === 'object') { + ciDebugBar.init(); + } + } else if (this.readyState === 4 && this.status === 404) { + console.log('CodeIgniter DebugBar: File "WRITEPATH/debugbar/debugbar_' + time + '" not found.'); + } + }; + + xhttp.open("GET", url + "?debugbar_time=" + time, true); + xhttp.send(); } + +// Track all AJAX requests +var oldXHR = window.XMLHttpRequest; + +function newXHR() { + var realXHR = new oldXHR(); + realXHR.addEventListener("readystatechange", function() { + // Only success responses and URLs that do not contains "debugbar_time" are tracked + if (realXHR.readyState === 4 && realXHR.status.toString()[0] === '2' && realXHR.responseURL.indexOf('debugbar_time') === -1) { + var debugbarTime = realXHR.getResponseHeader('Debugbar-Time'); + if (debugbarTime) { + var h2 = document.querySelector('#ci-history > h2'); + h2.innerHTML = 'History You have new debug data. '; + var badge = document.querySelector('a[data-tab="ci-history"] > span > .badge'); + badge.className += ' active'; + } + } + }, false); + return realXHR; +} + +window.XMLHttpRequest = newXHR; + diff --git a/system/Email/Email.php b/system/Email/Email.php index 04f04462f1..4a8750ee84 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2017, British Columbia Institute of Technology + * Copyright (c) 2014 - 2018, British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,16 +27,17 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author EllisLab Dev Team - * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/) - * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link https://codeigniter.com - * @since Version 1.0.0 + * @package CodeIgniter + * @author EllisLab Dev Team + * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/) + * @copyright Copyright (c) 2014 - 2018, British Columbia Institute of Technology (http://bcit.ca/) + * @license http://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com + * @since Version 1.0.0 * @filesource */ +use CodeIgniter\Config\BaseConfig; use Config\Mimes; @@ -45,132 +46,141 @@ use Config\Mimes; * * Permits email to be sent using Mail, Sendmail, or SMTP. * - * @package CodeIgniter - * @subpackage Libraries - * @category Libraries - * @author EllisLab Dev Team - * @link https://codeigniter.com/user_guide/libraries/email.html + * @package CodeIgniter + * @subpackage Libraries + * @category Libraries + * @author EllisLab Dev Team + * @link https://codeigniter.com/user_guide/libraries/email.html */ class Email { + /** + * @var string + */ + public $fromEmail; + + /** + * @var string + */ + public $fromName; /** * Used as the User-Agent and X-Mailer headers' value. * - * @var string + * @var string */ public $userAgent = 'CodeIgniter'; /** * Path to the Sendmail binary. * - * @var string + * @var string */ public $mailPath = '/usr/sbin/sendmail'; // Sendmail path /** * Which method to use for sending e-mails. * - * @var string 'mail', 'sendmail' or 'smtp' + * @var string 'mail', 'sendmail' or 'smtp' */ public $protocol = 'mail'; // mail/sendmail/smtp /** * STMP Server host * - * @var string + * @var string */ public $SMTPHost = ''; /** * SMTP Username * - * @var string + * @var string */ public $SMTPUser = ''; /** * SMTP Password * - * @var string + * @var string */ public $SMTPPass = ''; /** * SMTP Server port * - * @var int + * @var int */ public $SMTPPort = 25; /** * SMTP connection timeout in seconds * - * @var int + * @var int */ public $SMTPTimeout = 5; /** * SMTP persistent connection * - * @var bool + * @var bool */ public $SMTPKeepAlive = false; /** * SMTP Encryption * - * @var string empty, 'tls' or 'ssl' + * @var string Empty, 'tls' or 'ssl' */ public $SMTPCrypto = ''; /** * Whether to apply word-wrapping to the message body. * - * @var bool + * @var bool */ public $wordWrap = true; /** * Number of characters to wrap at. * - * @see CI_Email::$wordwrap - * @var int + * @see Email::$wordWrap + * @var int */ public $wrapChars = 76; /** * Message format. * - * @var string 'text' or 'html' + * @var string 'text' or 'html' */ public $mailType = 'text'; /** * Character set (default: utf-8) * - * @var string + * @var string */ public $charset = 'utf-8'; /** * Alternative message (for HTML messages only) * - * @var string + * @var string */ public $altMessage = ''; /** * Whether to validate e-mail addresses. * - * @var bool + * @var bool */ public $validate = true; /** * X-Priority header value. * - * @var int 1-5 + * @var int 1-5 */ public $priority = 3; // Default priority (1 - 5) @@ -178,8 +188,8 @@ class Email * Newline character sequence. * Use "\r\n" to comply with RFC 822. * - * @link http://www.ietf.org/rfc/rfc822.txt - * @var string "\r\n" or "\n" + * @link http://www.ietf.org/rfc/rfc822.txt + * @var string "\r\n" or "\n" */ public $newline = "\n"; // Default newline. "\r\n" or "\n" (Use "\r\n" to comply with RFC 822) @@ -192,15 +202,15 @@ class Email * switching to "\n", while improper, is the only solution * that seems to work for all environments. * - * @link http://www.ietf.org/rfc/rfc822.txt - * @var string + * @link http://www.ietf.org/rfc/rfc822.txt + * @var string */ public $CRLF = "\n"; /** * Whether to use Delivery Status Notification. * - * @var bool + * @var bool */ public $DSN = false; @@ -208,22 +218,22 @@ class Email * Whether to send multipart alternatives. * Yahoo! doesn't seem to like these. * - * @var bool + * @var bool */ public $sendMultipart = true; /** * Whether to send messages to BCC recipients in batches. * - * @var bool + * @var bool */ public $BCCBatchMode = false; /** * BCC Batch max number size. * - * @see CI_Email::$bcc_batch_mode - * @var int + * @see Email::$BCCBatchMode + * @var int */ public $BCCBatchSize = 200; @@ -232,107 +242,107 @@ class Email /** * Subject header * - * @var string + * @var string */ protected $subject = ''; /** * Message body * - * @var string + * @var string */ protected $body = ''; /** * Final message body to be sent. * - * @var string + * @var string */ protected $finalBody = ''; /** * Final headers to send * - * @var string + * @var string */ protected $headerStr = ''; /** * SMTP Connection socket placeholder * - * @var resource + * @var resource */ protected $SMTPConnect = ''; /** * Mail encoding * - * @var string '8bit' or '7bit' + * @var string '8bit' or '7bit' */ protected $encoding = '8bit'; /** * Whether to perform SMTP authentication * - * @var bool + * @var bool */ protected $SMTPAuth = false; /** * Whether to send a Reply-To header * - * @var bool + * @var bool */ protected $replyToFlag = false; /** * Debug messages * - * @see CI_Email::print_debugger() - * @var string + * @see Email::printDebugger() + * @var array */ protected $debugMessage = []; /** * Recipients * - * @var string[] + * @var array */ protected $recipients = []; /** * CC Recipients * - * @var string[] + * @var array */ protected $CCArray = []; /** * BCC Recipients * - * @var string[] + * @var array */ protected $BCCArray = []; /** * Message headers * - * @var string[] + * @var array */ protected $headers = []; /** * Attachment data * - * @var array + * @var array */ protected $attachments = []; /** * Valid $protocol values * - * @see CI_Email::$protocol - * @var string[] + * @see Email::$protocol + * @var array */ protected $protocols = ['mail', 'sendmail', 'smtp']; @@ -342,7 +352,7 @@ class Email * Character sets valid for 7-bit encoding, * excluding language suffix. * - * @var string[] + * @var array */ protected $baseCharsets = ['us-ascii', 'iso-2022-']; @@ -351,8 +361,8 @@ class Email * * Valid mail encodings * - * @see CI_Email::$_encoding - * @var string[] + * @see Email::$encoding + * @var array */ protected $bitDepths = ['7bit', '8bit']; @@ -361,7 +371,7 @@ class Email * * Actual values to send with the X-Priority header * - * @var string[] + * @var array */ protected $priorities = [ 1 => '1 (Highest)', @@ -374,7 +384,7 @@ class Email /** * mbstring.func_overload flag * - * @var bool + * @var bool */ protected static $func_overload; @@ -385,15 +395,13 @@ class Email * * The constructor can be passed an array of config values * - * @param array $config = array() - * - * @return void + * @param array|null $config */ public function __construct($config = null) { $this->initialize($config); - isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); + isset(self::$func_overload) || self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); log_message('info', 'Email Class Initialized'); } @@ -403,27 +411,32 @@ class Email /** * Initialize preferences * - * @param array $config + * @param array|\Config\Email $config * - * @return $this + * @return Email */ public function initialize($config) { $this->clear(); + if ($config instanceof \Config\Email) + { + $config = get_object_vars($config); + } + foreach (get_class_vars(get_class($this)) as $key => $value) { - if (isset($this->$key) && isset($config->$key)) + if (property_exists($this, $key) && isset($config[$key])) { $method = 'set'.ucfirst($key); if (method_exists($this, $method)) { - $this->$method($config->$key); + $this->$method($config[$key]); } else { - $this->$key = $config->$key; + $this->$key = $config[$key]; } } } @@ -439,9 +452,9 @@ class Email /** * Initialize the Email Data * - * @param bool + * @param bool $clearAttachments * - * @return $this + * @return Email */ public function clear($clearAttachments = false) { @@ -471,11 +484,11 @@ class Email /** * Set FROM * - * @param string $from - * @param string $name - * @param string $returnPath = NULL Return-Path + * @param string $from + * @param string $name + * @param string|null $returnPath Return-Path * - * @return $this + * @return Email */ public function setFrom($from, $name = '', $returnPath = null) { @@ -510,7 +523,7 @@ class Email $this->setHeader('From', $name.' <'.$from.'>'); - isset($returnPath) OR $returnPath = $from; + isset($returnPath) || $returnPath = $from; $this->setHeader('Return-Path', '<'.$returnPath.'>'); return $this; @@ -521,10 +534,10 @@ class Email /** * Set Reply-to * - * @param string - * @param string + * @param string $replyto + * @param string $name * - * @return $this + * @return Email */ public function setReplyTo($replyto, $name = '') { @@ -563,9 +576,9 @@ class Email /** * Set Recipients * - * @param string + * @param string $to * - * @return $this + * @return Email */ public function setTo($to) { @@ -592,9 +605,9 @@ class Email /** * Set CC * - * @param string + * @param string $cc * - * @return $this + * @return Email */ public function setCC($cc) { @@ -620,10 +633,10 @@ class Email /** * Set BCC * - * @param string - * @param string + * @param string $bcc + * @param string $limit * - * @return $this + * @return Email */ public function setBCC($bcc, $limit = '') { @@ -640,7 +653,7 @@ class Email $this->validateEmail($bcc); } - if ($this->getProtocol() === 'smtp' OR ($this->BCCBatchMode && count($bcc) > $this->BCCBatchSize)) + if ($this->getProtocol() === 'smtp' || ($this->BCCBatchMode && count($bcc) > $this->BCCBatchSize)) { $this->BCCArray = $bcc; } @@ -657,9 +670,9 @@ class Email /** * Set Email Subject * - * @param string + * @param string $subject * - * @return $this + * @return Email */ public function setSubject($subject) { @@ -674,9 +687,9 @@ class Email /** * Set Body * - * @param string + * @param string $body * - * @return $this + * @return Email */ public function setMessage($body) { @@ -690,12 +703,12 @@ class Email /** * Assign file attachments * - * @param string $file Can be local path, URL or buffered content - * @param string $disposition = 'attachment' - * @param string $newname = NULL - * @param string $mime = '' + * @param string $file Can be local path, URL or buffered content + * @param string $disposition 'attachment' + * @param string|null $newname + * @param string $mime * - * @return $this + * @return Email */ public function attach($file, $disposition = '', $newname = null, $mime = '') { @@ -743,9 +756,9 @@ class Email * * Useful for attached inline pictures * - * @param string $filename + * @param string $filename * - * @return string + * @return string */ public function setAttachmentCID($filename) { @@ -768,10 +781,10 @@ class Email /** * Add a Header Item * - * @param string - * @param string + * @param string $header + * @param string $value * - * @return $this + * @return Email */ public function setHeader($header, $value) { @@ -785,9 +798,9 @@ class Email /** * Convert a String to an Array * - * @param string + * @param string $email * - * @return array + * @return array */ protected function stringToArray($email) { @@ -806,9 +819,9 @@ class Email /** * Set Multipart Value * - * @param string + * @param string $str * - * @return $this + * @return Email */ public function setAltMessage($str) { @@ -822,9 +835,9 @@ class Email /** * Set Mailtype * - * @param string + * @param string $type * - * @return $this + * @return Email */ public function setMailType($type = 'text') { @@ -838,9 +851,9 @@ class Email /** * Set Wordwrap * - * @param bool + * @param bool $wordWrap * - * @return $this + * @return Email */ public function setWordWrap($wordWrap = true) { @@ -854,9 +867,9 @@ class Email /** * Set Protocol * - * @param string + * @param string $protocol * - * @return $this + * @return Email */ public function setProtocol($protocol = 'mail') { @@ -870,9 +883,9 @@ class Email /** * Set Priority * - * @param int + * @param int $n * - * @return $this + * @return Email */ public function setPriority($n = 3) { @@ -886,9 +899,9 @@ class Email /** * Set Newline Character * - * @param string + * @param string $newline * - * @return $this + * @return Email */ public function setNewline($newline = "\n") { @@ -902,9 +915,9 @@ class Email /** * Set CRLF * - * @param string + * @param string $CRLF * - * @return $this + * @return Email */ public function setCRLF($CRLF = "\n") { @@ -918,7 +931,7 @@ class Email /** * Get the Message ID * - * @return string + * @return string */ protected function getMessageID() { @@ -932,12 +945,12 @@ class Email /** * Get Mail Protocol * - * @return mixed + * @return string */ protected function getProtocol() { $this->protocol = strtolower($this->protocol); - in_array($this->protocol, $this->protocols, true) OR $this->protocol = 'mail'; + in_array($this->protocol, $this->protocols, true) || $this->protocol = 'mail'; return $this->protocol; } @@ -947,11 +960,11 @@ class Email /** * Get Mail Encoding * - * @return string + * @return string */ protected function getEncoding() { - in_array($this->encoding, $this->bitDepths) OR $this->encoding = '8bit'; + in_array($this->encoding, $this->bitDepths) || $this->encoding = '8bit'; foreach ($this->baseCharsets as $charset) { @@ -969,7 +982,7 @@ class Email /** * Get content type (text/html/attachment) * - * @return string + * @return string */ protected function getContentType() { @@ -992,7 +1005,7 @@ class Email /** * Set RFC 822 Date * - * @return string + * @return string */ protected function setDate() { @@ -1009,7 +1022,7 @@ class Email /** * Mime message * - * @return string + * @return string */ protected function getMimeMessage() { @@ -1021,9 +1034,9 @@ class Email /** * Validate Email Address * - * @param string + * @param string $email * - * @return bool + * @return bool */ public function validateEmail($email) { @@ -1052,13 +1065,13 @@ class Email /** * Email Validation * - * @param string + * @param string $email * - * @return bool + * @return bool */ public function isValidEmail($email) { - if (function_exists('idn_to_ascii') && $atpos = strpos($email, '@')) + if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && $atpos = strpos($email, '@')) { $email = self::substr($email, 0, ++$atpos).idn_to_ascii(self::substr($email, $atpos), 0, INTL_IDNA_VARIANT_UTS46); @@ -1072,9 +1085,9 @@ class Email /** * Clean Extended Email Address: Joe Smith * - * @param string + * @param string $email * - * @return string + * @return string */ public function cleanEmail($email) { @@ -1103,7 +1116,7 @@ class Email * If the user hasn't specified his own alternative message * it creates one by stripping the HTML * - * @return string + * @return string */ protected function getAltMessage() { @@ -1135,10 +1148,10 @@ class Email /** * Word Wrap * - * @param string - * @param int line-length limit + * @param string $str + * @param int|null $charlim Line-length limit * - * @return string + * @return string */ public function wordWrap($str, $charlim = null) { @@ -1226,8 +1239,6 @@ class Email /** * Build final headers - * - * @return void */ protected function buildHeaders() { @@ -1243,8 +1254,6 @@ class Email /** * Write Headers as a string - * - * @return void */ protected function writeHeaders() { @@ -1280,8 +1289,6 @@ class Email /** * Build Final Body and attachments - * - * @return void */ protected function buildMessage() { @@ -1470,11 +1477,11 @@ class Email /** * Prepares attachment string * - * @param string $body Message body to append to - * @param string $boundary Multipart boundary - * @param string $multipart When provided, only attachments of this type will be processed + * @param string &$body Message body to append to + * @param string $boundary Multipart boundary + * @param string|null $multipart When provided, only attachments of this type will be processed * - * @return string + * @return string */ protected function appendAttachments(&$body, $boundary, $multipart = null) { @@ -1501,7 +1508,7 @@ class Email // $name won't be set if no attachments were appended, // and therefore a boundary wouldn't be necessary - empty($name) OR $body .= '--'.$boundary.'--'; + empty($name) || $body .= '--'.$boundary.'--'; } //-------------------------------------------------------------------- @@ -1512,9 +1519,9 @@ class Email * Prepares string for Quoted-Printable Content-Transfer-Encoding * Refer to RFC 2045 http://www.ietf.org/rfc/rfc2045.txt * - * @param string + * @param string $str * - * @return string + * @return string */ protected function prepQuotedPrintable($str) { @@ -1641,7 +1648,7 @@ class Email $ascii = ord($char); // Convert spaces and tabs but only if it's the end of the line - if ($ascii === 32 OR $ascii === 9) + if ($ascii === 32 || $ascii === 9) { if ($i === ($length-1)) { @@ -1690,9 +1697,9 @@ class Email * It's related but not identical to quoted-printable, so it has its * own method. * - * @param string + * @param string $str * - * @return string + * @return string */ protected function prepQEncoding($str) { @@ -1732,7 +1739,7 @@ class Email } // We might already have this set for UTF-8 - isset($chars) OR $chars = self::strlen($str); + isset($chars) || $chars = self::strlen($str); $output = '=?'.$this->charset.'?Q?'; for ($i = 0, $length = self::strlen($output); $i < $chars; $i++) @@ -1745,7 +1752,7 @@ class Email // We'll append ?= to the end of each line though. if ($length+($l = self::strlen($chr)) > 74) { - $output .= '?='.$this->crlf // EOL + $output .= '?='.$this->CRLF // EOL .' =?'.$this->charset.'?Q?'.$chr; // New line $length = 6+self::strlen($this->charset)+$l; // Reset the length for the new line } else @@ -1764,12 +1771,17 @@ class Email /** * Send Email * - * @param bool $autoClear = TRUE + * @param bool $autoClear * - * @return bool + * @return bool */ public function send($autoClear = true) { + if (! isset($this->headers['From']) && ! empty($this->fromEmail)) + { + $this->setFrom($this->fromEmail, $this->fromName); + } + if (! isset($this->headers['From'])) { $this->setErrorMessage(lang('email.noFrom')); @@ -1821,8 +1833,6 @@ class Email /** * Batch Bcc Send. Sends groups of BCCs in batches - * - * @return void */ public function batchBCCSend() { @@ -1874,8 +1884,6 @@ class Email /** * Unwrap special elements - * - * @return void */ protected function unwrapSpecials() { @@ -1888,13 +1896,13 @@ class Email /** * Strip line-breaks via callback * - * @param string $matches + * @param string $matches * - * @return string + * @return string */ protected function removeNLCallback($matches) { - if (strpos($matches[1], "\r") !== false OR strpos($matches[1], "\n") !== false) + if (strpos($matches[1], "\r") !== false || strpos($matches[1], "\n") !== false) { $matches[1] = str_replace(["\r\n", "\r", "\n"], '', $matches[1]); } @@ -1907,7 +1915,7 @@ class Email /** * Spool mail to the mail server * - * @return bool + * @return bool */ protected function spoolEmail() { @@ -1941,9 +1949,9 @@ class Email * * Credits for the base concept go to Paul Buonopane * - * @param string $email + * @param string &$email * - * @return bool + * @return bool */ protected function validateEmailForShell(&$email) { @@ -1962,7 +1970,7 @@ class Email /** * Send using mail() * - * @return bool + * @return bool */ protected function sendWithMail() { @@ -1990,7 +1998,7 @@ class Email /** * Send using Sendmail * - * @return bool + * @return bool */ protected function sendWithSendmail() { @@ -2007,7 +2015,7 @@ class Email } // is popen() enabled? - if (! function_usable('popen') OR false === ($fp = @popen($this->mailPath.' -oi '.$from.' -t', 'w'))) + if (! function_usable('popen') || false === ($fp = @popen($this->mailPath.' -oi '.$from.' -t', 'w'))) { // server probably has popen disabled, so nothing we can do to get a verbose error. return false; @@ -2034,7 +2042,7 @@ class Email /** * Send using SMTP * - * @return bool + * @return bool */ protected function sendWithSmtp() { @@ -2045,7 +2053,7 @@ class Email return false; } - if (! $this->SMTPConnect() OR ! $this->SMTPAuthenticate()) + if (! $this->SMTPConnect() || ! $this->SMTPAuthenticate()) { return false; } @@ -2119,8 +2127,6 @@ class Email * SMTP End * * Shortcut to send RSET or QUIT depending on keep-alive - * - * @return void */ protected function SMTPEnd() { @@ -2132,7 +2138,7 @@ class Email /** * SMTP Connect * - * @return string + * @return string */ protected function SMTPConnect() { @@ -2184,17 +2190,17 @@ class Email /** * Send SMTP command * - * @param string - * @param string + * @param string $cmd + * @param string $data * - * @return bool + * @return bool */ protected function sendCommand($cmd, $data = '') { switch ($cmd) { case 'hello': - if ($this->SMTPAuth OR $this->getEncoding() === '8bit') + if ($this->SMTPAuth || $this->getEncoding() === '8bit') { $this->sendData('EHLO '.$this->getHostname()); } @@ -2262,7 +2268,7 @@ class Email /** * SMTP Authenticate * - * @return bool + * @return bool */ protected function SMTPAuthenticate() { @@ -2325,9 +2331,9 @@ class Email /** * Send SMTP data * - * @param string $data + * @param string $data * - * @return bool + * @return bool */ protected function sendData($data) { @@ -2375,7 +2381,7 @@ class Email /** * Get SMTP data * - * @return string + * @return string */ protected function getSMTPData() { @@ -2403,9 +2409,10 @@ class Email * qualified domain name (eg: "mail.example.com") or an IP literal * (eg: "[1.2.3.4]"). * - * @link https://tools.ietf.org/html/rfc5321#section-2.3.5 - * @link http://cbl.abuseat.org/namingproblems.html - * @return string + * @link https://tools.ietf.org/html/rfc5321#section-2.3.5 + * @link http://cbl.abuseat.org/namingproblems.html + * + * @return string */ protected function getHostname() { @@ -2422,10 +2429,10 @@ class Email /** * Get Debug Message * - * @param array $include List of raw data chunks to include in the output - * Valid options are: 'headers', 'subject', 'body' + * @param array $include List of raw data chunks to include in the output + * Valid options are: 'headers', 'subject', 'body' * - * @return string + * @return string */ public function printDebugger($include = ['headers', 'subject', 'body']) { @@ -2433,7 +2440,7 @@ class Email // Determine which parts of our raw data needs to be printed $raw_data = ''; - is_array($include) OR $include = [$include]; + is_array($include) || $include = [$include]; in_array('headers', $include, true) && $raw_data = htmlspecialchars($this->headerStr)."\n"; in_array('subject', $include, true) && $raw_data .= htmlspecialchars($this->subject)."\n"; @@ -2447,9 +2454,7 @@ class Email /** * Set Message * - * @param string $msg - * - * @return void + * @param string $msg */ protected function setErrorMessage($msg) { @@ -2461,9 +2466,9 @@ class Email /** * Mime Types * - * @param string + * @param string $ext * - * @return string + * @return string */ protected function mimeTypes($ext = '') { @@ -2478,8 +2483,6 @@ class Email /** * Destructor - * - * @return void */ public function __destruct() { @@ -2491,9 +2494,9 @@ class Email /** * Byte-safe strlen() * - * @param string $str + * @param string $str * - * @return int + * @return int */ protected static function strlen($str) { @@ -2507,11 +2510,11 @@ class Email /** * Byte-safe substr() * - * @param string $str - * @param int $start - * @param int $length + * @param string $str + * @param int $start + * @param int|null $length * - * @return string + * @return string */ protected static function substr($str, $start, $length = null) { diff --git a/system/Entity.php b/system/Entity.php index 0d429df847..7090211593 100644 --- a/system/Entity.php +++ b/system/Entity.php @@ -144,7 +144,7 @@ class Entity $result = $this->mutateDate($result); } // Or cast it as something? - else if (array_key_exists($key, $this->_options['casts'])) + else if (isset($this->_options['casts'][$key]) && ! empty($this->_options['casts'][$key])) { $result = $this->castAs($result, $this->_options['casts'][$key]); } @@ -267,7 +267,12 @@ class Entity */ protected function mapProperty(string $key) { - if (array_key_exists($key, $this->_options['datamap'])) + if (empty($this->_options['datamap'])) + { + return $key; + } + + if (isset($this->_options['datamap'][$key]) && ! empty($this->_options['datamap'][$key])) { return $this->_options['datamap'][$key]; } diff --git a/system/Events/Events.php b/system/Events/Events.php index 798c66a767..108690efa9 100644 --- a/system/Events/Events.php +++ b/system/Events/Events.php @@ -65,7 +65,7 @@ class Events * * @var string */ - protected static $eventsFile; + protected static $eventsFile = ''; /** * If true, events will not actually be fired. @@ -290,7 +290,7 @@ class Events * * @param string $path */ - public function setFile(string $path) + public static function setFile(string $path) { self::$eventsFile = $path; } diff --git a/system/Exceptions/AlertError.php b/system/Exceptions/AlertError.php new file mode 100644 index 0000000000..d229358bf9 --- /dev/null +++ b/system/Exceptions/AlertError.php @@ -0,0 +1,9 @@ +size / 1024, 3); - break; case 'mb': return number_format(($this->size / 1024) / 1024, 3); - break; } - return $this->size; + return (int) $this->size; } //-------------------------------------------------------------------- @@ -175,7 +173,7 @@ class File extends SplFileInfo if ( ! @rename($this->getPath(), $destination)) { $error = error_get_last(); - throw new \RuntimeException(sprintf('Could not move file %s to %s (%s)', $this->getBasename(), $targetPath, strip_tags($error['message']))); + throw FileException::forUnableToMove($this->getBasename(), $targetPath, strip_tags($error['message'])); } @chmod($targetPath, 0777 & ~umask()); diff --git a/system/Filters/Exceptions/FilterException.php b/system/Filters/Exceptions/FilterException.php new file mode 100644 index 0000000000..b2aba94bf6 --- /dev/null +++ b/system/Filters/Exceptions/FilterException.php @@ -0,0 +1,17 @@ +config->aliases)) { - throw new \InvalidArgumentException("'{$alias}' filter must have a matching alias defined."); + throw FilterException::forNoAlias($alias); } $class = new $this->config->aliases[$alias](); if ( ! $class instanceof FilterInterface) { - throw new \RuntimeException(get_class($class) . ' must implement CodeIgniter\Filters\FilterInterface.'); + throw FilterException::forIncorrectInterface(get_class($class)); } if ($position == 'before') @@ -134,10 +136,11 @@ class Filters // then send it and quit. if ($result instanceof ResponseInterface) { - $result->send(); - exit(EXIT_ERROR); + // short circuit - bypass any other filters + return $result; } + // Ignore an empty result if (empty($result)) { continue; diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php new file mode 100644 index 0000000000..442be8f3e2 --- /dev/null +++ b/system/Format/Exceptions/FormatException.php @@ -0,0 +1,17 @@ +"); diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 143ecaf532..8569d79d47 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -35,6 +35,7 @@ * @since Version 3.0.0 * @filesource */ +use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\App; /** @@ -113,7 +114,7 @@ class CURLRequest extends Request { if ( ! function_exists('curl_version')) { - throw new \RuntimeException('CURL must be enabled to use the CURLRequest class.'); + throw HTTPException::forMissingCurl(); } parent::__construct($config); @@ -570,9 +571,9 @@ class CURLRequest extends Request $cert = $cert[0]; } - if ( ! file_exists($cert)) + if (! file_exists($cert)) { - throw new \InvalidArgumentException('SSL certificate not found at: ' . $cert); + throw HTTPException::forSSLCertNotFound($cert); } $curl_options[CURLOPT_SSLCERT] = $cert; @@ -585,11 +586,11 @@ class CURLRequest extends Request { $file = realpath($config['ssl_key']); - if ( ! $file) + if (! $file) { - throw new \InvalidArgumentException('Cannot set SSL Key. ' . $config['ssl_key'] . - ' is not a valid file.'); + throw HTTPException::forInvalidSSLKey($config['ssl_key']); } + $curl_options[CURLOPT_CAINFO] = $file; $curl_options[CURLOPT_SSL_VERIFYPEER] = 1; } @@ -737,7 +738,7 @@ class CURLRequest extends Request if ($output === false) { - throw new \RuntimeException(curl_errno($ch) . ': ' . curl_error($ch)); + throw HTTPException::forCurlError(curl_errno($ch), curl_error($ch)); } curl_close($ch); diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index 61f74d9721..33cc6325e8 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -140,6 +140,12 @@ class ContentSecurityPolicy * @var array */ protected $styleSrc = []; + + /** + * Used for security enforcement + * @var array + */ + protected $manifestSrc = []; /** * Used for security enforcement @@ -432,6 +438,26 @@ class ContentSecurityPolicy return $this; } + + //-------------------------------------------------------------------- + + /** + * Adds a new valid endpoint for manifest sources. Can be either + * a URI class or simple string. + * + * @see https://www.w3.org/TR/CSP/#directive-manifest-src + * + * @param $uri + * @param bool $reportOnly + * + * @return $this + */ + public function addManifestSrc($uri, bool $reportOnly = false) + { + $this->addOption($uri, 'manifestSrc', $reportOnly); + + return $this; + } //-------------------------------------------------------------------- @@ -688,6 +714,7 @@ class ContentSecurityPolicy 'plugin-types' => 'pluginTypes', 'script-src' => 'scriptSrc', 'style-src' => 'styleSrc', + 'manifest-src' => 'manifestSrc', 'sandbox' => 'sandbox', 'report-uri' => 'reportURI' ]; diff --git a/system/HTTP/Exceptions/HTTPException.php b/system/HTTP/Exceptions/HTTPException.php new file mode 100644 index 0000000000..a8f2ae4642 --- /dev/null +++ b/system/HTTP/Exceptions/HTTPException.php @@ -0,0 +1,180 @@ +body = $body; $this->config = $config; + $this->userAgent = $userAgent; parent::__construct($config); @@ -314,7 +322,7 @@ class IncomingRequest extends Request */ public function getVar($index = null, $filter = null, $flags = null) { - return $this->fetchGlobal(INPUT_REQUEST, $index, $filter, $flags); + return $this->fetchGlobal('request', $index, $filter, $flags); } //-------------------------------------------------------------------- @@ -367,7 +375,7 @@ class IncomingRequest extends Request */ public function getGet($index = null, $filter = null, $flags = null) { - return $this->fetchGlobal(INPUT_GET, $index, $filter, $flags); + return $this->fetchGlobal('get', $index, $filter, $flags); } //-------------------------------------------------------------------- @@ -383,7 +391,7 @@ class IncomingRequest extends Request */ public function getPost($index = null, $filter = null, $flags = null) { - return $this->fetchGlobal(INPUT_POST, $index, $filter, $flags); + return $this->fetchGlobal('post', $index, $filter, $flags); } //-------------------------------------------------------------------- @@ -437,7 +445,7 @@ class IncomingRequest extends Request */ public function getCookie($index = null, $filter = null, $flags = null) { - return $this->fetchGlobal(INPUT_COOKIE, $index, $filter, $flags); + return $this->fetchGlobal('cookie', $index, $filter, $flags); } //-------------------------------------------------------------------- @@ -449,9 +457,9 @@ class IncomingRequest extends Request * * @return mixed */ - public function getUserAgent($filter = null) + public function getUserAgent() { - return $this->fetchGlobal(INPUT_SERVER, 'HTTP_USER_AGENT', $filter); + return $this->userAgent; } //-------------------------------------------------------------------- @@ -459,7 +467,7 @@ class IncomingRequest extends Request /** * Attempts to get old Input data that has been flashed to the session * with redirect_with_input(). It first checks for the data in the old - * POST data, then the old GET data. + * POST data, then the old GET data and finally check for dot arrays * * @param string $key * @@ -470,7 +478,9 @@ class IncomingRequest extends Request // If the session hasn't been started, or no // data was previously saved, we're done. if (empty($_SESSION['_ci_old_input'])) + { return; + } // Check for the value in the POST array first. if (isset($_SESSION['_ci_old_input']['post'][$key])) @@ -483,6 +493,28 @@ class IncomingRequest extends Request { return $_SESSION['_ci_old_input']['get'][$key]; } + + helper('array'); + + // Check for an array value in POST. + if (isset($_SESSION['_ci_old_input']['post'])) + { + $value = dot_array_search($key, $_SESSION['_ci_old_input']['post']); + if ( ! is_null($value)) + { + return $value; + } + } + + // Check for an array value in GET. + if (isset($_SESSION['_ci_old_input']['get'])) + { + $value = dot_array_search($key, $_SESSION['_ci_old_input']['get']); + if ( ! is_null($value)) + { + return $value; + } + } } /** @@ -556,17 +588,6 @@ class IncomingRequest extends Request else { throw FrameworkException::forEmptyBaseURL(); - -// $this->isSecure() ? $this->uri->setScheme('https') : $this->uri->setScheme('http'); -// -// // While both SERVER_NAME and HTTP_HOST are open to security issues, -// // if we have to choose, we will go with the server-controlled version first. -// ! empty($_SERVER['SERVER_NAME']) ? (isset($_SERVER['SERVER_NAME']) ? $this->uri->setHost($_SERVER['SERVER_NAME']) : null) : (isset($_SERVER['HTTP_HOST']) ? $this->uri->setHost($_SERVER['HTTP_HOST']) : null); -// -// if ( ! empty($_SERVER['SERVER_PORT'])) -// { -// $this->uri->setPort($_SERVER['SERVER_PORT']); -// } } } @@ -597,7 +618,7 @@ class IncomingRequest extends Request break; case 'PATH_INFO': default: - $path = $_SERVER[$protocol] ?? $this->parseRequestURI(); + $path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI(); break; } @@ -639,7 +660,7 @@ class IncomingRequest extends Request break; } - throw new \InvalidArgumentException($type . ' is not a valid negotiation type.'); + throw HTTPException::forInvalidNegotiationType($type); } //-------------------------------------------------------------------- diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 38b4a9568f..1d9634e1dd 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -1,5 +1,7 @@ protocolVersion; + return $this->protocolVersion ?? '1.1'; } //-------------------------------------------------------------------- @@ -381,7 +383,7 @@ class Message if ( ! in_array($version, $this->validProtocolVersions)) { - throw new \InvalidArgumentException('Invalid HTTP Protocol Version. Must be one of: ' . implode(', ', $this->validProtocolVersions)); + throw HTTPException::forInvalidHTTPProtocol(implode(', ', $this->validProtocolVersions)); } $this->protocolVersion = $version; diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index 390c0d5359..0f23152423 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -36,6 +36,8 @@ * @filesource */ +use CodeIgniter\HTTP\Exceptions\HTTPException; + /** * Class Negotiate * @@ -199,7 +201,7 @@ class Negotiate { if (empty($supported)) { - throw new \InvalidArgumentException('You must provide an array of supported values to all Negotiations.'); + throw HTTPException::forEmptySupportedNegotiations(); } if (empty($header)) diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index 1f23fb0f08..86c03b10ab 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -35,6 +35,7 @@ * @since Version 3.0.0 * @filesource */ +use CodeIgniter\HTTP\Exceptions\HTTPException; use Config\Services; class RedirectResponse extends Response @@ -81,7 +82,7 @@ class RedirectResponse extends Response if (! $route) { - throw new \InvalidArgumentException(lang('HTTP.invalidRoute', [$route])); + throw HTTPException::forInvalidRedirectRoute($route); } return $this->redirect($route, $method, $code); @@ -139,8 +140,8 @@ class RedirectResponse extends Response /** * Adds a key and message to the session as Flashdata. * - * @param string $key - * @param string $message + * @param string $key + * @param string|array $message * * @return $this */ diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 2adc533edd..139a457efe 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -64,6 +64,13 @@ class Request extends Message implements RequestInterface */ protected $method; + /** + * Stores values we've retrieved from + * PHP globals. + * @var array + */ + protected $globals = []; + //-------------------------------------------------------------------- /** @@ -141,7 +148,7 @@ class Request extends Message implements RequestInterface } // We have a subnet ... now the heavy lifting begins - isset($separator) OR $separator = $this->isValidIP($this->ipAddress, 'ipv6') ? ':' : '.'; + isset($separator) || $separator = $this->isValidIP($this->ipAddress, 'ipv6') ? ':' : '.'; // If the proxy entry doesn't match the IP protocol - skip it if (strpos($proxy_ips[$i], $separator) === FALSE) @@ -281,7 +288,7 @@ class Request extends Message implements RequestInterface */ public function getServer($index = null, $filter = null, $flags = null) { - return $this->fetchGlobal(INPUT_SERVER, $index, $filter, $flags); + return $this->fetchGlobal('server', $index, $filter, $flags); } //-------------------------------------------------------------------- @@ -297,7 +304,24 @@ class Request extends Message implements RequestInterface */ public function getEnv($index = null, $filter = null, $flags = null) { - return $this->fetchGlobal(INPUT_ENV, $index, $filter, $flags); + return $this->fetchGlobal('env', $index, $filter, $flags); + } + + //-------------------------------------------------------------------- + + /** + * Allows manually setting the value of PHP global, like $_GET, $_POST, etc. + * + * @param string $method + * @param $value + * + * @return $this + */ + public function setGlobal(string $method, $value) + { + $this->globals[$method] = $value; + + return $this; } //-------------------------------------------------------------------- @@ -312,45 +336,37 @@ class Request extends Message implements RequestInterface * * http://php.net/manual/en/filter.filters.sanitize.php * - * @param int $type Input filter constant + * @param int $method Input filter constant * @param string|array $index * @param int $filter Filter constant * @param null $flags * * @return mixed */ - protected function fetchGlobal($type, $index = null, $filter = null, $flags = null ) + public function fetchGlobal($method, $index = null, $filter = null, $flags = null ) { + $method = strtolower($method); + + if (! isset($this->globals[$method])) + { + $this->populateGlobals($method); + } + // Null filters cause null values to return. if (is_null($filter)) { $filter = FILTER_DEFAULT; } - $loopThrough = []; - switch ($type) - { - case INPUT_GET : $loopThrough = $_GET; - break; - case INPUT_POST : $loopThrough = $_POST; - break; - case INPUT_COOKIE : $loopThrough = $_COOKIE; - break; - case INPUT_SERVER : $loopThrough = $_SERVER; - break; - case INPUT_ENV : $loopThrough = $_ENV; - break; - case INPUT_REQUEST : $loopThrough = $_REQUEST; - break; - } - - // If $index is null, it means that the whole input type array is requested + // Return all values when $index is null if (is_null($index)) { $values = []; - foreach ($loopThrough as $key => $value) + foreach ($this->globals[$method] as $key => $value) { - $values[$key] = is_array($value) ? $this->fetchGlobal($type, $key, $filter, $flags) : filter_var($value, $filter, $flags); + $values[$key] = is_array($value) + ? $this->fetchGlobal($method, $key, $filter, $flags) + : filter_var($value, $filter, $flags); } return $values; @@ -363,7 +379,7 @@ class Request extends Message implements RequestInterface foreach ($index as $key) { - $output[$key] = $this->fetchGlobal($type, $key, $filter, $flags); + $output[$key] = $this->fetchGlobal($method, $key, $filter, $flags); } return $output; @@ -372,7 +388,7 @@ class Request extends Message implements RequestInterface // Does the index contain array notation? if (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) { - $value = $loopThrough; + $value = $this->globals[$method]; for ($i = 0; $i < $count; $i++) { $key = trim($matches[0][$i], '[]'); @@ -393,14 +409,12 @@ class Request extends Message implements RequestInterface } } - // Due to issues with FastCGI and testing, - // we need to do these all manually instead - // of the simpler filter_input(); if (empty($value)) { - $value = $loopThrough[$index] ?? null; + $value = $this->globals[$method][$index] ?? null; } + // Cannot filter these types of data automatically... if (is_array($value) || is_object($value) || is_null($value)) { return $value; @@ -410,4 +424,39 @@ class Request extends Message implements RequestInterface } //-------------------------------------------------------------------- + + /** + * Saves a copy of the current state of one of several PHP globals + * so we can retrieve them later. + * + * @param string $method + */ + protected function populateGlobals(string $method) + { + if (! isset($this->globals[$method])) + { + $this->globals[$method] = []; + } + + // Don't populate ENV as it might contain + // sensitive data that we don't want to get logged. + switch($method) + { + case 'get': + $this->globals['get'] = $_GET; + break; + case 'post': + $this->globals['post'] = $_POST; + break; + case 'request': + $this->globals['request'] = $_REQUEST; + break; + case 'cookie': + $this->globals['cookie'] = $_COOKIE; + break; + case 'server': + $this->globals['server'] = $_SERVER; + break; + } + } } diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 0276d2528e..44791bbc45 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -35,7 +35,10 @@ * @since Version 3.0.0 * @filesource */ +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\Services; use Config\App; +use Config\Format; use Config\Mimes; /** @@ -70,75 +73,75 @@ class Response extends Message implements ResponseInterface */ protected static $statusCodes = [ // 1xx: Informational - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', // http://www.iana.org/go/rfc2518 - 103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', // http://www.iana.org/go/rfc2518 + 103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt // 2xx: Success - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', // 1.1 - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-Status', // http://www.iana.org/go/rfc4918 - 208 => 'Already Reported', // http://www.iana.org/go/rfc5842 - 226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', // 1.1 + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // http://www.iana.org/go/rfc4918 + 208 => 'Already Reported', // http://www.iana.org/go/rfc5842 + 226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt // 3xx: Redirection - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', // Formerly 'Moved Temporarily' - 303 => 'See Other', // 1.1 - 304 => 'Not Modified', - 305 => 'Use Proxy', // 1.1 - 306 => 'Switch Proxy', // No longer used - 307 => 'Temporary Redirect', // 1.1 - 308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // Formerly 'Moved Temporarily' + 303 => 'See Other', // 1.1 + 304 => 'Not Modified', + 305 => 'Use Proxy', // 1.1 + 306 => 'Switch Proxy', // No longer used + 307 => 'Temporary Redirect', // 1.1 + 308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt // 4xx: Client error - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - 418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt // 419 (Authentication Timeout) is a non-standard status code with unknown origin - 421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2 - 422 => 'Unprocessable Entity', // http://www.iana.org/go/rfc4918 - 423 => 'Locked', // http://www.iana.org/go/rfc4918 - 424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918 - 426 => 'Upgrade Required', - 428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt - 429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt - 431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt - 451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725 - 499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133 + 421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2 + 422 => 'Unprocessable Entity', // http://www.iana.org/go/rfc4918 + 423 => 'Locked', // http://www.iana.org/go/rfc4918 + 424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918 + 426 => 'Upgrade Required', + 428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt + 429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt + 431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt + 451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725 + 499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133 // 5xx: Server error - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt - 507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918 - 508 => 'Loop Detected', // http://www.iana.org/go/rfc5842 - 510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt - 511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt - 599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599 + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt + 507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918 + 508 => 'Loop Detected', // http://www.iana.org/go/rfc5842 + 510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt + 511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt + 599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599 ]; /** @@ -205,6 +208,28 @@ class Response extends Message implements ResponseInterface */ protected $cookieHTTPOnly = false; + /** + * Stores all cookies that were set in the response. + * + * @var array + */ + protected $cookies = []; + + /** + * If true, will not write output. Useful during testing. + * + * @var bool + */ + protected $pretend = false; + + /** + * Type of format the body is in. + * Valid: html, json, xml + * + * @var string + */ + protected $bodyFormat = 'html'; + //-------------------------------------------------------------------- /** @@ -221,14 +246,14 @@ class Response extends Message implements ResponseInterface // Are we enforcing a Content Security Policy? if ($config->CSPEnabled === true) { - $this->CSP = new ContentSecurityPolicy(new \Config\ContentSecurityPolicy()); + $this->CSP = new ContentSecurityPolicy(new \Config\ContentSecurityPolicy()); $this->CSPEnabled = true; } - $this->cookiePrefix = $config->cookiePrefix; - $this->cookieDomain = $config->cookieDomain; - $this->cookiePath = $config->cookiePath; - $this->cookieSecure = $config->cookieSecure; + $this->cookiePrefix = $config->cookiePrefix; + $this->cookieDomain = $config->cookieDomain; + $this->cookiePath = $config->cookiePath; + $this->cookieSecure = $config->cookieSecure; $this->cookieHTTPOnly = $config->cookieHTTPOnly; // Default to an HTML Content-Type. Devs can override if needed. @@ -237,6 +262,20 @@ class Response extends Message implements ResponseInterface //-------------------------------------------------------------------- + /** + * Turns "pretend" mode on or off to aid in testing. + * + * @param bool $pretend + * + * @return $this + */ + public function pretend(bool $pretend = true) + { + $this->pretend = $pretend; + + return $this; + } + /** * Gets the response status code. * @@ -249,7 +288,7 @@ class Response extends Message implements ResponseInterface { if (empty($this->statusCode)) { - throw new \BadMethodCallException('HTTP Response is missing a status code'); + throw HTTPException::forMissingResponseStatus(); } return $this->statusCode; @@ -279,18 +318,18 @@ class Response extends Message implements ResponseInterface // Valid range? if ($code < 100 || $code > 599) { - throw new \InvalidArgumentException($code . ' is not a valid HTTP return status code'); + throw HTTPException::forInvalidStatusCode($code); } // Unknown and no message? - if ( ! array_key_exists($code, static::$statusCodes) && empty($reason)) + if (! array_key_exists($code, static::$statusCodes) && empty($reason)) { - throw new \InvalidArgumentException('Unknown HTTP status code provided with no message'); + throw HTTPException::forUnkownStatusCode($code); } $this->statusCode = $code; - if ( ! empty($reason)) + if (! empty($reason)) { $this->reason = $reason; } @@ -338,7 +377,7 @@ class Response extends Message implements ResponseInterface { $date->setTimezone(new \DateTimeZone('UTC')); - $this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT'); + $this->setHeader('Date', $date->format('D, d M Y H:i:s').' GMT'); return $this; } @@ -359,7 +398,7 @@ class Response extends Message implements ResponseInterface // add charset attribute if not already there and provided as parm if ((strpos($mime, 'charset=') < 1) && ! empty($charset)) { - $mime .= '; charset=' . $charset; + $mime .= '; charset='.$charset; } $this->removeHeader('Content-Type'); // replace existing content type @@ -369,6 +408,114 @@ class Response extends Message implements ResponseInterface } //-------------------------------------------------------------------- + + /** + * Converts the $body into JSON and sets the Content Type header. + * + * @param $body + * + * @return $this + */ + public function setJSON($body) + { + $this->body = $this->formatBody($body, 'json'); + + return $this; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Returns the current body, converted to JSON is it isn't already. + * + * @return mixed|string + */ + public function getJSON() + { + $body = $this->body; + + if ($this->bodyFormat != 'json') + { + $config = new Format(); + $formatter = $config->getFormatter('application/json'); + + $body = $formatter->format($body); + } + + return $body ?: null; + } + + //-------------------------------------------------------------------- + + /** + * Converts $body into XML, and sets the correct Content-Type. + * + * @param $body + * + * @return $this + */ + public function setXML($body) + { + $this->body = $this->formatBody($body, 'xml'); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Retrieves the current body into XML and returns it. + * + * @return mixed|string + */ + public function getXML() + { + $body = $this->body; + + if ($this->bodyFormat != 'xml') + { + $config = new Format(); + $formatter = $config->getFormatter('application/xml'); + + $body = $formatter->format($body); + } + + return $body; + } + + //-------------------------------------------------------------------- + + /** + * Handles conversion of the of the data into the appropriate format, + * and sets the correct Content-Type header for our response. + * + * @param $body + * @param string $format Valid: json, xml + * + * @return mixed + */ + protected function formatBody($body, string $format) + { + $mime = "application/{$format}"; + $this->setContentType($mime); + $this->bodyFormat = $format; + + // Nothing much to do for a string... + if (! is_string($body)) + { + $config = new Format(); + $formatter = $config->getFormatter($mime); + + $body = $formatter->format($body); + } + + return $body; + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- // Cache Control Methods // @@ -467,7 +614,7 @@ class Response extends Message implements ResponseInterface if ($date instanceof \DateTime) { $date->setTimezone(new \DateTimeZone('UTC')); - $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT'); + $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); } elseif (is_string($date)) { @@ -498,6 +645,7 @@ class Response extends Message implements ResponseInterface $this->sendHeaders(); $this->sendBody(); + $this->sendCookies(); return $this; } @@ -525,12 +673,13 @@ class Response extends Message implements ResponseInterface } // HTTP Status - header(sprintf('HTTP/%s %s %s', $this->protocolVersion, $this->statusCode, $this->reason), true, $this->statusCode); + header(sprintf('HTTP/%s %s %s', $this->protocolVersion, $this->statusCode, $this->reason), true, + $this->statusCode); // Send all of our headers foreach ($this->getHeaders() as $name => $values) { - header($name . ': ' . $this->getHeaderLine($name), false, $this->statusCode); + header($name.': '.$this->getHeaderLine($name), false, $this->statusCode); } return $this; @@ -552,6 +701,16 @@ class Response extends Message implements ResponseInterface //-------------------------------------------------------------------- + /** + * Grabs the current body. + * + * @return mixed|string + */ + public function getBody() + { + return $this->body; + } + /** * Perform a redirect to a new URL, in two flavors: header or location. * @@ -565,16 +724,18 @@ class Response extends Message implements ResponseInterface public function redirect(string $uri, string $method = 'auto', int $code = null) { // IIS environment likely? Use 'refresh' for better compatibility - if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) + if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) + && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false) { $method = 'refresh'; } elseif ($method !== 'refresh' && (empty($code) || ! is_numeric($code))) { - if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1') + if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1) { - $code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? 303 // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get - : 307; + $code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? 303 + // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get + : 307; } else { @@ -585,7 +746,7 @@ class Response extends Message implements ResponseInterface switch ($method) { case 'refresh': - $this->setHeader('Refresh', '0;url=' . $uri); + $this->setHeader('Refresh', '0;url='.$uri); break; default: $this->setHeader('Location', $uri); @@ -617,9 +778,15 @@ class Response extends Message implements ResponseInterface * @param bool|false $httponly Whether only make the cookie accessible via HTTP (no javascript) */ public function setCookie( - $name, $value = '', $expire = '', $domain = '', $path = '/', $prefix = '', $secure = false, $httponly = false - ) - { + $name, + $value = '', + $expire = '', + $domain = '', + $path = '/', + $prefix = '', + $secure = false, + $httponly = false + ) { if (is_array($name)) { // always leave 'name' in last place, as the loop will break otherwise, due to $$item @@ -657,20 +824,111 @@ class Response extends Message implements ResponseInterface $httponly = $this->cookieHTTPOnly; } - if ( ! is_numeric($expire)) + if (! is_numeric($expire)) { - $expire = time() - 86500; + $expire = time()-86500; } else { - $expire = ($expire > 0) ? time() + $expire : 0; + $expire = ($expire > 0) ? time()+$expire : 0; } - setcookie($prefix . $name, $value, $expire, $path, $domain, $secure, $httponly); + $this->cookies[] = [ + 'name' => $prefix.$name, + 'value' => $value, + 'expires' => $expire, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => $httponly, + ]; + + return $this; } //-------------------------------------------------------------------- + /** + * Checks to see if the Response has a specified cookie or not. + * + * @param string $name + * @param null $value + * @param string $prefix + * + * @return bool + */ + public function hasCookie(string $name, $value = null, string $prefix = '') + { + if ($prefix === '' && $this->cookiePrefix !== '') + { + $prefix = $this->cookiePrefix; + } + + $name = $prefix.$name; + + foreach ($this->cookies as $cookie) + { + if ($cookie['name'] != $prefix.$name) + { + continue; + } + + if ($value === null) + { + return true; + } + + return $cookie['value'] == $value; + } + + return false; + } + + /** + * Returns the cookie + * + * @param string $name + * @param string $prefix + * + * @return mixed + */ + public function getCookie(string $name, string $prefix = '') + { + if ($prefix === '' && $this->cookiePrefix !== '') + { + $prefix = $this->cookiePrefix; + } + + $name = $prefix.$name; + + foreach ($this->cookies as $cookie) + { + if ($cookie['name'] == $name) + { + return $cookie; + } + } + } + + /** + * Actually sets the cookies. + */ + protected function sendCookies() + { + if ($this->pretend) + { + return; + } + + foreach ($this->cookies as $params) + { + // PHP cannot unpack array with string keys + $params = array_values($params); + + setcookie(...$params); + } + } + /** * Force a download. * @@ -689,7 +947,7 @@ class Response extends Message implements ResponseInterface } elseif ($data === null) { - if ( ! @is_file($filename) || ($filesize = @filesize($filename)) === false) + if (! @is_file($filename) || ($filesize = @filesize($filename)) === false) { return; } @@ -706,12 +964,12 @@ class Response extends Message implements ResponseInterface // Set the default MIME type to send $mime = 'application/octet-stream'; - $x = explode('.', $filename); + $x = explode('.', $filename); $extension = end($x); if ($setMime === true) { - if (count($x) === 1 OR $extension === '') + if (count($x) === 1 || $extension === '') { /* If we're going to detect the MIME type, * we'll need a file extension. @@ -728,10 +986,11 @@ class Response extends Message implements ResponseInterface * * Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/ */ - if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) + if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT']) + && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) { - $x[count($x) - 1] = strtoupper($extension); - $filename = implode('.', $x); + $x[count($x)-1] = strtoupper($extension); + $filename = implode('.', $x); } if ($data === null && ($fp = @fopen($filepath, 'rb')) === false) @@ -746,11 +1005,11 @@ class Response extends Message implements ResponseInterface } // Generate the server headers - header('Content-Type: ' . $mime); - header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Type: '.$mime); + header('Content-Disposition: attachment; filename="'.$filename.'"'); header('Expires: 0'); header('Content-Transfer-Encoding: binary'); - header('Content-Length: ' . $filesize); + header('Content-Length: '.$filesize); header('Cache-Control: private, no-transform, no-store, must-revalidate'); // If we have raw data - just dump it @@ -760,7 +1019,7 @@ class Response extends Message implements ResponseInterface } // Flush 1MB chunks of data - while ( ! feof($fp) && ($data = fread($fp, 1048576)) !== false) + while (! feof($fp) && ($data = fread($fp, 1048576)) !== false) { echo $data; } @@ -769,5 +1028,4 @@ class Response extends Message implements ResponseInterface exit; } - //-------------------------------------------------------------------- } diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 8107cf0bf6..f0a9e82eb3 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -60,6 +60,8 @@ interface ResponseInterface // Informational const HTTP_CONTINUE = 100; const HTTP_SWITCHING_PROTOCOLS = 101; + const HTTP_PROCESSING = 102; + const HTTP_EARLY_HINTS = 103; // Success const HTTP_OK = 200; const HTTP_CREATED = 201; diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 3ece306e54..56ac99f02e 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -1,5 +1,7 @@ applyParts($parts); @@ -463,7 +465,7 @@ class URI if ($number > count($this->segments)) { - throw new \InvalidArgumentException('Request URI segment is our of range.'); + throw HTTPException::forURISegmentOutOfRange($number); } return $this->segments[$number] ?? ''; @@ -639,7 +641,7 @@ class URI if ($port <= 0 || $port > 65535) { - throw new \InvalidArgumentException('Invalid port given.'); + throw HTTPException::forInvalidPort($port); } $this->port = $port; @@ -679,7 +681,7 @@ class URI { if (strpos($query, '#') !== false) { - throw new \InvalidArgumentException('Query strings may not include URI fragments.'); + throw HTTPException::forMalformedQueryString(); } // Can't have leading ? @@ -698,11 +700,11 @@ class URI // Only 1 part? if (is_null($value)) { - $parts[$this->filterQuery($key)] = null; + $parts[$key] = null; continue; } - $parts[$this->filterQuery($key)] = $this->filterQuery($value); + $parts[$key] = $value; } $this->query = $parts; @@ -736,27 +738,6 @@ class URI //-------------------------------------------------------------------- - /** - * Ensures the query string has only acceptable characters - * per RFC 3986 - * - * @see http://tools.ietf.org/html/rfc3986 - * - * @param $str - * - * @return string The filtered query value. - */ - protected function filterQuery($str) - { - return preg_replace_callback( - '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', function(array $matches) { - return rawurlencode($matches[0]); - }, $str - ); - } - - //-------------------------------------------------------------------- - /** * A convenience method to pass an array of items in as the Query * portion of the URI. @@ -924,7 +905,7 @@ class URI } if ( ! empty($parts['fragment'])) { - $this->fragment = $this->filterQuery($parts['fragment']); + $this->fragment = $parts['fragment']; } // Scheme @@ -946,7 +927,7 @@ class URI if (1 > $port || 0xffff < $port) { - throw new \InvalidArgumentException('Ports must be between 1 and 65535'); + throw HTTPException::forInvalidPort($port); } $this->port = $port; diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php new file mode 100644 index 0000000000..94684cce96 --- /dev/null +++ b/system/HTTP/UserAgent.php @@ -0,0 +1,463 @@ +config = new UserAgents(); + } + + if (isset($_SERVER['HTTP_USER_AGENT'])) + { + $this->agent = trim($_SERVER['HTTP_USER_AGENT']); + $this->compileData(); + } + } + + //-------------------------------------------------------------------- + + /** + * Is Browser + * + * @param string $key + * + * @return bool + */ + public function isBrowser($key = null) + { + if (! $this->isBrowser) + { + return false; + } + + // No need to be specific, it's a browser + if ($key === null) + { + return true; + } + + // Check for a specific browser + return (isset($this->config->browsers[$key]) && $this->browser === $this->config->browsers[$key]); + } + + //-------------------------------------------------------------------- + + /** + * Is Robot + * + * @param string $key + * + * @return bool + */ + public function isRobot($key = null) + { + if (! $this->isRobot) + { + return false; + } + + // No need to be specific, it's a robot + if ($key === null) + { + return true; + } + + // Check for a specific robot + return (isset($this->config->robots[$key]) && $this->robot === $this->config->robots[$key]); + } + + //-------------------------------------------------------------------- + + /** + * Is Mobile + * + * @param string $key + * + * @return bool + */ + public function isMobile($key = null) + { + if (! $this->isMobile) + { + return false; + } + + // No need to be specific, it's a mobile + if ($key === null) + { + return true; + } + + // Check for a specific robot + return (isset($this->config->mobiles[$key]) && $this->mobile === $this->config->mobiles[$key]); + } + + //-------------------------------------------------------------------- + + /** + * Is this a referral from another site? + * + * @return bool + */ + public function isReferral() + { + if (! isset($this->referrer)) + { + if (empty($_SERVER['HTTP_REFERER'])) + { + $this->referrer = false; + } + else + { + $referer_host = @parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST); + $own_host = parse_url(\base_url(), PHP_URL_HOST); + + $this->referrer = ($referer_host && $referer_host !== $own_host); + } + } + + return $this->referrer; + } + + //-------------------------------------------------------------------- + + /** + * Agent String + * + * @return string + */ + public function getAgentString() + { + return $this->agent; + } + + //-------------------------------------------------------------------- + + /** + * Get Platform + * + * @return string + */ + public function getPlatform() + { + return $this->platform; + } + + //-------------------------------------------------------------------- + + /** + * Get Browser Name + * + * @return string + */ + public function getBrowser() + { + return $this->browser; + } + + //-------------------------------------------------------------------- + + /** + * Get the Browser Version + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + //-------------------------------------------------------------------- + + /** + * Get The Robot Name + * + * @return string + */ + public function getRobot() + { + return $this->robot; + } + //-------------------------------------------------------------------- + + /** + * Get the Mobile Device + * + * @return string + */ + public function getMobile() + { + return $this->mobile; + } + + //-------------------------------------------------------------------- + + /** + * Get the referrer + * + * @return bool + */ + public function getReferrer() + { + return empty($_SERVER['HTTP_REFERER']) ? '' : trim($_SERVER['HTTP_REFERER']); + } + + //-------------------------------------------------------------------- + + /** + * Parse a custom user-agent string + * + * @param string $string + * + * @return void + */ + public function parse($string) + { + // Reset values + $this->isBrowser = false; + $this->isRobot = false; + $this->isMobile = false; + $this->browser = ''; + $this->version = ''; + $this->mobile = ''; + $this->robot = ''; + + // Set the new user-agent string and parse it, unless empty + $this->agent = $string; + + if (! empty($string)) + { + $this->compileData(); + } + } + //-------------------------------------------------------------------- + + /** + * Compile the User Agent Data + * + * @return bool + */ + protected function compileData() + { + $this->setPlatform(); + + foreach (['setRobot', 'setBrowser', 'setMobile'] as $function) + { + if ($this->$function() === true) + { + break; + } + } + } + + //-------------------------------------------------------------------- + + /** + * Set the Platform + * + * @return bool + */ + protected function setPlatform() + { + if (is_array($this->config->platforms) && count($this->config->platforms) > 0) + { + foreach ($this->config->platforms as $key => $val) + { + if (preg_match('|'.preg_quote($key).'|i', $this->agent)) + { + $this->platform = $val; + + return true; + } + } + } + + $this->platform = 'Unknown Platform'; + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Set the Browser + * + * @return bool + */ + protected function setBrowser() + { + if (is_array($this->config->browsers) && count($this->config->browsers) > 0) + { + foreach ($this->config->browsers as $key => $val) + { + if (preg_match('|'.$key.'.*?([0-9\.]+)|i', $this->agent, $match)) + { + $this->isBrowser = true; + $this->version = $match[1]; + $this->browser = $val; + $this->setMobile(); + + return true; + } + } + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Set the Robot + * + * @return bool + */ + protected function setRobot() + { + if (is_array($this->config->robots) && count($this->config->robots) > 0) + { + foreach ($this->config->robots as $key => $val) + { + if (preg_match('|'.preg_quote($key).'|i', $this->agent)) + { + $this->isRobot = true; + $this->robot = $val; + $this->setMobile(); + + return true; + } + } + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Set the Mobile Device + * + * @return bool + */ + protected function setMobile() + { + if (is_array($this->config->mobiles) && count($this->config->mobiles) > 0) + { + foreach ($this->config->mobiles as $key => $val) + { + if (false !== (stripos($this->agent, $key))) + { + $this->isMobile = true; + $this->mobile = $val; + + return true; + } + } + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Outputs the original Agent String when cast as a string. + * + * @return string + */ + public function __toString() + { + return $this->getAgentString(); + } + +} diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index d4c09f7aa8..346404b613 100755 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -73,10 +73,7 @@ if ( ! function_exists('set_cookie')) // The following line shows as a syntax error in NetBeans IDE //(\Config\Services::response())->setcookie $response = \Config\Services::response(); - $response->setcookie - ( - $name, $value, $expire, $domain, $path, $prefix, $secure, $httpOnly - ); + $response->setcookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httpOnly); } } diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index 0e274a4710..f947305a10 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -1,4 +1,5 @@ 0) ? @rmdir($path) : true; + } + catch (\Exception $fe) { return false; } - - while (false !== ($filename = @readdir($current_dir))) - { - if ($filename !== '.' && $filename !== '..') - { - if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.') - { - delete_files($path . DIRECTORY_SEPARATOR . $filename, $delDir, $htdocs, $_level + 1); - } - elseif ($htdocs !== true || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename)) - { - @unlink($path . DIRECTORY_SEPARATOR . $filename); - } - } - } - - closedir($current_dir); - - return ($delDir === true && $_level > 0) ? @rmdir($path) : true; } } @@ -217,8 +228,9 @@ if ( ! function_exists('get_filenames')) { static $filedata = []; - if ($fp = @opendir($source_dir)) + try { + $fp = opendir($source_dir); // reset the array and make sure $source_dir has a trailing slash on the initial call if ($recursion === false) { @@ -241,8 +253,10 @@ if ( ! function_exists('get_filenames')) closedir($fp); return $filedata; } - - return []; + catch (\Exception $fe) + { + return []; + } } } @@ -271,34 +285,38 @@ if ( ! function_exists('get_dir_file_info')) static $filedata = []; $relative_path = $source_dir; - if ($fp = @opendir($source_dir)) + try { - // reset the array and make sure $source_dir has a trailing slash on the initial call - if ($recursion === false) - { - $filedata = []; - $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } - - // Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast - while (false !== ($file = readdir($fp))) - { - if (is_dir($source_dir . $file) && $file[0] !== '.' && $top_level_only === false) + $fp = @opendir($source_dir); { + // reset the array and make sure $source_dir has a trailing slash on the initial call + if ($recursion === false) { - get_dir_file_info($source_dir . $file . DIRECTORY_SEPARATOR, $top_level_only, true); + $filedata = []; + $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } - elseif ($file[0] !== '.') - { - $filedata[$file] = get_file_info($source_dir . $file); - $filedata[$file]['relative_path'] = $relative_path; - } - } - closedir($fp); - return $filedata; + // Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast + while (false !== ($file = readdir($fp))) + { + if (is_dir($source_dir . $file) && $file[0] !== '.' && $top_level_only === false) + { + get_dir_file_info($source_dir . $file . DIRECTORY_SEPARATOR, $top_level_only, true); + } + elseif ($file[0] !== '.') + { + $filedata[$file] = get_file_info($source_dir . $file); + $filedata[$file]['relative_path'] = $relative_path; + } + } + + closedir($fp); + return $filedata; + } + } + catch (\Exception $fe) + { + return []; } - - return []; } } @@ -319,9 +337,9 @@ if ( ! function_exists('get_file_info')) * @param string $file Path to file * @param mixed $returned_values Array or comma separated string of information returned * - * @return array + * @return array|null */ - function get_file_info(string $file, $returned_values = ['name', 'server_path', 'size', 'date']): array + function get_file_info(string $file, $returned_values = ['name', 'server_path', 'size', 'date']) { if ( ! file_exists($file)) { @@ -335,8 +353,7 @@ if ( ! function_exists('get_file_info')) foreach ($returned_values as $key) { - switch ($key) - { + switch ($key) { case 'name': $fileinfo['name'] = basename($file); break; diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index f9180b74cc..dcb7ab750d 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -236,7 +236,7 @@ if ( ! function_exists('form_password')) */ function form_password($data = '', string $value = '', $extra = ''): string { - is_array($data) OR $data = ['name' => $data]; + is_array($data) || $data = ['name' => $data]; $data['type'] = 'password'; return form_input($data, $value, $extra); @@ -263,7 +263,7 @@ if ( ! function_exists('form_upload')) function form_upload($data = '', string $value = '', $extra = ''): string { $defaults = ['type' => 'file', 'name' => '']; - is_array($data) OR $data = ['name' => $data]; + is_array($data) || $data = ['name' => $data]; $data['type'] = 'file'; return '\n"; @@ -292,7 +292,7 @@ if ( ! function_exists('form_textarea')) 'cols' => '40', 'rows' => '10', ]; - if ( ! is_array($data) OR ! isset($data['value'])) + if ( ! is_array($data) || ! isset($data['value'])) { $val = $value; } @@ -374,8 +374,8 @@ if ( ! function_exists('form_dropdown')) $defaults = ['name' => $data]; } - is_array($selected) OR $selected = [$selected]; - is_array($options) OR $options = [$options]; + is_array($selected) || $selected = [$selected]; + is_array($options) || $options = [$options]; // If no selected state was submitted we will attempt to set it automatically if (empty($selected)) @@ -490,7 +490,7 @@ if ( ! function_exists('form_radio')) */ function form_radio($data = '', string $value = '', bool $checked = false, $extra = ''): string { - is_array($data) OR $data = ['name' => $data]; + is_array($data) || $data = ['name' => $data]; $data['type'] = 'radio'; return form_checkbox($data, $value, $checked, $extra); @@ -868,7 +868,7 @@ if ( ! function_exists('set_checkbox')) } // Unchecked checkbox and radio inputs are not even submitted by browsers ... - if ($_POST) + if (! empty($request->getPost()) || ! empty(old($field))) { return ($input === $value) ? ' checked="checked"' : ''; } @@ -925,7 +925,7 @@ if ( ! function_exists('set_radio')) } // Unchecked checkbox and radio inputs are not even submitted by browsers ... - if ($_POST) + if ($request->getPost()) { return ($input === $value) ? ' checked="checked"' : ''; } diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index 0c75a5f68e..356d1989a6 100644 --- a/system/Helpers/number_helper.php +++ b/system/Helpers/number_helper.php @@ -268,7 +268,7 @@ if ( ! function_exists('number_to_roman')) function number_to_roman($num) { $num = (int) $num; - if ($num < 1 OR $num > 3999) + if ($num < 1 || $num > 3999) { return; } diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 87b102b2d6..4133fe708d 100755 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -441,7 +441,7 @@ if ( ! function_exists('word_wrap')) function word_wrap(string $str, int $charlim = 76): string { // Set the character limit - is_numeric($charlim) OR $charlim = 76; + is_numeric($charlim) || $charlim = 76; // Reduce multiple spaces $str = preg_replace('| +|', ' ', $str); diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index ba1a74592b..e45fbe8b8f 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -565,14 +565,14 @@ if ( ! function_exists('prep_url')) */ function prep_url($str = ''): string { - if ($str === 'http://' OR $str === '') + if ($str === 'http://' || $str === '') { return ''; } $url = parse_url($str); - if ( ! $url OR ! isset($url['scheme'])) + if ( ! $url || ! isset($url['scheme'])) { return 'http://' . $str; } diff --git a/system/Honeypot/Exceptions/HoneypotException.php b/system/Honeypot/Exceptions/HoneypotException.php new file mode 100644 index 0000000000..c6085655c9 --- /dev/null +++ b/system/Honeypot/Exceptions/HoneypotException.php @@ -0,0 +1,28 @@ +config = $config; + + if($this->config->hidden === '') + { + throw HoneypotException::forNoHiddenValue(); + } + + if($this->config->template === '') + { + throw HoneypotException::forNoTemplate(); + } + + if($this->config->name === '') + { + throw HoneypotException::forNoNameField(); + } + } + + //-------------------------------------------------------------------- + + /** + * Checks the request if honeypot field has data. + * + * @param \CodeIgniter\HTTP\RequestInterface $request + * + */ + public function hasContent(RequestInterface $request) + { + if($request->getVar($this->config->name)) + { + return true; + } + return false; + } + + /** + * Attachs Honeypot template to response. + * + * @param \CodeIgniter\HTTP\ResponseInterface $response + */ + public function attachHoneypot(ResponseInterface $response) + { + $prep_field = $this->prepareTemplate($this->config->template); + + $body = $response->getBody(); + $body = str_ireplace('', $prep_field, $body); + $response->setBody($body); + } + + /** + * Prepares the template by adding label + * content and field name. + * + * @param string $template + * @return string + */ + protected function prepareTemplate($template): string + { + $template = str_ireplace('{label}', $this->config->label, $template); + $template = str_ireplace('{name}', $this->config->name, $template); + + if($this->config->hidden) + { + $template = '
'. $template . '
'; + } + return $template; + } + +} \ No newline at end of file diff --git a/system/I18n/Exceptions/I18nException.php b/system/I18n/Exceptions/I18nException.php new file mode 100644 index 0000000000..2c50055170 --- /dev/null +++ b/system/I18n/Exceptions/I18nException.php @@ -0,0 +1,32 @@ + 12) { - throw new \InvalidArgumentException(lang('time.invalidMonth')); + throw I18nException::forInvalidMonth($value); } if (is_string($value) && ! is_numeric($value)) @@ -640,7 +641,7 @@ class Time extends DateTime { if ($value < 0 || $value > 31) { - throw new \InvalidArgumentException(lang('time.invalidDay')); + throw I18nException::forInvalidDay($value); } return $this->setValue('day', $value); @@ -657,7 +658,7 @@ class Time extends DateTime { if ($value < 0 || $value > 23) { - throw new \InvalidArgumentException(lang('time.invalidHours')); + throw I18nException::forInvalidHour($value); } return $this->setValue('hour', $value); @@ -674,7 +675,7 @@ class Time extends DateTime { if ($value < 0 || $value > 59) { - throw new \InvalidArgumentException(lang('time.invalidMinutes')); + throw I18nException::forInvalidMinutes($value); } return $this->setValue('minute', $value); @@ -691,7 +692,7 @@ class Time extends DateTime { if ($value < 0 || $value > 59) { - throw new \InvalidArgumentException(lang('time.invalidSeconds')); + throw I18nException::forInvalidSeconds($value); } return $this->setValue('second', $value); diff --git a/system/Images/Exceptions/ImageException.php b/system/Images/Exceptions/ImageException.php index 9615192b7d..cf8d355d24 100644 --- a/system/Images/Exceptions/ImageException.php +++ b/system/Images/Exceptions/ImageException.php @@ -1,3 +1,42 @@ image = new Image($path, true); - $this->image->getProperties(); + $this->image->getProperties(false); + $this->width = $this->image->origWidth; + $this->height = $this->image->origHeight; return $this; } //-------------------------------------------------------------------- + /** + * Make the image resource object if needed + */ + protected function ensureResource() + { + if ($this->resource == null) + { + $path = $this->image->getPathname(); + // if valid image type, make corresponding image resource + switch ($this->image->imageType) + { + case IMAGETYPE_GIF: + $this->resource = imagecreatefromgif($path); + break; + case IMAGETYPE_JPEG: + $this->resource = imagecreatefromjpeg($path); + break; + case IMAGETYPE_PNG: + $this->resource = imagecreatefrompng($path); + break; + } + } + } + + //-------------------------------------------------------------------- + /** * Returns the image instance. * @@ -140,6 +168,7 @@ abstract class BaseHandler implements ImageHandlerInterface */ public function getResource() { + $this->ensureResource(); return $this->resource; } @@ -228,19 +257,18 @@ abstract class BaseHandler implements ImageHandlerInterface if ($angle === '' || ! in_array($angle, $degs)) { - throw new ImageException(lang('images.rotationAngleRequired')); + throw ImageException::forMissingAngle(); } + // cast angle as an int, for our use + $angle = (int) $angle; + // Reassign the width and height - if ($angle === 90 OR $angle === 270) + if ($angle === 90 || $angle === 270) { - $this->width = $this->image->origHeight; - $this->height = $this->image->origWidth; - } - else - { - $this->width = $this->image->origWidth; - $this->height = $this->image->origHeight; + $temp = $this->height; + $this->width = $this->height; + $this->height = $temp; } // Call the Handler-specific version. @@ -303,13 +331,13 @@ abstract class BaseHandler implements ImageHandlerInterface * * @return $this */ - public function flip(string $dir) + public function flip(string $dir = 'vertical') { $dir = strtolower($dir); if ($dir !== 'vertical' && $dir !== 'horizontal') { - throw new ImageException(lang('images.invalidDirection')); + throw ImageException::forInvalidDirection($dir); } return $this->_flip($dir); @@ -347,7 +375,7 @@ abstract class BaseHandler implements ImageHandlerInterface * @param string $text * @param array $options * - * @return BaseHandler + * @return $this */ public function text(string $text, array $options = []) { @@ -437,12 +465,9 @@ abstract class BaseHandler implements ImageHandlerInterface { return null; } - - throw new ImageException(lang('images.exifNotSupported')); } $exif = exif_read_data($this->image->getPathname()); - if ( ! is_null($key) && is_array($exif)) { $exif = array_key_exists($key, $exif) ? $exif[$key] : false; @@ -662,7 +687,12 @@ abstract class BaseHandler implements ImageHandlerInterface */ protected function reproportion() { - if (($this->width === 0 && $this->height === 0) || $this->image->origWidth === 0 || $this->image->origHeight === 0 || ( ! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height)) || ! ctype_digit((string) $this->image->origWidth) || ! ctype_digit((string) $this->image->origHeight) + if (($this->width === 0 && $this->height === 0) || + $this->image->origWidth === 0 || + $this->image->origHeight === 0 || + ( ! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height)) || + ! ctype_digit((string) $this->image->origWidth) || + ! ctype_digit((string) $this->image->origHeight) ) { return; @@ -700,4 +730,16 @@ abstract class BaseHandler implements ImageHandlerInterface } //-------------------------------------------------------------------- + // accessor for testing; not part of interface + public function getWidth() + { + return ($this->resource != null) ? $this->_getWidth() : $this->width; + } + + // accessor for testing; not part of interface + public function getHeight() + { + return ($this->resource != null) ? $this->_getHeight() : $this->height; + } + } diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index bbda86a8cb..6719b560c5 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -42,21 +42,17 @@ class GDHandler extends BaseHandler public $version; - /** - * Stores image resource in memory. - * - * @var - */ - protected $resource; - public function __construct($config = null) { parent::__construct($config); + // We should never see this, so can't test it + // @codeCoverageIgnoreStart if ( ! extension_loaded('gd')) { - throw new ImageException('GD Extension is not loaded.'); + throw ImageException::forMissingExtension('GD'); } + // @codeCoverageIgnoreEnd } //-------------------------------------------------------------------- @@ -72,10 +68,7 @@ class GDHandler extends BaseHandler protected function _rotate(int $angle) { // Create the image handle - if ( ! ($srcImg = $this->createImage())) - { - return false; - } + $srcImg = $this->createImage(); // Set the background color // This won't work with transparent PNG files so we are @@ -85,7 +78,7 @@ class GDHandler extends BaseHandler $white = imagecolorallocate($srcImg, 255, 255, 255); // Rotate it! - $destImg = imagerotate($this->resource, $angle, $white); + $destImg = imagerotate($srcImg, $angle, $white); // Kill the file handles imagedestroy($srcImg); @@ -106,12 +99,9 @@ class GDHandler extends BaseHandler * * @return $this */ - public function _flatten(int $red = 255, int $green = 255, int $blue = 255) { - - if ( ! ($src = $this->createImage())) - { - return false; - } + public function _flatten(int $red = 255, int $green = 255, int $blue = 255) + { + $srcImg = $this->createImage(); if (function_exists('imagecreatetruecolor')) { @@ -128,15 +118,14 @@ class GDHandler extends BaseHandler $matte = imagecolorallocate($dest, $red, $green, $blue); imagefilledrectangle($dest, 0, 0, $this->width, $this->height, $matte); - imagecopy($dest, $src, 0, 0, 0, 0, $this->width, $this->height); + imagecopy($dest, $srcImg, 0, 0, 0, 0, $this->width, $this->height); // Kill the file handles - imagedestroy($src); + imagedestroy($srcImg); $this->resource = $dest; return $this; - } //-------------------------------------------------------------------- @@ -157,7 +146,7 @@ class GDHandler extends BaseHandler if ($direction === 'horizontal') { - for ($i = 0; $i < $height; $i ++ ) + for ($i = 0; $i < $height; $i ++) { $left = 0; $right = $width - 1; @@ -177,7 +166,7 @@ class GDHandler extends BaseHandler } else { - for ($i = 0; $i < $width; $i ++ ) + for ($i = 0; $i < $width; $i ++) { $top = 0; $bottom = $height - 1; @@ -326,38 +315,38 @@ class GDHandler extends BaseHandler case IMAGETYPE_GIF: if ( ! function_exists('imagegif')) { - throw new ImageException(lang('images.unsupportedImagecreate') . ' ' . lang('images.gifNotSupported')); + throw ImageException::forInvalidImageCreate(lang('images.gifNotSupported')); } if ( ! @imagegif($this->resource, $target)) { - throw new ImageException(lang('images.saveFailed')); + throw ImageException::forSaveFailed(); } break; case IMAGETYPE_JPEG: if ( ! function_exists('imagejpeg')) { - throw new ImageException(lang('images.unsupportedImagecreate') . ' ' . lang('images.jpgNotSupported')); + throw ImageException::forInvalidImageCreate(lang('images.jpgNotSupported')); } if ( ! @imagejpeg($this->resource, $target, $quality)) { - throw new ImageException(lang('images.saveFailed')); + throw ImageException::forSaveFailed(); } break; case IMAGETYPE_PNG: if ( ! function_exists('imagepng')) { - throw new ImageException(lang('images.unsupportedImagecreate') . ' ' . lang('images.pngNotSupported')); + throw ImageException::forInvalidImageCreate(lang('images.pngNotSupported')); } if ( ! @imagepng($this->resource, $target)) { - throw new ImageException(lang('images.saveFailed')); + throw ImageException::forSaveFailed(); } break; default: - throw new ImageException(lang('images.unsupportedImagecreate')); + throw ImageException::forInvalidImageCreate(); break; } @@ -403,26 +392,26 @@ class GDHandler extends BaseHandler case IMAGETYPE_GIF: if ( ! function_exists('imagecreatefromgif')) { - throw new ImageException(lang('images.gifNotSupported')); + throw ImageException::forInvalidImageCreate(lang('images.gifNotSupported')); } return imagecreatefromgif($path); case IMAGETYPE_JPEG: if ( ! function_exists('imagecreatefromjpeg')) { - throw new ImageException(lang('images.jpgNotSupported')); + throw ImageException::forInvalidImageCreate(lang('images.jpgNotSupported')); } return imagecreatefromjpeg($path); case IMAGETYPE_PNG: if ( ! function_exists('imagecreatefrompng')) { - throw new ImageException(lang('images.pngNotSupported')); + throw ImageException::forInvalidImageCreate(lang('images.pngNotSupported')); } return imagecreatefrompng($path); default: - throw new ImageException(lang('images.unsupportedImagecreate')); + throw ImageException::forInvalidImageCreate('Ima'); } } @@ -560,4 +549,15 @@ class GDHandler extends BaseHandler } //-------------------------------------------------------------------- + + public function _getWidth() + { + return imagesx($this->resource); + } + + public function _getHeight() + { + return imagesy($this->resource); + } + } diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 9634360767..a720730909 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -36,6 +36,7 @@ * @filesource */ use CodeIgniter\Images\Exceptions\ImageException; +use CodeIgniter\Images\Image; /** * Class ImageMagickHandler @@ -43,6 +44,10 @@ use CodeIgniter\Images\Exceptions\ImageException; * To make this library as compatible as possible with the broadest * number of installations, we do not use the Imagick extension, * but simply use the command line version. + * + * hmm - the width & height accessors at the end use the imagick extension. + * + * FIXME - This needs conversion & unit testing, to use the imagick extension * * @package CodeIgniter\Images\Handlers */ @@ -60,6 +65,13 @@ class ImageMagickHandler extends BaseHandler //-------------------------------------------------------------------- + public function __construct($config = null) + { + parent::__construct($config); + } + + //-------------------------------------------------------------------- + /** * Handles the actual resizing of the image. * @@ -75,7 +87,8 @@ class ImageMagickHandler extends BaseHandler //todo FIX THIS HANDLER PROPERLY $escape = "\\"; - if (strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN') { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') + { $escape = ""; } @@ -140,9 +153,10 @@ class ImageMagickHandler extends BaseHandler * * @return $this */ - public function _flatten(int $red = 255, int $green = 255, int $blue = 255){ + public function _flatten(int $red = 255, int $green = 255, int $blue = 255) + { - $flatten = "-background RGB({$red},{$green},{$blue}) -flatten"; + $flatten = "-background RGB({$red},{$green},{$blue}) -flatten"; $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); $destination = $this->getResourcePath(); @@ -209,7 +223,7 @@ class ImageMagickHandler extends BaseHandler // Do we have a vaild library path? if (empty($this->config->libraryPath)) { - throw new ImageException(lang('images.libPathInvalid')); + throw ImageException::forInvalidImageLibraryPath($this->config->libraryPath); } if ( ! preg_match('/convert$/i', $this->config->libraryPath)) @@ -230,7 +244,7 @@ class ImageMagickHandler extends BaseHandler // Did it work? if ($retval > 0) { - throw new ImageException(lang('imageProcessFailed')); + throw ImageException::forImageProcessFailed(); } return $output; @@ -407,4 +421,18 @@ class ImageMagickHandler extends BaseHandler } //-------------------------------------------------------------------- + + //-------------------------------------------------------------------- + + public function _getWidth() + { + return imagesx($this->resource); + } + + public function _getHeight() + { + return imagesy($this->resource); + } + + } diff --git a/system/Images/Image.php b/system/Images/Image.php index 92a7e48bce..7c7d5c87f4 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -36,6 +36,7 @@ * @filesource */ use CodeIgniter\Files\File; +use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Images\Exceptions\ImageException; class Image extends File @@ -96,7 +97,7 @@ class Image extends File if (empty($targetName)) { - throw new ImageException('Invalid file name.'); + throw ImageException::forInvalidFile($targetName); } if ( ! is_dir($targetPath)) @@ -106,7 +107,7 @@ class Image extends File if ( ! copy($this->getPathname(), "{$targetPath}{$targetName}")) { - throw new ImageException('Unable to copy image to new destination.'); + throw ImageException::forCopyError($targetPath); } chmod("{$targetPath}/{$targetName}", $perms); @@ -131,6 +132,7 @@ class Image extends File $vals = getimagesize($path); $types = [1 => 'gif', 2 => 'jpeg', 3 => 'png']; + $mime = 'image/' . ($types[$vals[2]] ?? 'jpg'); if ($return === true) diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php index 87ec21362b..f33774353e 100644 --- a/system/Images/ImageHandlerInterface.php +++ b/system/Images/ImageHandlerInterface.php @@ -44,8 +44,9 @@ interface ImageHandlerInterface * @param int $width * @param int $height * @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true. + * @param string $masterDim */ - public function resize(int $width, int $height, bool $maintainRatio = false); + public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto'); //-------------------------------------------------------------------- @@ -58,10 +59,12 @@ interface ImageHandlerInterface * @param int|null $height * @param int|null $x X-axis coord to start cropping from the left of image * @param int|null $y Y-axis coord to start cropping from the top of image + * @param bool $maintainRatio + * @param string $masterDim * * @return mixed */ - public function crop(int $width = null, int $height = null, int $x = null, int $y = null); + public function crop(int $width = null, int $height = null, int $x = null, int $y = null, bool $maintainRatio = false, string $masterDim = 'auto'); //-------------------------------------------------------------------- @@ -110,6 +113,17 @@ interface ImageHandlerInterface //-------------------------------------------------------------------- + /** + * Flip an image horizontally or vertically + * + * @param string $dir Direction to flip, either 'vertical' or 'horizontal' + * + * @return mixed + */ + public function flip(string $dir = 'vertical'); + + //-------------------------------------------------------------------- + /** * Combine cropping and resizing into a single command. * @@ -133,4 +147,42 @@ interface ImageHandlerInterface public function fit(int $width, int $height, string $position); //-------------------------------------------------------------------- + + /** + * Overlays a string of text over the image. + * + * Valid options: + * + * - color Text Color (hex number) + * - shadowColor Color of the shadow (hex number) + * - hAlign Horizontal alignment: left, center, right + * - vAlign Vertical alignment: top, middle, bottom + * - hOffset + * - vOffset + * - fontPath + * - fontSize + * - shadowOffset + * + * @param string $text + * @param array $options + * + * @return $this + */ + public function text(string $text, array $options = []); + + //-------------------------------------------------------------------- + + /** + * Saves any changes that have been made to file. + * + * Example: + * $image->resize(100, 200, true) + * ->save($target); + * + * @param string $target + * @param int $quality + * + * @return mixed + */ + public function save(string $target = null, int $quality = 90); } diff --git a/system/Language/Language.php b/system/Language/Language.php index 40a8c934d7..96cbdf73e3 100644 --- a/system/Language/Language.php +++ b/system/Language/Language.php @@ -35,7 +35,6 @@ * @since Version 3.0.0 * @filesource */ - class Language { @@ -85,6 +84,23 @@ class Language //-------------------------------------------------------------------- + /** + * Sets the current locale to use when performing string lookups. + * + * @param string $locale + * + * @return $this + */ + public function setLocale(string $locale = null) + { + if ( ! is_null($locale)) + { + $this->locale = $locale; + } + + return $this; + } + /** * Parses the language string for a file, loads the file, if necessary, * getting the line. @@ -98,15 +114,14 @@ class Language { // Parse out the file name and the actual alias. // Will load the language file and strings. - list($file, $line) = $this->parseLine($line); + list($file, $parsedLine) = $this->parseLine($line); - $output = $this->language[$file][$line] ?? $line; + $output = $this->language[$this->locale][$file][$parsedLine] ?? $line; - if (! empty($args)) + if ( ! empty($args)) { $output = $this->formatMessage($output, $args); } - return $output; } @@ -143,7 +158,7 @@ class Language return [ $file, - $this->language[$line] ?? $line + $this->language[$this->locale][$line] ?? $line ]; } @@ -191,30 +206,40 @@ class Language */ protected function load(string $file, string $locale, bool $return = false) { - if (in_array($file, $this->loadedFiles)) + if ( ! array_key_exists($locale, $this->loadedFiles)) { + $this->loadedFiles[$locale] = []; + } + + if (in_array($file, $this->loadedFiles[$locale])) + { + // Don't load it more than once. return []; } - if ( ! array_key_exists($file, $this->language)) + if ( ! array_key_exists($locale, $this->language)) { - $this->language[$file] = []; + $this->language[$locale] = []; + } + + if ( ! array_key_exists($file, $this->language[$locale])) + { + $this->language[$locale][$file] = []; } $path = "Language/{$locale}/{$file}.php"; $lang = $this->requireFile($path); - // Don't load it more than once. - $this->loadedFiles[] = $file; - if ($return) { return $lang; } + $this->loadedFiles[$locale][] = $file; + // Merge our string - $this->language[$file] = $lang; + $this->language[$this->locale][$file] = $lang; } //-------------------------------------------------------------------- diff --git a/system/Language/en/CLI.php b/system/Language/en/CLI.php index 5c3945bf1f..9b7dc156b5 100644 --- a/system/Language/en/CLI.php +++ b/system/Language/en/CLI.php @@ -1,44 +1,22 @@ 'Usage:', - 'helpDescription' => 'Description:', - 'helpOptions' => 'Options:', - 'helpArguments' => 'Arguments:', -]; \ No newline at end of file + 'helpUsage' => 'Usage:', + 'helpDescription' => 'Description:', + 'helpOptions' => 'Options:', + 'helpArguments' => 'Arguments:', + 'invalidColor' => 'Invalid {0} color: {1}.', +]; diff --git a/system/Language/en/Cache.php b/system/Language/en/Cache.php index 85591c2929..e0dbfed024 100644 --- a/system/Language/en/Cache.php +++ b/system/Language/en/Cache.php @@ -1,43 +1,20 @@ 'Cache config must have an array of $validHandlers.', - 'cacheNoBackup' => 'Cache config must have a handler and backupHandler set.', - 'cacheHandlerNotFound' => 'Cache config has an invalid handler or backup handler specified.', + 'invalidHandlers' => 'Cache config must have an array of $validHandlers.', + 'noBackup' => 'Cache config must have a handler and backupHandler set.', + 'handlerNotFound' => 'Cache config has an invalid handler or backup handler specified.', ]; diff --git a/system/Language/en/Core.php b/system/Language/en/Core.php new file mode 100644 index 0000000000..97d7b95195 --- /dev/null +++ b/system/Language/en/Core.php @@ -0,0 +1,21 @@ + 'Invalid file: {0}', + 'copyError' => 'An error was encountered while attempting to replace the file. Please make sure your file directory is writable.', + 'missingExtension' => '{0} extension is not loaded.', + 'noHandlers' => '{0} must provide at least one Handler.', +]; diff --git a/system/Language/en/Database.php b/system/Language/en/Database.php index 197e6d90e1..5181b9b448 100644 --- a/system/Language/en/Database.php +++ b/system/Language/en/Database.php @@ -1,5 +1,21 @@ '{0, string} is not a valid Model Event callback.', + 'invalidEvent' => '{0} is not a valid Model Event callback.', + 'invalidArgument' => 'You must provide a valid {0}.', + 'invalidAllowedFields' => 'Allowed fields must be specified for model: {0}', + 'emptyDataset' => 'There is no data to {0}.', ]; diff --git a/system/Language/en/Email.php b/system/Language/en/Email.php index b923cdc1b9..49d26f7c71 100644 --- a/system/Language/en/Email.php +++ b/system/Language/en/Email.php @@ -1,10 +1,23 @@ 'The email validation method must be passed an array.', - 'invalidAddress' => 'Invalid email address: {0, string}', - 'attachmentMissing' => 'Unable to locate the following email attachment: {0, string}', - 'attachmentUnreadable' => 'Unable to open this attachment: {0, string}', + 'invalidAddress' => 'Invalid email address: {0}', + 'attachmentMissing' => 'Unable to locate the following email attachment: {0}', + 'attachmentUnreadable' => 'Unable to open this attachment: {0}', 'noFrom' => 'Cannot send mail with no "From" header.', 'noRecipients' => 'You must include recipients: To, Cc, or Bcc', 'sendFailurePHPMail' => 'Unable to send email using PHP mail(). Your server might not be configured to send mail using this method.', @@ -13,11 +26,11 @@ return [ 'sent' => 'Your message has been successfully sent using the following protocol: {0, string}', 'noSocket' => 'Unable to open a socket to Sendmail. Please check settings.', 'noHostname' => 'You did not specify a SMTP hostname.', - 'SMYPError' => 'The following SMTP error was encountered: {0, string}', + 'SMTPError' => 'The following SMTP error was encountered: {0}', 'noSMTPAuth' => 'Error: You must assign a SMTP username and password.', - 'failedSMTPLogin' => 'Failed to send AUTH LOGIN command. Error: {0, string}', - 'SMTPAuthUsername' => 'Failed to authenticate username. Error: {0, string}', - 'SMTPAuthPassword' => 'Failed to authenticate password. Error: {0, string}', - 'SMTPDataFailure' => 'Unable to send data: {0, string}', - 'exitStatus' => 'Exit status code: {0, string}', + 'failedSMTPLogin' => 'Failed to send AUTH LOGIN command. Error: {0}', + 'SMTPAuthUsername' => 'Failed to authenticate username. Error: {0}', + 'SMTPAuthPassword' => 'Failed to authenticate password. Error: {0}', + 'SMTPDataFailure' => 'Unable to send data: {0}', + 'exitStatus' => 'Exit status code: {0}', ]; diff --git a/system/Language/en/Files.php b/system/Language/en/Files.php new file mode 100644 index 0000000000..6e1eb5f319 --- /dev/null +++ b/system/Language/en/Files.php @@ -0,0 +1,20 @@ + 'File not found: {0}', + 'cannotMove' => 'Could not move file {0} to {1} ({2})', + 'invalidFilename' => 'Target filename missing or invalid: {0}', + 'cannotCopy' => 'Could not copy to {0} - make sure the folder is writeable', +]; diff --git a/system/Language/en/Filters.php b/system/Language/en/Filters.php new file mode 100644 index 0000000000..3c84d5903d --- /dev/null +++ b/system/Language/en/Filters.php @@ -0,0 +1,19 @@ + '\'{0}\' filter must have a matching alias defined.', + 'incorrectInterface' => '{0} must implement CodeIgniter\Filters\FilterInterface.', +]; diff --git a/system/Language/en/Format.php b/system/Language/en/Format.php new file mode 100644 index 0000000000..2cf35607ed --- /dev/null +++ b/system/Language/en/Format.php @@ -0,0 +1,19 @@ + 'Failed to parse json string, error: "{0}".', + 'missingExtension' => 'The SimpleXML extension is required to format XML.', +]; diff --git a/system/Language/en/HTTP.php b/system/Language/en/HTTP.php index 37dadc58f4..49bf72fb1b 100644 --- a/system/Language/en/HTTP.php +++ b/system/Language/en/HTTP.php @@ -1,5 +1,54 @@ '{0, string} is not a valid route.', + // CurlRequest + 'missingCurl' => 'CURL must be enabled to use the CURLRequest class.', + 'invalidSSLKey' => 'Cannot set SSL Key. {0} is not a valid file.', + 'sslCertNotFound' => 'SSL certificate not found at: {0}', + 'curlError' => '{0} : {1}', + + // IncomingRequest + 'invalidNegotiationType' => '{0} is not a valid negotiation type. Must be one of: media, charset, encoding, language.', + + // Message + 'invalidHTTPProtocol' => 'Invalid HTTP Protocol Version. Must be one of: {0}', + + // Negotiate + 'emptySupportedNegotiations' => 'You must provide an array of supported values to all Negotiations.', + + // RedirectResponse + 'invalidRoute' => '{0, string} is not a valid route.', + + // Response + 'missingResponseStatus' => 'HTTP Response is missing a status code', + 'invalidStatusCode' => '{0, string} is not a valid HTTP return status code', + 'unknownStatusCode' => 'Unknown HTTP status code provided with no message: {0}', + + // URI + 'cannotParseURI' => 'Unable to parse URI: {0}', + 'segmentOutOfRange' => 'Request URI segment is our of range: {0}', + 'invalidPort' => 'Ports must be between 0 and 65535. Given: {0}', + 'malformedQueryString' => 'Query strings may not include URI fragments.', + + // Page Not Found + 'pageNotFound' => 'Page Not Found', + 'emptyController' => 'No Controller specified.', + 'controllerNotFound' => 'Controller or its method is not found: {0}::{1}', + 'methodNotFound' => 'Controller method is not found: {0}', + + // CSRF + 'disallowedAction' => 'The action you requested is not allowed.', ]; diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index 05e77c98db..a31b0bca78 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -1,5 +1,18 @@ 'You must specify a source image in your preferences.', 'gdRequired' => 'The GD image library is required to use this feature.', @@ -9,16 +22,14 @@ return [ 'pngNotSupported' => 'PNG images are not supported.', 'unsupportedImagecreate' => 'Your server does not support the GD function required to process this type of image.', 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', - 'copyError' => 'An error was encountered while attempting to replace the file. Please make sure your file directory is writable.', 'rotateUnsupported' => 'Image rotation does not appear to be supported by your server.', - 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences.', + 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. {0, string)', 'imageProcessFailed' => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.', 'rotationAngleRequired' => 'An angle of rotation is required to rotate the image.', 'invalidPath' => 'The path to the image is not correct.', 'copyFailed' => 'The image copy routine failed.', 'missingFont' => 'Unable to find a font to use.', 'saveFailed' => 'Unable to save the image. Please make sure the image and file directory are writable.', - 'invalidDirection' => 'Flip direction can be only `vertical` or `horizontal`.', + 'invalidDirection' => 'Flip direction can be only `vertical` or `horizontal`. Given: {0}', 'exifNotSupported' => 'Reading EXIF data is not supported by this PHP installation.', - 'pageNotFound' => 'Page Not Found', ]; diff --git a/system/Language/en/Language.php b/system/Language/en/Language.php index a20e1ce033..eca9718f8f 100644 --- a/system/Language/en/Language.php +++ b/system/Language/en/Language.php @@ -1,5 +1,18 @@ 'Get line must be a string or array of strings.' ]; diff --git a/system/Language/en/Log.php b/system/Language/en/Log.php new file mode 100644 index 0000000000..bd35bb914a --- /dev/null +++ b/system/Language/en/Log.php @@ -0,0 +1,18 @@ + '{0} is an invalid log level.', +]; diff --git a/system/Language/en/Migrations.php b/system/Language/en/Migrations.php index e6006289b9..98e52cb39e 100644 --- a/system/Language/en/Migrations.php +++ b/system/Language/en/Migrations.php @@ -1,78 +1,54 @@ 'Migrations table must be set.', - 'migInvalidType' => 'An invalid migration numbering type was specified: ', - 'migDisabled' => 'Migrations have been loaded but are disabled or setup incorrectly.', - 'migNotFound' => 'Migration file not found: ', - 'migEmpty' => 'No Migration files found', - 'migGap' => 'There is a gap in the migration sequence near version number: ', - 'migClassNotFound' => 'The migration class "%s" could not be found.', - 'migMissingMethod' => 'The migration class is missing an "%s" method.', - 'migMultiple' => 'There are multiple migrations with the same version number: ', + 'missingTable' => 'Migrations table must be set.', + 'invalidType' => 'An invalid migration numbering type was specified: {0}', + 'disabled' => 'Migrations have been loaded but are disabled or setup incorrectly.', + 'notFound' => 'Migration file not found: ', + 'empty' => 'No Migration files found', + 'gap' => 'There is a gap in the migration sequence near version number: ', + 'classNotFound' => 'The migration class "%s" could not be found.', + 'missingMethod' => 'The migration class is missing an "%s" method.', // Migration Command - 'migHelpLatest' => "\t\tMigrates database to latest available migration.", - 'migHelpCurrent' => "\t\tMigrates database to version set as 'current' in configuration.", - 'migHelpVersion' => "\tMigrates database to version {v}.", - 'migHelpRollback' => "\tRuns all migrations 'down' to version 0.", - 'migHelpRefresh' => "\t\tUninstalls and re-runs all migrations to freshen database.", - 'migHelpSeed' => "\tRuns the seeder named [name].", - 'migCreate' => "\tCreates a new migration named [name]", - 'migNameMigration' => "Name the migration file", - 'migBadCreateName' => 'You must provide a migration file name.', - 'migWriteError' => 'Error trying to create file.', + 'migHelpLatest' => "\t\tMigrates database to latest available migration.", + 'migHelpCurrent' => "\t\tMigrates database to version set as 'current' in configuration.", + 'migHelpVersion' => "\tMigrates database to version {v}.", + 'migHelpRollback' => "\tRuns all migrations 'down' to version 0.", + 'migHelpRefresh' => "\t\tUninstalls and re-runs all migrations to freshen database.", + 'migHelpSeed' => "\tRuns the seeder named [name].", + 'migCreate' => "\tCreates a new migration named [name]", + 'nameMigration' => "Name the migration file", + 'badCreateName' => 'You must provide a migration file name.', + 'writeError' => 'Error trying to create file.', - 'migToLatest' => 'Migrating to latest version...', + 'toLatest' => 'Migrating to latest version...', 'migInvalidVersion' => 'Invalid version number provided.', - 'migToVersionPH' => 'Migrating to version %s...', - 'migToVersion' => 'Migrating to current version...', - 'migRollingBack' => "Rolling back all migrations...", - 'migNoneFound' => 'No migrations were found.', - 'migOn' => 'Migrated On: ', + 'toVersionPH' => 'Migrating to version %s...', + 'toVersion' => 'Migrating to current version...', + 'rollingBack' => "Rolling back all migrations...", + 'noneFound' => 'No migrations were found.', + 'on' => 'Migrated On: ', 'migSeeder' => 'Seeder name', 'migMissingSeeder' => 'You must provide a seeder name.', - 'migHistoryFor' => 'Migration history For ', - 'migRemoved' => 'Rolling back: ', - 'migAdded' => 'Running: ', + 'historyFor' => 'Migration history For ', + 'removed' => 'Rolling back: ', + 'added' => 'Running: ', - 'version' => 'Version', - 'filename' => 'Filename', + 'version' => 'Version', + 'filename' => 'Filename', ]; diff --git a/system/Language/en/Number.php b/system/Language/en/Number.php index 98a2c0e94a..418a470cb3 100644 --- a/system/Language/en/Number.php +++ b/system/Language/en/Number.php @@ -1,50 +1,30 @@ 'TB', 'gigabyteAbbr' => 'GB', 'megabyteAbbr' => 'MB', 'kilobyteAbbr' => 'KB', - 'bytes' => 'Bytes', + 'bytes' => 'Bytes', + // don't forget the space in front of these! - 'thousand' => ' thousand', - 'million' => ' million', - 'billion' => ' billion', - 'trillion' => ' trillion', - 'quadrillion' => ' quadrillion', + 'thousand' => ' thousand', + 'million' => ' million', + 'billion' => ' billion', + 'trillion' => ' trillion', + 'quadrillion' => ' quadrillion', ]; diff --git a/system/Language/en/Pager.php b/system/Language/en/Pager.php index ac09920c8a..340149b4e7 100644 --- a/system/Language/en/Pager.php +++ b/system/Language/en/Pager.php @@ -1,31 +1,7 @@ 'Page navigation', - 'first' => 'First', - 'previous' => 'Previous', - 'next' => 'Next', - 'last' => 'Last', - 'older' => 'Older', - 'newer' => 'Newer', + 'pageNavigation' => 'Page navigation', + 'first' => 'First', + 'previous' => 'Previous', + 'next' => 'Next', + 'last' => 'Last', + 'older' => 'Older', + 'newer' => 'Newer', + 'invalidTemplate' => '{0} is not a valid Pager template.', + 'invalidPaginationGroup' => '{0} is not a valid Pagination group.', ]; diff --git a/system/Language/en/Router.php b/system/Language/en/Router.php new file mode 100644 index 0000000000..969403197b --- /dev/null +++ b/system/Language/en/Router.php @@ -0,0 +1,19 @@ + 'A parameter does not match the expected type.', + 'missingDefaultRoute' => 'Unable to determine what should be displayed. A default route has not been specified in the routing file.', +]; diff --git a/system/Language/en/Session.php b/system/Language/en/Session.php new file mode 100644 index 0000000000..06beb720e5 --- /dev/null +++ b/system/Language/en/Session.php @@ -0,0 +1,22 @@ + '`sessionSavePath` must have the table name for the Database Session Handler to work.', + 'invalidSavePath' => "Session: Configured save path '{0}' is not a directory, doesn't exist or cannot be created.", + 'writeProtectedSavePath' => "Session: Configured save path '{0}' is not writable by the PHP process.", + 'emptySavePath' => 'Session: No save path configured.', + 'invalidSavePathFormat' => 'Session: Invalid Redis save path format: {0}', +]; diff --git a/system/Language/en/Time.php b/system/Language/en/Time.php index 12fa336e3e..d27ca3ef09 100644 --- a/system/Language/en/Time.php +++ b/system/Language/en/Time.php @@ -1,21 +1,34 @@ 'Months must be between 0 and 12.', - 'invalidDay' => 'Days must be between 0 and 31.', - 'invalidHours' => 'Hours must be between 0 and 23.', - 'invalidMinutes' => 'Minutes must be between 0 and 59.', - 'invalidSeconds' => 'Seconds must be between 0 and 59.', - 'years' => '{0, plural, =1{# year} other{# years}}', - 'months' => '{0, plural, =1{# month} other{# months}}', - 'weeks' => '{0, plural, =1{# week} other{# weeks}}', - 'days' => '{0, plural, =1{# day} other{# days}}', - 'hours' => '{0, plural, =1{# hour} other{# hours}}', - 'minutes' => '{0, plural, =1{# minute} other{# minutes}}', - 'seconds' => '{0, plural, =1{# second} other{# seconds}}', - 'ago' => '{0} ago', - 'inFuture' => 'in {0}', - 'yesterday' => 'Yesterday', - 'tomorrow' => 'Tomorrow', - 'now' => 'Just now', + 'invalidMonth' => 'Months must be between 0 and 12. Given: {0}', + 'invalidDay' => 'Days must be between 0 and 31. Given: {0}', + 'invalidHours' => 'Hours must be between 0 and 23. Given: {0}', + 'invalidMinutes' => 'Minutes must be between 0 and 59. Given: {0}', + 'invalidSeconds' => 'Seconds must be between 0 and 59. Given: {0}', + 'years' => '{0, plural, =1{# year} other{# years}}', + 'months' => '{0, plural, =1{# month} other{# months}}', + 'weeks' => '{0, plural, =1{# week} other{# weeks}}', + 'days' => '{0, plural, =1{# day} other{# days}}', + 'hours' => '{0, plural, =1{# hour} other{# hours}}', + 'minutes' => '{0, plural, =1{# minute} other{# minutes}}', + 'seconds' => '{0, plural, =1{# second} other{# seconds}}', + 'ago' => '{0} ago', + 'inFuture' => 'in {0}', + 'yesterday' => 'Yesterday', + 'tomorrow' => 'Tomorrow', + 'now' => 'Just now', ]; diff --git a/system/Language/en/Validation.php b/system/Language/en/Validation.php index 8059e58864..74aaf5c449 100644 --- a/system/Language/en/Validation.php +++ b/system/Language/en/Validation.php @@ -1,31 +1,7 @@ 'No rulesets specified in Validation configuration.', - 'ruleNotFound' => '{rule} is not a valid rule.', - 'groupNotFound' => '%s is not a validation rules group.', - 'groupNotArray' => '%s rule group must be an array.', + 'ruleNotFound' => '{0} is not a valid rule.', + 'groupNotFound' => '{0} is not a validation rules group.', + 'groupNotArray' => '{0} rule group must be an array.', + 'invalidTemplate' => '{0} is not a valid Validation template.', // Rule Messages 'alpha' => 'The {field} field may only contain alphabetical characters.', 'alpha_dash' => 'The {field} field may only contain alpha-numeric characters, underscores, and dashes.', 'alpha_numeric' => 'The {field} field may only contain alpha-numeric characters.', - 'alpha_numeric_space' => 'The {field} field may only contain alpha-numeric characters and spaces.', + 'alpha_numeric_space' => 'The {field} field may only contain alpha-numeric characters and spaces.', 'alpha_space' => 'The {field} field may only contain alphabetical characters and spaces.', 'decimal' => 'The {field} field must contain a decimal number.', 'differs' => 'The {field} field must differ from the {param} field.', @@ -68,7 +47,7 @@ return [ 'regex_match' => 'The {field} field is not in the correct format.', 'required' => 'The {field} field is required.', 'required_with' => 'The {field} field is required when {param} is present.', - 'required_without' => 'The {field} field is required when {param} in not present.', + 'required_without' => 'The {field} field is required when {param} is not present.', 'timezone' => 'The {field} field must be a valid timezone.', 'valid_base64' => 'The {field} field must be a valid base64 string.', 'valid_email' => 'The {field} field must contain a valid email address.', @@ -87,5 +66,4 @@ return [ 'mime_in' => '{field} does not have a valid mime type.', 'ext_in' => '{field} does not have a valid file extension.', 'max_dims' => '{field} is either not an image, or it is too wide or tall.', - '', ]; diff --git a/system/Language/en/View.php b/system/Language/en/View.php new file mode 100644 index 0000000000..4000850cef --- /dev/null +++ b/system/Language/en/View.php @@ -0,0 +1,22 @@ + '{class}::{method} is not a valid method."', + 'missingCellParameters' => '{class}::{method} has no params.', + 'invalidCellParameter' => '{0} is not a valid param name.', + 'noCellClass' => 'No view cell class provided.', + 'invalidCellClass' => 'Unable to locate view cell class: {0}.', + 'tagSyntaxError' => 'You have a syntax error in your Parser tags: {0}', +]; diff --git a/system/Log/Exceptions/LogException.php b/system/Log/Exceptions/LogException.php new file mode 100644 index 0000000000..1768efd76e --- /dev/null +++ b/system/Log/Exceptions/LogException.php @@ -0,0 +1,14 @@ +levels)) + if (array_key_exists($level, $this->levels)) { $type = $this->levels[$level]; } diff --git a/system/Log/Handlers/FileHandler.php b/system/Log/Handlers/FileHandler.php index dc2b14a8a0..abbf05aabd 100644 --- a/system/Log/Handlers/FileHandler.php +++ b/system/Log/Handlers/FileHandler.php @@ -105,7 +105,7 @@ class FileHandler extends BaseHandler implements HandlerInterface // Only add protection to php files if ($this->fileExtension === 'php') { - $msg .= "\n\n"; + $msg .= "\n\n"; } } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 3d7688d4e6..4de39d1bc8 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -36,6 +36,7 @@ * @filesource */ use Psr\Log\LoggerInterface; +use CodeIgniter\Log\Exceptions\LogException; /** * The CodeIgntier Logger @@ -143,7 +144,7 @@ class Logger implements LoggerInterface /** * Constructor. - * + * * @param type $config * @param bool $debug * @throws \RuntimeException @@ -170,7 +171,7 @@ class Logger implements LoggerInterface if ( ! is_array($config->handlers) || empty($config->handlers)) { - throw new \RuntimeException('LoggerConfig must provide at least one Handler.'); + throw LogException::forNoHandlers('LoggerConfig'); } // Save the handler configuration for later. @@ -336,7 +337,7 @@ class Logger implements LoggerInterface // Is the level a valid level? if ( ! array_key_exists($level, $this->logLevels)) { - throw new \InvalidArgumentException($level . ' is an invalid log level.'); + throw LogException::forInvalidLogLevel($level); } // Does the app want to log this right now? diff --git a/system/Model.php b/system/Model.php index 29f82f8f7a..4669891ebe 100644 --- a/system/Model.php +++ b/system/Model.php @@ -35,6 +35,7 @@ * @since Version 3.0.0 * @filesource */ +use CodeIgniter\Database\Exceptions\DatabaseException; use Config\App; use Config\Database; use CodeIgniter\I18n\Time; @@ -44,7 +45,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Validation\ValidationInterface; -use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\Exceptions\DataException; /** * Class Model @@ -586,7 +587,7 @@ class Model if (empty($data)) { - throw new \InvalidArgumentException('No data to insert.'); + throw DataException::forEmptyDataset('insert'); } // Must use the set() method to ensure objects get converted to arrays @@ -662,7 +663,7 @@ class Model if (empty($data)) { - throw new \InvalidArgumentException('No data to update.'); + throw DataException::forEmptyDataset('update'); } // Must use the set() method to ensure objects get converted to arrays @@ -728,14 +729,14 @@ class Model * @param bool $purge Allows overriding the soft deletes setting. * * @return mixed - * @throws \CodeIgniter\Database\Exceptions\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DataException */ public function deleteWhere($key, $value = null, $purge = false) { // Don't let them shoot themselves in the foot... if (empty($key)) { - throw new DatabaseException('You must provided a valid key to deleteWhere.'); + throw DataException::forInvalidArgument('key'); } $this->trigger('beforeDelete', ['key' => $key, 'value' => $value, 'purge' => $purge]); @@ -866,7 +867,7 @@ class Model * @param int $size * @param \Closure $userFunc * - * @throws \CodeIgniter\Database\Exceptions\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DataException */ public function chunk($size = 100, \Closure $userFunc) { @@ -883,7 +884,7 @@ class Model if ($rows === false) { - throw new DatabaseException('Unable to get results from the query.'); + throw DataException::forEmptyDataset('chunk'); } $rows = $rows->getResult(); @@ -993,7 +994,7 @@ class Model * @param array $data * * @return array - * @throws \CodeIgniter\Database\Exceptions\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DataException */ protected function doProtectFields($data) { @@ -1004,7 +1005,7 @@ class Model if (empty($this->allowedFields)) { - throw new DatabaseException('No Allowed fields specified for model: ' . get_class($this)); + throw DataException::forInvalidAllowedFields(get_class($this)); } foreach ($data as $key => $val) @@ -1150,7 +1151,11 @@ class Model } else { - $this->validation->setRules($this->validationRules, $this->validationMessages); + // Replace any placeholders (i.e. {id}) in the rules with + // the value found in $data, if exists. + $rules = $this->fillPlaceholders($this->validationRules, $data); + + $this->validation->setRules($rules, $this->validationMessages); $valid = $this->validation->run($data); } @@ -1159,6 +1164,57 @@ class Model //-------------------------------------------------------------------- + /** + * Replace any placeholders within the rules with the values that + * match the 'key' of any properties being set. For example, if + * we had the following $data array: + * + * [ 'id' => 13 ] + * + * and the following rule: + * + * 'required|is_unique[users,email,id,{id}]' + * + * The value of {id} would be replaced with the actual id in the form data: + * + * 'required|is_unique[users,email,id,13]' + * + * @param array $rules + * @param array $data + * + * @return array + */ + protected function fillPlaceholders(array $rules, array $data) + { + $replacements = []; + + foreach ($data as $key => $value) + { + $replacements["{{$key}}"] = $value; + } + + if (! empty($replacements)) + { + foreach ($rules as &$rule) + { + if (is_array($rule)) + { + foreach ($rule as &$row) + { + $row = strtr($row, $replacements); + } + continue; + } + + $rule = strtr($rule, $replacements); + } + } + + return $rules; + } + + //-------------------------------------------------------------------- + /** * Returns the model's defined validation rules so that they * can be used elsewhere, if needed. @@ -1213,6 +1269,7 @@ class Model * @param array $data * * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DataException */ protected function trigger(string $event, array $data) { @@ -1226,7 +1283,7 @@ class Model { if ( ! method_exists($this, $callback)) { - throw new \BadMethodCallException(lang('Database.invalidEvent', [$callback])); + throw DataException::forInvalidMethodTriggered($callback); } $data = $this->{$callback}($data); @@ -1279,11 +1336,11 @@ class Model if (method_exists($this->db, $name)) { - $result = call_user_func_array([$this->db, $name], $params); + $result = $this->db->$name(...$params); } - elseif (method_exists($this->builder(), $name)) + elseif (method_exists($builder = $this->builder(), $name)) { - $result = call_user_func_array([$this->builder(), $name], $params); + $result = $builder->$name(...$params); } // Don't return the builder object unless specifically requested diff --git a/system/Pager/Exceptions/PagerException.php b/system/Pager/Exceptions/PagerException.php new file mode 100644 index 0000000000..239d7939e9 --- /dev/null +++ b/system/Pager/Exceptions/PagerException.php @@ -0,0 +1,17 @@ +config->templates)) { - throw new \InvalidArgumentException($template . ' is not a valid Pager template.'); + throw PagerException::forInvalidTemplate($template); } return $this->view->setVar('pager', $pager) @@ -295,11 +303,11 @@ class Pager implements PagerInterface /** * Returns the URI for a specific page for the specified group. * - * @param int $page - * @param string $group - * @param bool $returnObject + * @param int|null $page + * @param string $group + * @param bool $returnObject * - * @return string + * @return string|\CodeIgniter\HTTP\URI */ public function getPageURI(int $page = null, string $group = 'default', $returnObject = false) { @@ -307,7 +315,18 @@ class Pager implements PagerInterface $uri = $this->groups[$group]['uri']; - $uri->addQuery('page', $page); + if ($this->only) + { + $query = array_intersect_key($_GET, array_flip($this->only)); + + $query['page'] = $page; + + $uri->setQueryArray($query); + } + else + { + $uri->addQuery('page', $page); + } return $returnObject === true ? $uri : (string) $uri; } @@ -406,7 +425,7 @@ class Pager implements PagerInterface { if ( ! array_key_exists($group, $this->groups)) { - throw new \InvalidArgumentException($group . ' is not a valid Pagination group.'); + throw PagerException::forInvalidPaginationGroup($group); } $newGroup = $this->groups[$group]; @@ -419,6 +438,22 @@ class Pager implements PagerInterface //-------------------------------------------------------------------- + /** + * Sets only allowed queries on pagination links. + * + * @param array $queries + * + * @return Pager + */ + public function only(array $queries):Pager + { + $this->only = $queries; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Ensures that an array exists for the group specified. * diff --git a/system/Pager/PagerRenderer.php b/system/Pager/PagerRenderer.php index 26d7377aff..66e60d0e55 100644 --- a/system/Pager/PagerRenderer.php +++ b/system/Pager/PagerRenderer.php @@ -74,11 +74,11 @@ class PagerRenderer * side of the current page. Adjusts the first and last counts * to reflect it. * - * @param int $count + * @param int|null $count * * @return PagerRenderer */ - public function setSurroundCount(int $count) + public function setSurroundCount(int $count = null) { $this->updatePages($count); @@ -226,7 +226,7 @@ class PagerRenderer * which is the number of links surrounding the active page * to show. * - * @param int|null $count + * @param int|null $count The new "surroundCount" */ protected function updatePages(int $count = null) { diff --git a/system/Pager/Views/default_full.php b/system/Pager/Views/default_full.php index 5f57afcfe8..4eb10aa792 100644 --- a/system/Pager/Views/default_full.php +++ b/system/Pager/Views/default_full.php @@ -3,16 +3,16 @@