diff --git a/.travis.yml b/.travis.yml
index 8ef13ed2de..cdda6078b5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
language: php
php:
- - 7.1
- 7.2
- 7.3
- nightly
@@ -9,7 +8,6 @@ php:
matrix:
fast_finish: true
allow_failures:
- - php: 7.3
- php: nightly
global:
diff --git a/README.md b/README.md
index c900a5205b..d450a1a9df 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
## What is CodeIgniter?
+
CodeIgniter is a PHP full-stack web framework that is light, fast, flexible, and secure.
More information can be found at the [official site](http://codeigniter.com).
@@ -35,6 +36,7 @@ framework are exposed.
The user guide updating and deployment is a bit awkward at the moment, but we are working on it!
## Repository Management
+
We use Github issues to track **BUGS** and to track approved **DEVELOPMENT** work packages.
We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss
FEATURE REQUESTS.
@@ -57,6 +59,7 @@ Remember that some components that were part of CodeIgniter 3 are being moved
to optional packages, with their own repository.
## Contributing
+
We **are** accepting contributions from the community, specifically those identified as part of phase 2.
We will try to manage the process somewhat, by adding a "Help wanted" label to those that we are
@@ -68,7 +71,9 @@ We are not looking for out-of-scope contributions, only those that would be cons
Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing.md) section in the user guide
## Server Requirements
-PHP version 7.1 or higher is required, with the following extensions installed:
+
+PHP version 7.2 or higher is required, with the following extensions installed:
+
- [intl](http://php.net/manual/en/intl.requirements.php)
- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library
@@ -81,4 +86,5 @@ Additionally, make sure that the following extensions are enabled in your PHP:
- xml (enabled by default - don't turn it off)
## Running CodeIgniter Tests
+
Information on running CodeIgniter test suite can be found in the [README.md](tests/README.md) file in the tests directory.
diff --git a/admin/README.md b/admin/README.md
index 242793acac..ffa514bfce 100644
--- a/admin/README.md
+++ b/admin/README.md
@@ -1,8 +1,8 @@
-#CodeIgniter 4 Admin
+# CodeIgniter 4 Admin
This folder contains tools or docs useful for project maintainers.
-##Repositories inside https://github.com/codeigniter4
+## Repositories inside https://github.com/codeigniter4
- **CodeIgniter4** is the main development repository.
It supports issues and pull requests, and has a rule to enforce GPG-signed commits.
@@ -35,7 +35,7 @@ This folder contains tools or docs useful for project maintainers.
It is community-maintained, and accepts issues and pull requests.
It could be downloaded, forked or composer-installed.
-##Contributor Scripts
+## Contributor Scripts
- **setup.sh** installs a git pre-commit hook into a contributor's
local clone of their fork of the `CodeIgniter4` repository.
@@ -43,7 +43,7 @@ This folder contains tools or docs useful for project maintainers.
to be added as part of a git commit, ensuring that they conform to the
framework coding style standards, and automatically fixing what can be.
-##Maintainer Scripts
+## Maintainer Scripts
- **release-config** holds variables used for the maintainer & release building
- **docbot** re-builds the user guide from the RST source for it,
@@ -51,7 +51,7 @@ This folder contains tools or docs useful for project maintainers.
repository (if the user running it has maintainer rights on that repo).
See the [writeup](./docbot.md).
-##Release Building Scripts
+## Release Building Scripts
The release workflow is detailed in its own writeup; these are the main
scripts used by the release manager:
@@ -79,7 +79,7 @@ scripts used by the release manager:
Remember to be polite when running it.
-##Other Stuff
+## Other Stuff
- **release-notes.bb** is a boilerplate for forum announcements of a new release.
It is marked up using [BBcode](https://en.wikipedia.org/wiki/BBCode).
diff --git a/admin/framework/README.md b/admin/framework/README.md
index 559185645e..8ab1360385 100644
--- a/admin/framework/README.md
+++ b/admin/framework/README.md
@@ -1,6 +1,7 @@
# CodeIgniter 4 Framework
## What is CodeIgniter?
+
CodeIgniter is a PHP full-stack web framework that is light, fast, flexible, and secure.
More information can be found at the [official site](http://codeigniter.com).
@@ -29,6 +30,7 @@ framework are exposed.
The user guide updating and deployment is a bit awkward at the moment, but we are working on it!
## Repository Management
+
We use Github issues to track **BUGS** and to track approved **DEVELOPMENT** work packages.
We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss
FEATURE REQUESTS.
@@ -51,12 +53,14 @@ Remember that some components that were part of CodeIgniter 3 are being moved
to optional packages, with their own repository.
## Contributing
+
We welcome contributions from the community.
Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing.md) section in the development repository.
## Server Requirements
-PHP version 7.1 or higher is required, with the following extensions installed:
+
+PHP version 7.2 or higher is required, with the following extensions installed:
- [intl](http://php.net/manual/en/intl.requirements.php)
- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library
diff --git a/admin/framework/composer.json b/admin/framework/composer.json
index 672c5f183c..012f66e66a 100644
--- a/admin/framework/composer.json
+++ b/admin/framework/composer.json
@@ -5,7 +5,7 @@
"homepage": "https://codeigniter.com",
"license": "MIT",
"require": {
- "php": ">=7.1",
+ "php": ">=7.2",
"ext-curl": "*",
"ext-intl": "*",
"kint-php/kint": "^2.1",
@@ -14,7 +14,7 @@
},
"require-dev": {
"codeigniter4/codeigniter4-standard": "^1.0",
- "mikey179/vfsStream": "1.6.*",
+ "mikey179/vfsstream": "1.6.*",
"phpunit/phpunit": "^7.0",
"squizlabs/php_codesniffer": "^3.3"
},
diff --git a/admin/release-appstarter b/admin/release-appstarter
index 8591856101..87281c1536 100644
--- a/admin/release-appstarter
+++ b/admin/release-appstarter
@@ -12,7 +12,7 @@ git checkout $branch
echo -e "${BOLD}Build the framework distributable${NORMAL}"
echo -e "${BOLD}Copy the main files/folders...${NORMAL}"
-releasable='app public writable README.md contributing.md env license.txt spark'
+releasable='app public writable README.md contributing.md env license.txt spark tests/_support'
for fff in $releasable ; do
if [ -d "$fff" ] ; then
rm -rf $fff
@@ -23,10 +23,6 @@ done
echo -e "${BOLD}Override as needed...${NORMAL}"
cp -rf ${CI_DIR}/admin/starter/* .
-############# this can only happen after composer create-project/update
-#echo -e "${BOLD}Fix paths...${NORMAL}"
-#sed -i "/public $systemDirectory = 'system';/s/'system'/'vendor/codeigniter4/framework/system'/" app/Config/Paths.php
-
#---------------------------------------------------
# And finally, get ready for merging
echo -e "${BOLD}Assemble the pieces...${NORMAL}"
diff --git a/admin/release-framework b/admin/release-framework
index 50ec5f2064..110da980ea 100644
--- a/admin/release-framework
+++ b/admin/release-framework
@@ -12,7 +12,7 @@ git checkout $branch
echo -e "${BOLD}Build the framework distributable${NORMAL}"
echo -e "${BOLD}Copy the main files/folders...${NORMAL}"
-releasable='app docs public system writable contributing.md env license.txt spark'
+releasable='app docs public system writable contributing.md env license.txt spark tests/_support'
for fff in $releasable ; do
if [ -d "$fff" ] ; then
rm -rf $fff
diff --git a/admin/starter/README.md b/admin/starter/README.md
index f71eacaf12..9d1726d52f 100644
--- a/admin/starter/README.md
+++ b/admin/starter/README.md
@@ -47,7 +47,7 @@ The user guide updating and deployment is a bit awkward at the moment, but we ar
## Server Requirements
-PHP version 7.1 or higher is required, with the following extensions installed:
+PHP version 7.2 or higher is required, with the following extensions installed:
- [intl](http://php.net/manual/en/intl.requirements.php)
- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library
diff --git a/admin/starter/app/Config/Paths.php b/admin/starter/app/Config/Paths.php
new file mode 100644
index 0000000000..6251124c64
--- /dev/null
+++ b/admin/starter/app/Config/Paths.php
@@ -0,0 +1,77 @@
+=7.1",
+ "php": ">=7.2",
"codeigniter4/framework": "^4@alpha"
},
"require-dev": {
- "mikey179/vfsStream": "1.6.*",
+ "mikey179/vfsstream": "1.6.*",
"phpunit/phpunit": "^7.0"
},
"scripts": {
diff --git a/admin/starter/phpunit.xml.dist b/admin/starter/phpunit.xml.dist
new file mode 100644
index 0000000000..73d51ffc8d
--- /dev/null
+++ b/admin/starter/phpunit.xml.dist
@@ -0,0 +1,28 @@
+
+
+
+
+ ./tests
+ ./tests/system
+
+
+
+
+
+ ./system
+
+ ./system
+
+
+
+
+
diff --git a/admin/userguide/README.md b/admin/userguide/README.md
index fc4004e04b..1560abe7f7 100644
--- a/admin/userguide/README.md
+++ b/admin/userguide/README.md
@@ -1,6 +1,7 @@
# CodeIgniter 4 User Guide
## What is CodeIgniter?
+
CodeIgniter is a PHP full-stack web framework that is light, fast, flexible, and secure.
More information can be found at the [official site](http://codeigniter.com).
diff --git a/admin/userguide/composer.json b/admin/userguide/composer.json
index 6eb7612d8a..62ed362a81 100644
--- a/admin/userguide/composer.json
+++ b/admin/userguide/composer.json
@@ -5,7 +5,7 @@
"homepage": "https://codeigniter.com",
"license": "MIT",
"require": {
- "php": ">=7.1",
+ "php": ">=7.2",
"codeigniter4/framework": "^4"
},
"support": {
diff --git a/app/Config/App.php b/app/Config/App.php
index 15b20003c8..4627dfa0bc 100644
--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -21,7 +21,7 @@ class App extends BaseConfig
| environments.
|
*/
- public $baseURL = '';
+ public $baseURL = 'http://localhost:8080';
/*
|--------------------------------------------------------------------------
diff --git a/app/Config/Cache.php b/app/Config/Cache.php
index 15038399a4..015bd6fd68 100644
--- a/app/Config/Cache.php
+++ b/app/Config/Cache.php
@@ -97,6 +97,7 @@ class Cache extends BaseConfig
'password' => null,
'port' => 6379,
'timeout' => 0,
+ 'database' => 0,
];
/*
diff --git a/app/Config/Filters.php b/app/Config/Filters.php
index a6c8a2143a..11359e7c9d 100644
--- a/app/Config/Filters.php
+++ b/app/Config/Filters.php
@@ -7,9 +7,9 @@ class Filters extends BaseConfig
// Makes reading things below nicer,
// and simpler to change out script that's used.
public $aliases = [
- 'csrf' => \App\Filters\CSRF::class,
- 'toolbar' => \App\Filters\DebugToolbar::class,
- 'honeypot' => \App\Filters\Honeypot::class,
+ 'csrf' => \CodeIgniter\Filters\CSRF::class,
+ 'toolbar' => \CodeIgniter\Filters\DebugToolbar::class,
+ 'honeypot' => \CodeIgniter\Filters\Honeypot::class,
];
// Always applied before every request
diff --git a/app/Config/Modules.php b/app/Config/Modules.php
index 2dcd22be3c..28bbc7d930 100644
--- a/app/Config/Modules.php
+++ b/app/Config/Modules.php
@@ -14,6 +14,16 @@ class Modules
*/
public $enabled = true;
+ /*
+ |--------------------------------------------------------------------------
+ | Auto-Discovery Within Composer Packages Enabled?
+ |--------------------------------------------------------------------------
+ |
+ | If true, then auto-discovery will happen across all namespaces loaded
+ | by Composer, as well as the namespaces configured locally.
+ */
+ public $discoverInComposer = true;
+
/*
|--------------------------------------------------------------------------
| Auto-discover Rules
diff --git a/app/Config/Paths.php b/app/Config/Paths.php
index 2fd1995caa..5ef168c713 100644
--- a/app/Config/Paths.php
+++ b/app/Config/Paths.php
@@ -35,7 +35,7 @@ class Paths
*
* NO TRAILING SLASH!
*/
- public $appDirectory = __DIR__ . '/../../app';
+ public $appDirectory = __DIR__ . '/..';
/*
* ---------------------------------------------------------------
@@ -73,5 +73,5 @@ class Paths
* default this is in `app/Views`. This value
* is used when no value is provided to `Services::renderer()`.
*/
- public $viewDirectory = __DIR__ . '/../../app/Views';
+ public $viewDirectory = __DIR__ . '/../Views';
}
diff --git a/app/Filters/CSRF.php b/app/Filters/CSRF.php
deleted file mode 100644
index a5355556ca..0000000000
--- a/app/Filters/CSRF.php
+++ /dev/null
@@ -1,64 +0,0 @@
-isCLI())
- {
- return;
- }
-
- $security = Services::security();
-
- try
- {
- $security->CSRFVerify($request);
- }
- catch (SecurityException $e)
- {
- if (config('App')->CSRFRedirect && ! $request->isAJAX())
- {
- return redirect()->back()->with('error', $e->getMessage());
- }
-
- throw $e;
- }
- }
-
- //--------------------------------------------------------------------
-
- /**
- * We don't have anything to do here.
- *
- * @param RequestInterface|\CodeIgniter\HTTP\IncomingRequest $request
- * @param ResponseInterface|\CodeIgniter\HTTP\Response $response
- *
- * @return mixed
- */
- public function after(RequestInterface $request, ResponseInterface $response)
- {
- }
-
- //--------------------------------------------------------------------
-}
diff --git a/app/Filters/DebugToolbar.php b/app/Filters/DebugToolbar.php
deleted file mode 100644
index 8616bc3c64..0000000000
--- a/app/Filters/DebugToolbar.php
+++ /dev/null
@@ -1,38 +0,0 @@
-prepare();
- }
-
- //--------------------------------------------------------------------
-}
diff --git a/app/Filters/Honeypot.php b/app/Filters/Honeypot.php
deleted file mode 100644
index a16a55c02a..0000000000
--- a/app/Filters/Honeypot.php
+++ /dev/null
@@ -1,44 +0,0 @@
-hasContent($request))
- {
- throw HoneypotException::isBot();
- }
- }
-
- /**
- * Attach a honypot to the current response.
- *
- * @param CodeIgniter\HTTP\RequestInterface $request
- * @param CodeIgniter\HTTP\ResponseInterface $response
- * @return mixed
- */
- public function after(RequestInterface $request, ResponseInterface $response)
- {
- $honeypot = Services::honeypot(new \Config\Honeypot());
- $honeypot->attachHoneypot($response);
- }
-
-}
diff --git a/app/Filters/Throttle.php b/app/Filters/Throttle.php
deleted file mode 100644
index b2659e549e..0000000000
--- a/app/Filters/Throttle.php
+++ /dev/null
@@ -1,46 +0,0 @@
-check($request->getIPAddress(), 60, MINUTE) === false)
- {
- return Services::response()->setStatusCode(429);
- }
- }
-
- //--------------------------------------------------------------------
-
- /**
- * We don't have anything to do here.
- *
- * @param RequestInterface|\CodeIgniter\HTTP\IncomingRequest $request
- * @param ResponseInterface|\CodeIgniter\HTTP\Response $response
- *
- * @return mixed
- */
- public function after(RequestInterface $request, ResponseInterface $response)
- {
- }
-
- //--------------------------------------------------------------------
-}
diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php
index 2939098ce2..ad6168e9a8 100644
--- a/app/Views/welcome_message.php
+++ b/app/Views/welcome_message.php
@@ -12,8 +12,9 @@
height: 200px;
width: 155px;
display: inline-block;
- opacity: 0.08;
+ opacity: 0.12;
position: absolute;
+ z-index: 0;
top: 2rem;
left: 50%;
margin-left: -73px;
@@ -32,6 +33,8 @@
margin-top: 145px;
margin-bottom: 0;
color: #222;
+ position: relative;
+ z-index: 1;
}
.wrap {
max-width: 1024px;
@@ -81,15 +84,11 @@
-
Welcome to CodeIgniter
-
-
version = CodeIgniter\CodeIgniter::CI_VERSION ?>
-
-
+
version = CodeIgniter\CodeIgniter::CI_VERSION ?>
+
The page you are looking at is being generated dynamically by CodeIgniter.
diff --git a/composer.json b/composer.json
index 4a75cdcc5e..e08b013ca7 100644
--- a/composer.json
+++ b/composer.json
@@ -5,7 +5,7 @@
"homepage": "https://codeigniter.com",
"license": "MIT",
"require": {
- "php": ">=7.1",
+ "php": ">=7.2",
"ext-curl": "*",
"ext-intl": "*",
"ext-json": "*",
@@ -15,7 +15,7 @@
},
"require-dev": {
"codeigniter4/codeigniter4-standard": "^1.0",
- "mikey179/vfsStream": "1.6.*",
+ "mikey179/vfsstream": "1.6.*",
"phpunit/phpunit": "^7.0",
"squizlabs/php_codesniffer": "^3.3"
},
diff --git a/contributing.md b/contributing.md
index 8e236f0cc3..78593e2533 100644
--- a/contributing.md
+++ b/contributing.md
@@ -35,7 +35,7 @@ If you change anything that requires a change to documentation then you will nee
### Compatibility
-CodeIgniter4 requires PHP 7.1.
+CodeIgniter4 requires PHP 7.2.
### Branching
diff --git a/contributing/README.rst b/contributing/README.rst
index 695b549258..a69bb748ce 100644
--- a/contributing/README.rst
+++ b/contributing/README.rst
@@ -54,7 +54,8 @@ If you've found a critical vulnerability, we'd be happy to credit you in our
Tips for a Good Issue Report
****************************
-Use a descriptive subject line (eg parser library chokes on commas) rather than a vague one (eg. your code broke).
+Use a descriptive subject line (eg parser library chokes on commas) rather than
+a vague one (eg. your code broke).
Address a single issue in a report.
@@ -64,12 +65,13 @@ Explain what you expected to happen, and what did happen.
Include error messages and stacktrace, if any.
Include short code segments if they help to explain.
-Use a pastebin or dropbox facility to include longer segments of code or screenshots - do not include them in the issue report itself.
+Use a pastebin or dropbox facility to include longer segments of code or
+screenshots - do not include them in the issue report itself.
This means setting a reasonable expiry for those, until the issue is resolved or closed.
If you know how to fix the issue, you can do so in your own fork & branch, and submit a pull request.
The issue report information above should be part of that.
If your issue report can describe the steps to reproduce the problem, that is great.
-If you can include a unit test that reproduces the problem, that is even better, as it gives whoever is fixing
-it a clearer target!
+If you can include a unit test that reproduces the problem, that is even better,
+as it gives whoever is fixing it a clearer target!
diff --git a/contributing/guidelines.rst b/contributing/guidelines.rst
index dabfa31ef8..2079d1b4c9 100644
--- a/contributing/guidelines.rst
+++ b/contributing/guidelines.rst
@@ -64,16 +64,17 @@ Change Log
The change-log, in the user guide root, needs to be kept up-to-date.
Not all changes will need an entry in it, but new classes, major or BC changes
-to existing classes, and bug fixes should.
+to existing classes should. Once we have a stable release, bug fixes would
+appear in the changelog too.
-See the `CodeIgniter 4 change log
-
`_
-for an example.
+The changelog is independently maintained by the framework release manager
+Make sure that your PR descriptions help us decide if the contribution should
+be highlighted in the next release after it has been merged.
PHP Compatibility
=================
-CodeIgniter4 requires PHP 7.1.
+CodeIgniter4 requires PHP 7.2.
Backwards Compatibility
=======================
@@ -89,7 +90,7 @@ with earlier versions of the framework.
Mergeability
============
-Your PRs need to be mergeable before they will be considered.
+Your PRs need to be mergeable and GPG-signed before they will be considered.
We suggest that you synchronize your repository's ``develop`` branch with
that in the main repository, and then your feature branch and
diff --git a/contributing/internals.rst b/contributing/internals.rst
index 687ac77ac3..2ef86d0b56 100644
--- a/contributing/internals.rst
+++ b/contributing/internals.rst
@@ -2,18 +2,19 @@
CodeIgniter Internals Overview
##############################
-This guide should help contributors understand how the core of the framework works, and what needs to be done
-when creating new functionality. Specifically, it details the information needed to create new packages for the
-core.
+This guide should help contributors understand how the core of the framework works,
+and what needs to be done when creating new functionality. Specifically, it
+details the information needed to create new packages for the core.
Dependencies
============
-All packages should be designed to be completely isolated from the rest of the packages. This will allow
-them to be used in projects outside of CodeIgniter. Basically, this means that all dependencies should be
-kept to a minimum. Any dependencies must be able to be passed into the constructor. If you do need to use one
-of the other core packages, you can create that in the constructor using the Services class, as long as you
-provide a way for dependencies to override that::
+All packages should be designed to be completely isolated from the rest of the
+packages, if possible. This will allow them to be used in projects outside of CodeIgniter.
+Basically, this means that any dependencies should be kept to a minimum.
+Any dependencies must be able to be passed into the constructor. If you do need to use one
+of the other core packages, you can create that in the constructor using the
+Services class, as long as you provide a way for dependencies to override that::
public function __construct(Foo $foo=null)
{
@@ -26,17 +27,18 @@ Type hinting
============
PHP7 provides the ability to `type hint `_
-method parameters and return types. Use it where possible. Return type hinting is not always practical, but do try to
-make it work.
+method parameters and return types. Use it where possible. Return type hinting
+is not always practical, but do try to make it work.
At this time, we are not using strict type hinting.
Abstractions
============
-The amount of abstraction required to implement a solution should be the minimal amount required. Every layer of
-abstraction brings additional levels of technical debt and unnecessary complexity. That said, don't be afraid to
-use it when it's needed and can help things.
+The amount of abstraction required to implement a solution should be the minimal
+amount required. Every layer of abstraction brings additional levels of technical
+debt and unnecessary complexity. That said, don't be afraid to use it when it's
+needed and can help things.
* Don't create a new container class when an array will do just fine.
* Start simple, refactor as necessary to achieve clean separation of code, but don't overdo it.
@@ -44,73 +46,93 @@ use it when it's needed and can help things.
Testing
=======
-Any new packages submitted to the framework must be accompanied by unit tests. The target is 80%+ coverage of all
-classes within the package.
+Any new packages submitted to the framework must be accompanied by unit tests.
+The target is 80%+ code coverage of all classes within the package.
* Test only public methods, not protected and private unless the method really needs it due to complexity.
* Don't just test that the method works, but test for all fail states, thrown exceptions, and other pathways through your code.
+You should be aware of the extra assertions that we have made, provisions for
+accessing private properties for testing, and mock services.
+We have also made a **CITestStreamFilter** to capture test output.
+Do check out similar tests in ``tests/system/``, and read the "Testing" section
+in the user guide, before you dig in to your own.
+
+Some testing needs to be done in a separate process, in order to setup the
+PHP globals to mimic test situations properly. See
+``tests/system/HTTP/ResponseSendTest`` for an example of this.
+
Namespaces and Files
====================
-All new packages should live under the ``CodeIgniter`` namespace. The package itself will need its own sub-namespace
+All new packages should live under the ``CodeIgniter`` namespace.
+The package itself will need its own sub-namespace
that collects all related files into one grouping, like ``CodeIgniter\HTTP``.
-Files MUST be named the same as the class they hold, and they must match the :doc:`Style Guide `, meaning
-CamelCase class and file names. The should be in their own directory that matches the sub-namespace under the **system**
-directory.
+Files MUST be named the same as the class they hold, and they must match the
+:doc:`Style Guide <./styleguide.rst>`, meaning CamelCase class and file names.
+They should be in their own directory that matches the sub-namespace under the
+**system** directory.
-The the Router as an example. The Router lives in the ``CodeIgniter\Router`` namespace. It has two classes,
-**RouteCollection** and **Router**, which are in the files, **system/Router/RouteCollection.php** and
+Take the Router class as an example. The Router lives in the ``CodeIgniter\Router``
+namespace. Its two main classes,
+**RouteCollection** and **Router**, are in the files **system/Router/RouteCollection.php** and
**system/Router/Router.php** respectively.
Interfaces
----------
-Most base classes should have an interface defined for them. At the very least this allows them to be easily mocked
-and passed in other classes as a dependency without breaking the type-hinting. The interface names should match
-the name of the class with "Interface" appended to it, like ``RouteCollectionInterface``.
+Most base classes should have an interface defined for them.
+At the very least this allows them to be easily mocked
+and passed to other classes as a dependency, without breaking the type-hinting.
+The interface names should match the name of the class with "Interface" appended
+to it, like ``RouteCollectionInterface``.
The Router package mentioned above includes the
-``CodeIgniter\Router\RouterCollectionInterface`` and ``CodeIgniter\Router\RouterInterface``
+``CodeIgniter\Router\RouteCollectionInterface`` and ``CodeIgniter\Router\RouterInterface``
interfaces to provide the abstractions for the two classes in the package.
Handlers
--------
-When a package supports multiple "drivers", the convention is to place them in a **Handlers** directory, and
-name the child classes as Handlers. You will often find that creating a ``BaseHandler`` the child classes can
-extend to be beneficial in keeping the code DRY.
+When a package supports multiple "drivers", the convention is to place them in
+a **Handlers** directory, and name the child classes as Handlers.
+You will often find that creating a ``BaseHandler``, that the child classes can
+extend, to be beneficial in keeping the code DRY.
See the Log and Session packages for examples.
Configuration
=============
-Should the package require user-configurable settings, you should create a new file just for that package under
-**application/Config**. The file name should generally match the package name.
+Should the package require user-configurable settings, you should create a new
+file just for that package under **app/Config**.
+The file name should generally match the package name.
Autoloader
==========
-All files within the package should be added to **system/Config/AutoloadConfig.php**, in the "classmap" property.
-This is only used for core framework files, and helps to minimize file system scans and keep performance high.
+All files within the package should be added to **system/Config/AutoloadConfig.php**,
+in the "classmap" property. This is only used for core framework files, and helps
+to minimize file system scans and keep performance high.
Command-Line Support
====================
-CodeIgniter has never been known for it's strong CLI support. However, if your package could benefit from it, create a
-new file under **system/Commands**. The class contained within is simply a controller that is intended for CLI
-usage only. The ``index()`` method should provide a list of available commands provided by that package.
+CodeIgniter has never been known for it's strong CLI support. However, if your
+package could benefit from it, create a new file under **system/Commands**.
+The class contained within is simply a controller that is intended for CLI
+usage only. The ``index()`` method should provide a list of available commands
+provided by that package.
-Routes must be added to **system/Config/Routes.php** using the ``cli()`` method to ensure it is not accessible
-through the browser, but is restricted to the CLI only.
+Routes must be added to **system/Config/Routes.php** using the ``cli()`` method
+to ensure it is not accessible through the browser, but is restricted to the CLI only.
See the **MigrationsCommand** file for an example.
Documentation
=============
-All packages must contain appropriate documentation that matches the tone and style of the rest of the user guide.
-In most cases, the top portion of the package's page should be treated in tutorial fashion, while the second
-half would be a class reference.
+All packages must contain appropriate documentation that matches the tone and
+style of the rest of the user guide. In most cases, the top portion of the package's
+page should be treated in tutorial fashion, while the second half would be a class reference.
diff --git a/contributing/signing.rst b/contributing/signing.rst
index ef8e2c9f27..8d74c1a5aa 100644
--- a/contributing/signing.rst
+++ b/contributing/signing.rst
@@ -9,24 +9,22 @@ The developer pushing a commit as part of a PR isn't necessarily the person
who committed it originally, if the commit is not signed. This distorts the
commit history and makes it hard to tell where code came from.
-If a person "signs" a commit, they are free to use any name, specifically
+If a person "signs off" a commit, they are free to use any name, specifically
one not their own. Again, the commit history cannot be relied on to determine
the origin of the code, if one developer is spoofing another. A malicious person
could commit bad code (for instance a virus) and make it look like another
developer created it.
The best solution, while not fool-proof, is to "securely sign" your
-commits. Such commits are digitally signed, with a GPG-key, and
+commits. Such commits are digitally signed, with a GPG-key
associated with your github account. It still isn't foolproof, because
a malicious developer could create a bogus email and account, but it is
-more reliable than an unsigned or a "signed" commit.
+more reliable than an unsigned or a "signed-off by" commit.
If you don't sign your commits, we **may** accept your contribution,
assuming it meets usefulness and contribution guidelines, but only
if it isn't critical code and only after checking it carefully.
-If code performs an important role, we will insist that it be signed, and if
-it is critical code (however we interpret that), we will insist that your
-contributions be securely signed.
+If code performs an important role, we will insist that it be securely signed.
Read below to find out how to sign your commits :)
diff --git a/contributing/workflow.rst b/contributing/workflow.rst
index c3e1053a2c..a6cbfa8cae 100644
--- a/contributing/workflow.rst
+++ b/contributing/workflow.rst
@@ -25,14 +25,14 @@ which requires all pull requests to be sent to the "develop" branch. This is
where the next planned version will be developed. The "master" branch will
always contain the latest stable version and is kept clean so a "hotfix" (e.g:
an emergency security patch) can be applied to master to create a new version,
-without worrying about other features holding it up. For this reason all
+without worrying about other features holding it up. For this reason, all
commits need to be made to "develop" and any sent to "master" will be closed
automatically. If you have multiple changes to submit, please place each
change into their own branch on your fork.
One thing at a time: a pull request should only contain one change. That does
not mean only one commit, but one change - however many commits it took. The
-reason for this is that if you change X and Y but send a pull request for both
+reason for this is that if you change X and Y but send a single pull request for both
at the same time, we might really want X but disagree with Y, meaning we
cannot merge the request. Using the Git-Flow branching model you can create
new branches for both of these features and send two requests.
@@ -45,7 +45,8 @@ in your github account. You can make changes to your forked repository, while
you cannot do the same with the shared one - you have to submit pull requests
to it instead.
-`Creating a fork `_ is done through the Github website. Navigate to `our
+`Creating a fork `_
+is done through the Github website. Navigate to `our
repository `_,
click the **Fork** button in the top-right of the page, and choose which account or
organization of yours should contain that fork.
@@ -70,7 +71,7 @@ Synching
Within your local repository, Git will have created an alias, **origin**, for the
Github repository it is bound to. You want to create an alias for the shared
-repository, so that you can "synch" the two, making sure that your repository
+repository as well, so that you can "synch" the two, making sure that your repository
includes any other contributions that have been merged by us into the shared repo::
git remote add upstream UPSTREAM_URL
@@ -98,8 +99,11 @@ Branching Revisited
The top of this page talked about the **master** and **develop** branches.
The *best practice* for your work is to create a *feature branch* locally,
to hold a group of related changes (source, unit testing, documentation,
-change log, etc). This local branch should be named appropriately,
-for instance "fix/problem123" or "new/mind-reader".
+change log, etc).
+
+This local branch should be named appropriately, for instance
+"fix/problem123" or "new/mind-reader". The slashes in these branch names is
+optional, and implies a sort of namespacing if used.
For instance, make sure you are in the *develop* branch, and create a
new feature branch, based on *develop*, for a new feature you are creating::
@@ -113,7 +117,7 @@ Committing
==========
Your local changes need to be *committed* to save them in your local repository.
-This is where `contribution signing `_ comes in.
+This is where `contribution signing <./signing.rst>`_ comes in.
You can have as many commits in a branch as you need to "get it right".
For instance, to commit your work from a debugging session::
@@ -179,13 +183,13 @@ Make sure that the PR title is helpful for the maintainers and other developers.
Add any comments appropriate, for instance asking for review.
.. note::
- If you do not provide a title for your PR, the odds of it being summarily rejected
+ If you do not provide a title or description for your PR, the odds of it being summarily rejected
rise astronomically.
When your PR is submitted, a continuous integration task will be triggered,
running all the unit tests as well as any other checking we have configured for it.
If the unit tests fail, or if there are merge conflicts, your PR will not
-be mergeable until fixed.
+be mergeable until those are fixed.
Fix such changes locally, commit them properly, and then push your branch again.
That will update the PR automatically, and re-run the CI tests. You don't need
diff --git a/env b/env
index d5559450fb..bc1a15671d 100644
--- a/env
+++ b/env
@@ -10,6 +10,12 @@
# at the beginning of the line.
#--------------------------------------------------------------------
+#--------------------------------------------------------------------
+# ENVIRONMENT
+#--------------------------------------------------------------------
+
+# CI_ENVIRONMENT = production
+
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
diff --git a/public/index.php b/public/index.php
index cd38b911b5..5b9e912f82 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,7 +1,7 @@
classmap = $config->classmap;
}
- unset($config);
+ // Should we load through Composer's namespaces, also?
+ if ($moduleConfig->discoverInComposer)
+ {
+ $this->discoverComposerNamespaces();
+ }
return $this;
}
@@ -303,8 +310,8 @@ class Autoloader
/**
* Attempts to load the class from common locations in previous
- * version of CodeIgniter, namely 'application/libraries', and
- * 'application/Models'.
+ * version of CodeIgniter, namely 'app/Libraries', and
+ * 'app/Models'.
*
* @param string $class The class name. This typically should NOT have a namespace.
*
@@ -391,5 +398,37 @@ class Autoloader
return $filename;
}
+
//--------------------------------------------------------------------
+
+ /**
+ * Locates all PSR4 compatible namespaces from Composer.
+ */
+ protected function discoverComposerNamespaces()
+ {
+ if (! is_file(COMPOSER_PATH))
+ {
+ return false;
+ }
+
+ $composer = include COMPOSER_PATH;
+
+ $paths = $composer->getPrefixesPsr4();
+ unset($composer);
+
+ // Get rid of CodeIgniter so we don't have duplicates
+ if (isset($paths['CodeIgniter\\']))
+ {
+ unset($paths['CodeIgniter\\']);
+ }
+
+ // Composer stores paths with trailng slash. We don't.
+ $newPaths = [];
+ foreach ($paths as $key => $value)
+ {
+ $newPaths[rtrim($key, '\\ ')] = $value;
+ }
+
+ $this->prefixes = array_merge($this->prefixes, $newPaths);
+ }
}
diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php
index 2ced3b123d..5e59c91427 100644
--- a/system/Autoloader/FileLocator.php
+++ b/system/Autoloader/FileLocator.php
@@ -119,7 +119,8 @@ class FileLocator
{
continue;
}
- $path = $this->getNamespaces($prefix);
+ $path = $this->getNamespaces($prefix);
+
$filename = implode('/', $segments);
break;
}
@@ -201,8 +202,8 @@ class FileLocator
* $locator->search('Config/Routes.php');
* // Assuming PSR4 namespaces include foo and bar, might return:
* [
- * 'application/modules/foo/Config/Routes.php',
- * 'application/modules/bar/Config/Routes.php',
+ * 'app/Modules/foo/Config/Routes.php',
+ * 'app/Modules/bar/Config/Routes.php',
* ]
*
* @param string $path
@@ -268,7 +269,9 @@ class FileLocator
{
$path = $this->autoloader->getNamespace($prefix);
- return isset($path[0]) ? $path[0] : '';
+ return isset($path[0])
+ ? rtrim($path[0], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
+ : '';
}
$namespaces = [];
@@ -279,7 +282,7 @@ class FileLocator
{
$namespaces[] = [
'prefix' => $prefix,
- 'path' => $path,
+ 'path' => rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR,
];
}
}
diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php
index 57792e7a9a..8336f6d661 100644
--- a/system/Cache/Handlers/RedisHandler.php
+++ b/system/Cache/Handlers/RedisHandler.php
@@ -60,6 +60,7 @@ class RedisHandler implements CacheInterface
'password' => null,
'port' => 6379,
'timeout' => 0,
+ 'database' => 0,
];
/**
@@ -116,7 +117,12 @@ class RedisHandler implements CacheInterface
if (isset($config['password']) && ! $this->redis->auth($config['password']))
{
- // log_message('error', 'Cache: Redis authentication failed.');
+ log_message('error', 'Cache: Redis authentication failed.');
+ }
+
+ if (isset($config['database']) && ! $this->redis->select($config['database']))
+ {
+ log_message('error', 'Cache: Redis select database failed.');
}
}
catch (\RedisException $e)
diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php
index 81577591a4..5de6b65b97 100644
--- a/system/CodeIgniter.php
+++ b/system/CodeIgniter.php
@@ -61,7 +61,7 @@ class CodeIgniter
/**
* The current version of CodeIgniter Framework
*/
- const CI_VERSION = '4.0.0-alpha.4';
+ const CI_VERSION = '4.0.0-beta.1';
/**
* App startup time.
@@ -199,7 +199,7 @@ class CodeIgniter
* @param \CodeIgniter\Router\RouteCollectionInterface $routes
* @param boolean $returnResponse
*
- * @throws \CodeIgniter\HTTP\RedirectException
+ * @throws \CodeIgniter\Filters\Exceptions\FilterException
* @throws \Exception
*/
public function run(RouteCollectionInterface $routes = null, bool $returnResponse = false)
@@ -234,7 +234,7 @@ class CodeIgniter
{
return $this->handleRequest($routes, $cacheConfig, $returnResponse);
}
- catch (Router\RedirectException $e)
+ catch (\CodeIgniter\Filters\Exceptions\FilterException $e)
{
$logger = Services::logger();
$logger->info('REDIRECTED ROUTE at ' . $e->getMessage());
@@ -533,7 +533,7 @@ class CodeIgniter
*
* @throws \Exception
*
- * @return boolean
+ * @return bool|\CodeIgniter\HTTP\ResponseInterface
*/
public function displayCache($config)
{
@@ -564,7 +564,9 @@ class CodeIgniter
$this->response->setBody($output);
return $this->response;
- };
+ }
+
+ return false;
}
//--------------------------------------------------------------------
@@ -574,7 +576,7 @@ class CodeIgniter
*
* @param integer $time
*
- * @return $this
+ * @return void
*/
public static function cache(int $time)
{
diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php
index cfededefb7..10475f5389 100644
--- a/system/Commands/Database/MigrateRollback.php
+++ b/system/Commands/Database/MigrateRollback.php
@@ -117,7 +117,7 @@ class MigrateRollback extends BaseCommand
}
try
{
- if (! $this->isAllNamespace())
+ if (! $this->isAllNamespace($params))
{
$namespace = $params['-n'] ?? CLI::getOption('n');
$runner->version(0, $namespace);
diff --git a/system/Commands/Server/Serve.php b/system/Commands/Server/Serve.php
index e69e278cc7..9691e971d4 100644
--- a/system/Commands/Server/Serve.php
+++ b/system/Commands/Server/Serve.php
@@ -48,7 +48,7 @@ use CodeIgniter\CLI\CLI;
*/
class Serve extends BaseCommand
{
- protected $minPHPVersion = '7.1';
+ protected $minPHPVersion = '7.2';
protected $group = 'CodeIgniter';
protected $name = 'serve';
diff --git a/system/Common.php b/system/Common.php
index b882472f30..4291e60908 100644
--- a/system/Common.php
+++ b/system/Common.php
@@ -102,6 +102,35 @@ if (! function_exists('config'))
//--------------------------------------------------------------------
+if (! function_exists('db_connnect'))
+{
+ /**
+ * Grabs a database connection and returns it to the user.
+ *
+ * This is a convenience wrapper for \Config\Database::connect()
+ * and supports the same parameters. Namely:
+ *
+ * When passing in $db, you may pass any of the following to connect:
+ * - group name
+ * - existing connection instance
+ * - array of database configuration values
+ *
+ * If $getShared === false then a new connection instance will be provided,
+ * otherwise it will all calls will return the same instance.
+ *
+ * @param \CodeIgniter\Database\ConnectionInterface|array|string $db
+ * @param boolean $getShared
+ *
+ * @return \CodeIgniter\Database\BaseConnection
+ */
+ function db_connect($db = null, bool $getShared = true)
+ {
+ return \Config\Database::connect($db, $getShared);
+ }
+}
+
+//--------------------------------------------------------------------
+
if (! function_exists('view'))
{
/**
@@ -540,7 +569,7 @@ if (! function_exists('helper'))
* both in and out of the 'helpers' directory of a namespaced directory.
*
* Will load ALL helpers of the matching name, in the following order:
- * 1. application/Helpers
+ * 1. app/Helpers
* 2. {namespace}/Helpers
* 3. system/Helpers
*
@@ -571,44 +600,62 @@ if (! function_exists('helper'))
$filename .= '_helper';
}
- $paths = $loader->search('Helpers/' . $filename);
-
- if (! empty($paths))
+ // If the file is namespaced, we'll just grab that
+ // file and not search for any others
+ if (strpos($filename, '\\') !== false)
{
- foreach ($paths as $path)
+ $path = $loader->locateFile($filename, 'Helpers');
+
+ if (empty($path))
{
- if (strpos($path, APPPATH) === 0)
+ throw \CodeIgniter\Files\Exceptions\FileNotFoundException::forFileNotFound($filename);
+ }
+
+ $includes[] = $path;
+ }
+
+ // No namespaces, so search in all available locations
+ else
+ {
+ $paths = $loader->search('Helpers/' . $filename);
+
+ if (! empty($paths))
+ {
+ foreach ($paths as $path)
{
- // @codeCoverageIgnoreStart
- $appHelper = $path;
- // @codeCoverageIgnoreEnd
- }
- elseif (strpos($path, SYSTEMPATH) === 0)
- {
- $systemHelper = $path;
- }
- else
- {
- $localIncludes[] = $path;
+ if (strpos($path, APPPATH) === 0)
+ {
+ // @codeCoverageIgnoreStart
+ $appHelper = $path;
+ // @codeCoverageIgnoreEnd
+ }
+ elseif (strpos($path, SYSTEMPATH) === 0)
+ {
+ $systemHelper = $path;
+ }
+ else
+ {
+ $localIncludes[] = $path;
+ }
}
}
- }
- // App-level helpers should override all others
- if (! empty($appHelper))
- {
- // @codeCoverageIgnoreStart
- $includes[] = $appHelper;
- // @codeCoverageIgnoreEnd
- }
+ // App-level helpers should override all others
+ if (! empty($appHelper))
+ {
+ // @codeCoverageIgnoreStart
+ $includes[] = $appHelper;
+ // @codeCoverageIgnoreEnd
+ }
- // All namespaced files get added in next
- $includes = array_merge($includes, $localIncludes);
+ // All namespaced files get added in next
+ $includes = array_merge($includes, $localIncludes);
- // And the system default one should be added in last.
- if (! empty($systemHelper))
- {
- $includes[] = $systemHelper;
+ // And the system default one should be added in last.
+ if (! empty($systemHelper))
+ {
+ $includes[] = $systemHelper;
+ }
}
}
diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php
index 41a9b79cda..62929cef8a 100644
--- a/system/Config/BaseService.php
+++ b/system/Config/BaseService.php
@@ -207,7 +207,7 @@ class BaseService
if ($init_autoloader)
{
- static::autoloader()->initialize(new \Config\Autoload());
+ static::autoloader()->initialize(new \Config\Autoload(), new \Config\Modules());
}
}
diff --git a/system/Config/Config.php b/system/Config/Config.php
index 1750867d8a..fd270a2931 100644
--- a/system/Config/Config.php
+++ b/system/Config/Config.php
@@ -97,6 +97,16 @@ class Config
//--------------------------------------------------------------------
+ /**
+ * Resets the instances array
+ */
+ public static function reset()
+ {
+ static::$instances = [];
+ }
+
+ //--------------------------------------------------------------------
+
/**
* Find configuration class and create instance
*
diff --git a/system/Config/Services.php b/system/Config/Services.php
index 8a36db13f1..f9ce19336d 100644
--- a/system/Config/Services.php
+++ b/system/Config/Services.php
@@ -466,7 +466,7 @@ class Services extends BaseService
*
* @return \CodeIgniter\View\Parser
*/
- public static function parser($viewPath = APPPATH . 'Views/', $config = null, bool $getShared = true)
+ public static function parser($viewPath = null, $config = null, bool $getShared = true)
{
if ($getShared)
{
@@ -478,6 +478,12 @@ class Services extends BaseService
$config = new \Config\View();
}
+ if (is_null($viewPath))
+ {
+ $paths = config('Paths');
+ $viewPath = $paths->viewDirectory;
+ }
+
return new \CodeIgniter\View\Parser($config, $viewPath, static::locator(true), CI_DEBUG, static::logger(true));
}
@@ -808,7 +814,7 @@ class Services extends BaseService
if (is_null($config))
{
- $config = new \Config\Validation();
+ $config = config('Validation');
}
return new \CodeIgniter\Validation\Validation($config, static::renderer());
diff --git a/system/Controller.php b/system/Controller.php
index 269e78beaa..ea5f0f3222 100644
--- a/system/Controller.php
+++ b/system/Controller.php
@@ -35,7 +35,7 @@
* @since Version 3.0.0
* @filesource
*/
-
+use CodeIgniter\Config\Services;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Validation\Validation;
@@ -106,7 +106,7 @@ class Controller
* @param ResponseInterface $response
* @param \Psr\Log\LoggerInterface $logger
*
- * @throws \CodeIgniter\HTTP\RedirectException
+ * @throws \CodeIgniter\HTTP\Exceptions\HTTPException
*/
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
@@ -135,7 +135,7 @@ class Controller
* considered secure for. Only with HSTS header.
* Default value is 1 year.
*
- * @throws \CodeIgniter\HTTP\RedirectException
+ * @throws \CodeIgniter\HTTP\Exceptions\HTTPException
*/
public function forceHTTPS(int $duration = 31536000)
{
@@ -179,8 +179,8 @@ class Controller
* A shortcut to performing validation on input data. If validation
* is not successful, a $errors property will be set on this class.
*
- * @param array $rules
- * @param array $messages An array of custom error messages
+ * @param array|string $rules
+ * @param array $messages An array of custom error messages
*
* @return boolean
*/
diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
index 6ee3925048..be8269c0ca 100644
--- a/system/Database/BaseBuilder.php
+++ b/system/Database/BaseBuilder.php
@@ -664,7 +664,7 @@ class BaseBuilder
$op = $this->getOperator($k);
$k = trim(str_replace($op, '', $k));
- $bind = $this->setBind($k, $v);
+ $bind = $this->setBind($k, $v, $escape);
if (empty($op))
{
@@ -813,8 +813,8 @@ class BaseBuilder
$not = ($not) ? ' NOT' : '';
- $where_in = array_values($values);
- $this->binds[$ok] = $where_in;
+ $where_in = array_values($values);
+ $ok = $this->setBind($ok, $where_in, $escape);
$prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type);
@@ -955,19 +955,19 @@ class BaseBuilder
if ($side === 'none')
{
- $bind = $this->setBind($k, $v);
+ $bind = $this->setBind($k, $v, $escape);
}
elseif ($side === 'before')
{
- $bind = $this->setBind($k, "%$v");
+ $bind = $this->setBind($k, "%$v", $escape);
}
elseif ($side === 'after')
{
- $bind = $this->setBind($k, "$v%");
+ $bind = $this->setBind($k, "$v%", $escape);
}
else
{
- $bind = $this->setBind($k, "%$v%");
+ $bind = $this->setBind($k, "%$v%", $escape);
}
$like_statement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
@@ -1345,7 +1345,7 @@ class BaseBuilder
{
if ($escape)
{
- $bind = $this->setBind($k, $v);
+ $bind = $this->setBind($k, $v, $escape);
$this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = ":$bind:";
}
else
@@ -1399,11 +1399,32 @@ class BaseBuilder
$this->resetSelect();
}
- return $select;
+ return $this->compileFinalQuery($select);
}
//--------------------------------------------------------------------
+ /**
+ * Returns a finalized, compiled query string with the bindings
+ * inserted and prefixes swapped out.
+ *
+ * @param string $sql
+ *
+ * @return mixed|string
+ */
+ protected function compileFinalQuery(string $sql): string
+ {
+ $query = new Query($this->db);
+ $query->setQuery($sql, $this->binds, false);
+
+ if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix))
+ {
+ $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
+ }
+
+ return $query->getQuery();
+ }
+
/**
* Get
*
@@ -1423,7 +1444,10 @@ class BaseBuilder
{
$this->limit($limit, $offset);
}
- $result = $returnSQL ? $this->getCompiledSelect() : $this->db->query($this->compileSelect(), $this->binds);
+
+ $result = $returnSQL
+ ? $this->getCompiledSelect()
+ : $this->db->query($this->compileSelect(), $this->binds, false);
if ($reset === true)
{
@@ -1461,7 +1485,7 @@ class BaseBuilder
return $sql;
}
- $query = $this->db->query($sql);
+ $query = $this->db->query($sql, null, false);
if (empty($query->getResult()))
{
return 0;
@@ -1510,7 +1534,7 @@ class BaseBuilder
return $sql;
}
- $result = $this->db->query($sql, $this->binds);
+ $result = $this->db->query($sql, $this->binds, false);
if ($reset === true)
{
@@ -1559,7 +1583,7 @@ class BaseBuilder
$this->limit($limit, $offset);
}
- $result = $this->db->query($this->compileSelect(), $this->binds);
+ $result = $this->db->query($this->compileSelect(), $this->binds, false);
$this->resetSelect();
return $result;
@@ -1624,7 +1648,7 @@ class BaseBuilder
}
else
{
- $this->db->query($sql, $this->binds);
+ $this->db->query($sql, $this->binds, false);
$affected_rows += $this->db->affectedRows();
}
}
@@ -1696,7 +1720,7 @@ class BaseBuilder
$clean = [];
foreach ($row as $k => $value)
{
- $clean[] = ':' . $this->setBind($k, $value) . ':';
+ $clean[] = ':' . $this->setBind($k, $value, $escape) . ':';
}
$row = $clean;
@@ -1741,7 +1765,7 @@ class BaseBuilder
$this->resetWrite();
}
- return $sql;
+ return $this->compileFinalQuery($sql);
}
//--------------------------------------------------------------------
@@ -1779,7 +1803,7 @@ class BaseBuilder
{
$this->resetWrite();
- $result = $this->db->query($sql, $this->binds);
+ $result = $this->db->query($sql, $this->binds, false);
// Clear our binds so we don't eat up memory
$this->binds = [];
@@ -1868,7 +1892,7 @@ class BaseBuilder
$this->resetWrite();
- return $returnSQL ? $sql : $this->db->query($sql, $this->binds);
+ return $returnSQL ? $sql : $this->db->query($sql, $this->binds, false);
}
//--------------------------------------------------------------------
@@ -1931,7 +1955,7 @@ class BaseBuilder
$this->resetWrite();
}
- return $sql;
+ return $this->compileFinalQuery($sql);
}
//--------------------------------------------------------------------
@@ -1981,7 +2005,7 @@ class BaseBuilder
{
$this->resetWrite();
- if ($this->db->query($sql, $this->binds))
+ if ($this->db->query($sql, $this->binds, false))
{
// Clear our binds so we don't eat up memory
$this->binds = [];
@@ -2115,7 +2139,7 @@ class BaseBuilder
}
else
{
- $this->db->query($sql, $this->binds);
+ $this->db->query($sql, $this->binds, false);
$affected_rows += $this->db->affectedRows();
}
@@ -2203,7 +2227,7 @@ class BaseBuilder
$index_set = true;
}
- $bind = $this->setBind($k2, $v2);
+ $bind = $this->setBind($k2, $v2, $escape);
$clean[$this->db->protectIdentifiers($k2, false, $escape)] = ":$bind:";
}
@@ -2242,7 +2266,7 @@ class BaseBuilder
$this->resetWrite();
- return $this->db->query($sql);
+ return $this->db->query($sql, null, false);
}
//--------------------------------------------------------------------
@@ -2271,7 +2295,7 @@ class BaseBuilder
$this->resetWrite();
- return $this->db->query($sql);
+ return $this->db->query($sql, null, false);
}
//--------------------------------------------------------------------
@@ -2312,7 +2336,7 @@ class BaseBuilder
$sql = $this->delete($table, '', null, $reset);
$this->returnDeleteSQL = false;
- return $sql;
+ return $this->compileFinalQuery($sql);
}
//--------------------------------------------------------------------
@@ -2371,7 +2395,7 @@ class BaseBuilder
$this->resetWrite();
}
- return ($returnSQL === true) ? $sql : $this->db->query($sql, $this->binds);
+ return ($returnSQL === true) ? $sql : $this->db->query($sql, $this->binds, false);
}
//--------------------------------------------------------------------
@@ -2390,7 +2414,7 @@ class BaseBuilder
$sql = $this->_update($this->QBFrom[0], [$column => "{$column} + {$value}"]);
- return $this->db->query($sql, $this->binds);
+ return $this->db->query($sql, $this->binds, false);
}
//--------------------------------------------------------------------
@@ -2409,7 +2433,7 @@ class BaseBuilder
$sql = $this->_update($this->QBFrom[0], [$column => "{$column}-{$value}"]);
- return $this->db->query($sql, $this->binds);
+ return $this->db->query($sql, $this->binds, false);
}
//--------------------------------------------------------------------
@@ -2918,17 +2942,24 @@ class BaseBuilder
/**
* Stores a bind value after ensuring that it's unique.
+ * While it might be nicer to have named keys for our binds array
+ * with PHP 7+ we get a huge memory/performance gain with indexed
+ * arrays instead, so lets take advantage of that here.
*
- * @param string $key
- * @param null $value
+ * @param string $key
+ * @param null $value
+ * @param boolean $escape
*
* @return string
*/
- protected function setBind(string $key, $value = null)
+ protected function setBind(string $key, $value = null, bool $escape = true)
{
if (! array_key_exists($key, $this->binds))
{
- $this->binds[$key] = $value;
+ $this->binds[$key] = [
+ $value,
+ $escape,
+ ];
return $key;
}
@@ -2937,10 +2968,13 @@ class BaseBuilder
while (array_key_exists($key . $count, $this->binds))
{
- ++ $count;
+ ++$count;
}
- $this->binds[$key . $count] = $value;
+ $this->binds[$key . $count] = [
+ $value,
+ $escape,
+ ];
return $key . $count;
}
diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php
index d860304d6f..477574d98d 100644
--- a/system/Database/BaseConnection.php
+++ b/system/Database/BaseConnection.php
@@ -596,12 +596,14 @@ abstract class BaseConnection implements ConnectionInterface
* Should automatically handle different connections for read/write
* queries if needed.
*
- * @param string $sql
- * @param array ...$binds
- * @param string $queryClass
+ * @param string $sql
+ * @param array ...$binds
+ * @param boolean $setEscapeFlags
+ * @param string $queryClass
+ *
* @return BaseResult|Query|false
*/
- public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Database\\Query')
+ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, $queryClass = 'CodeIgniter\\Database\\Query')
{
if (empty($this->connID))
{
@@ -609,13 +611,12 @@ abstract class BaseConnection implements ConnectionInterface
}
$resultClass = str_replace('Connection', 'Result', get_class($this));
-
/**
* @var Query $query
*/
$query = new $queryClass($this);
- $query->setQuery($sql, $binds);
+ $query->setQuery($sql, $binds, $setEscapeFlags);
if (! empty($this->swapPre) && ! empty($this->DBPrefix))
{
@@ -1709,6 +1710,20 @@ abstract class BaseConnection implements ConnectionInterface
//--------------------------------------------------------------------
+ /**
+ * Empties our data cache. Especially helpful during testing.
+ *
+ * @return $this
+ */
+ public function resetDataCache()
+ {
+ $this->dataCache = [];
+
+ return $this;
+ }
+
+ //--------------------------------------------------------------------
+
/**
* Returns the last error code and message.
*
diff --git a/system/Database/Config.php b/system/Database/Config.php
index c236ff45e7..f986df7e09 100644
--- a/system/Database/Config.php
+++ b/system/Database/Config.php
@@ -74,6 +74,12 @@ class Config extends BaseConfig
*/
public static function connect($group = null, bool $getShared = true)
{
+ // If a DB connection is passed in, just pass it back
+ if ($group instanceof BaseConnection)
+ {
+ return $group;
+ }
+
if (is_array($group))
{
$config = $group;
@@ -135,44 +141,7 @@ class Config extends BaseConfig
*/
public static function forge($group = null)
{
- // Allow custom connections to be sent in
- if (is_array($group))
- {
- $config = $group;
- $group = 'custom-' . md5(json_encode($config));
- }
- else
- {
- $config = config('Database');
- }
-
- static::ensureFactory();
-
- if (empty($group))
- {
- $group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
- }
-
- if (is_string($group) && ! isset($config->$group) && ! is_array($config))
- {
- throw new \InvalidArgumentException($group . ' is not a valid database connection group.');
- }
-
- if (! isset(static::$instances[$group]))
- {
- if (is_array($config))
- {
- $db = static::connect($config);
- }
- else
- {
- $db = static::connect($group);
- }
- }
- else
- {
- $db = static::$instances[$group];
- }
+ $db = static::connect($group);
return static::$factory->loadForge($db);
}
@@ -182,50 +151,13 @@ class Config extends BaseConfig
/**
* Returns a new instance of the Database Utilities class.
*
- * @param string|null $group
+ * @param string|array|null $group
*
* @return BaseUtils
*/
- public static function utils(string $group = null)
+ public static function utils($group = null)
{
- // Allow custom connections to be sent in
- if (is_array($group))
- {
- $config = $group;
- $group = 'custom-' . md5(json_encode($config));
- }
- else
- {
- $config = config('Database');
- }
-
- static::ensureFactory();
-
- if (empty($group))
- {
- $group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
- }
-
- if (is_string($group) && ! isset($config->$group) && ! is_array($config))
- {
- throw new \InvalidArgumentException($group . ' is not a valid database connection group.');
- }
-
- if (! isset(static::$instances[$group]))
- {
- if (is_array($config))
- {
- $db = static::connect($config);
- }
- else
- {
- $db = static::connect($group);
- }
- }
- else
- {
- $db = static::$instances[$group];
- }
+ $db = static::connect($group);
return static::$factory->loadUtils($db);
}
@@ -241,7 +173,7 @@ class Config extends BaseConfig
*/
public static function seeder(string $group = null)
{
- $config = new \Config\Database();
+ $config = config('Database');
return new Seeder($config, static::connect($group));
}
diff --git a/system/Database/Exceptions/DataException.php b/system/Database/Exceptions/DataException.php
index 806139211e..1d8006d7df 100644
--- a/system/Database/Exceptions/DataException.php
+++ b/system/Database/Exceptions/DataException.php
@@ -45,4 +45,9 @@ class DataException extends \RuntimeException implements ExceptionInterface
{
return new static(lang('Database.invalidAllowedFields', [$model]));
}
+
+ public static function forTableNotFound(string $table)
+ {
+ return new static(lang('Database.tableNotFound', [$table]));
+ }
}
diff --git a/system/Database/Forge.php b/system/Database/Forge.php
index b84557c3c8..558fb4c27c 100644
--- a/system/Database/Forge.php
+++ b/system/Database/Forge.php
@@ -468,7 +468,7 @@ class Forge
if (is_bool($sql))
{
- $this->_reset();
+ $this->reset();
if ($sql === false)
{
if ($this->db->DBDebug)
@@ -494,7 +494,7 @@ class Forge
}
}
- $this->_reset();
+ $this->reset();
return $result;
}
@@ -730,7 +730,7 @@ class Forge
}
$sqls = $this->_alterTable('ADD', $this->db->DBPrefix . $table, $this->_processFields());
- $this->_reset();
+ $this->reset();
if ($sqls === false)
{
if ($this->db->DBDebug)
@@ -806,7 +806,7 @@ class Forge
}
$sqls = $this->_alterTable('CHANGE', $this->db->DBPrefix . $table, $this->_processFields());
- $this->_reset();
+ $this->reset();
if ($sqls === false)
{
if ($this->db->DBDebug)
@@ -817,11 +817,14 @@ class Forge
return false;
}
- for ($i = 0, $c = count($sqls); $i < $c; $i++)
+ if ($sqls !== null)
{
- if ($this->db->query($sqls[$i]) === false)
+ for ($i = 0, $c = count($sqls); $i < $c; $i++)
{
- return false;
+ if ($this->db->query($sqls[$i]) === false)
+ {
+ return false;
+ }
}
}
@@ -1251,7 +1254,7 @@ class Forge
*
* @return void
*/
- protected function _reset()
+ public function reset()
{
$this->fields = $this->keys = $this->uniqueKeys = $this->primaryKeys = $this->foreignKeys = [];
}
diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php
index ba1cb3116e..b0de8c69fe 100644
--- a/system/Database/MigrationRunner.php
+++ b/system/Database/MigrationRunner.php
@@ -126,17 +126,37 @@ class MigrationRunner
*/
protected $cliMessages = [];
+ /**
+ * Tracks whether we have already ensured
+ * the table exists or not.
+ *
+ * @var boolean
+ */
+ protected $tableChecked = false;
+
+ /**
+ * The full path to locate migration files.
+ *
+ * @var string
+ */
+ protected $path;
+
//--------------------------------------------------------------------
/**
* Constructor.
*
- * @param BaseConfig $config
- * @param \CodeIgniter\Database\ConnectionInterface $db
+ * When passing in $db, you may pass any of the following to connect:
+ * - group name
+ * - existing connection instance
+ * - array of database configuration values
+ *
+ * @param BaseConfig $config
+ * @param \CodeIgniter\Database\ConnectionInterface|array|string $db
*
* @throws ConfigException
*/
- public function __construct(BaseConfig $config, ConnectionInterface $db = null)
+ public function __construct(BaseConfig $config, $db = null)
{
$this->enabled = $config->enabled ?? false;
$this->type = $config->type ?? 'timestamp';
@@ -147,15 +167,10 @@ class MigrationRunner
$this->namespace = APP_NAMESPACE;
// get default database group
- $config = new \Config\Database();
+ $config = config('Database');
$this->group = $config->defaultGroup;
unset($config);
- if (empty($this->table))
- {
- throw ConfigException::forMissingMigrationsTable();
- }
-
if (! in_array($this->type, ['sequential', 'timestamp']))
{
throw ConfigException::forInvalidMigrationType($this->type);
@@ -166,9 +181,7 @@ class MigrationRunner
// If no db connection passed in, use
// default database group.
- $this->db = ! empty($db) ? $db : \Config\Database::connect();
-
- $this->ensureTable();
+ $this->db = db_connect($db);
}
//--------------------------------------------------------------------
@@ -192,6 +205,9 @@ class MigrationRunner
{
throw ConfigException::forDisabledMigrations();
}
+
+ $this->ensureTable();
+
// Set Namespace if not null
if (! is_null($namespace))
{
@@ -204,6 +220,12 @@ class MigrationRunner
$this->setGroup($group);
}
+ // Sequential versions need adjusting to 3 places so they can be found later.
+ if ($this->type === 'sequential')
+ {
+ $targetVersion = str_pad($targetVersion, 3, '0', STR_PAD_LEFT);
+ }
+
$migrations = $this->findMigrations();
if (empty($migrations))
@@ -284,6 +306,8 @@ class MigrationRunner
*/
public function latest(string $namespace = null, string $group = null)
{
+ $this->ensureTable();
+
// Set Namespace if not null
if (! is_null($namespace))
{
@@ -315,6 +339,8 @@ class MigrationRunner
*/
public function latestAll(string $group = null)
{
+ $this->ensureTable();
+
// Set database group if not null
if (! is_null($group))
{
@@ -322,7 +348,7 @@ class MigrationRunner
}
// Get all namespaces form PSR4 paths.
- $config = new Autoload();
+ $config = config('Autoload');
$namespaces = $config->psr4;
foreach ($namespaces as $namespace => $path)
@@ -361,6 +387,8 @@ class MigrationRunner
*/
public function current(string $group = null)
{
+ $this->ensureTable();
+
// Set database group if not null
if (! is_null($group))
{
@@ -380,33 +408,59 @@ class MigrationRunner
public function findMigrations()
{
$migrations = [];
- // Get namespace location form PSR4 paths.
- $config = new Autoload();
+ helper('filesystem');
- $location = $config->psr4[$this->namespace];
+ // If $this->path contains a valid directory use it.
+ if (! empty($this->path))
+ {
+ $dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
+ }
+ // Otherwise, get namespace location form PSR4 paths
+ // and add Database/Migrations for a standard loation.
+ else
+ {
+ $config = config('Autoload');
- // Setting migration directories.
- $dir = rtrim($location, DIRECTORY_SEPARATOR) . '/Database/Migrations/';
+ $location = $config->psr4[$this->namespace];
+
+ // Setting migration directories.
+ $dir = rtrim($location, DIRECTORY_SEPARATOR) . '/Database/Migrations/';
+ }
// Load all *_*.php files in the migrations path
- foreach (glob($dir . '*_*.php') as $file)
+ // We can't use glob if we want it to be testable....
+ $files = get_filenames($dir, true);
+
+ foreach ($files as $file)
{
+ if (substr($file, -4) !== '.php')
+ {
+ continue;
+ }
+
+ // Remove the extension
$name = basename($file, '.php');
+
// Filter out non-migration files
if (preg_match($this->regex, $name))
{
// Create migration object using stdClass
$migration = new \stdClass();
+
// Get migration version number
$migration->version = $this->getMigrationNumber($name);
$migration->name = $this->getMigrationName($name);
- $migration->path = $file;
+ $migration->path = ! empty($this->path) && strpos($file, $this->path) !== 0
+ ? $this->path . $file
+ : $file;
// Add to migrations[version]
$migrations[$migration->version] = $migration;
}
}
+ ksort($migrations);
+
return $migrations;
}
@@ -436,7 +490,7 @@ class MigrationRunner
}
// Check if $targetversion file is found
- if ($targetversion !== '0' && ! array_key_exists($targetversion, $migrations))
+ if ((int)$targetversion !== 0 && ! array_key_exists($targetversion, $migrations))
{
if ($this->silent)
{
@@ -458,14 +512,14 @@ class MigrationRunner
{
if ($this->type === 'sequential' && abs($migration->version - $loop) > 1)
{
- throw new \RuntimeException(lang('Migration.gap') . ' ' . $migration->version);
+ throw new \RuntimeException(lang('Migrations.gap') . ' ' . $migration->version);
}
// Check if all old migration files are all available to do downgrading
if ($method === 'down')
{
if ($loop <= $history_size && $history_migrations[$loop]['version'] !== $migration->version)
{
- throw new \RuntimeException(lang('Migration.gap') . ' ' . $migration->version);
+ throw new \RuntimeException(lang('Migrations.gap') . ' ' . $migration->version);
}
}
$loop ++;
@@ -476,6 +530,22 @@ class MigrationRunner
//--------------------------------------------------------------------
+ /**
+ * Sets the path to the base directory that will be used
+ * when locating migrations. If left null, the value will
+ * be chosen from $this->namespace's directory.
+ *
+ * @param string|null $path
+ *
+ * @return $this
+ */
+ public function setPath(string $path = null)
+ {
+ $this->path = $path;
+
+ return $this;
+ }
+
/**
* Set namespace.
* Allows other scripts to modify on the fly as needed.
@@ -514,10 +584,14 @@ class MigrationRunner
* Set migration Name.
*
* @param string $name
+ *
+ * @return \CodeIgniter\Database\MigrationRunner
*/
public function setName(string $name)
{
$this->name = $name;
+
+ return $this;
}
//--------------------------------------------------------------------
@@ -531,6 +605,8 @@ class MigrationRunner
*/
public function getHistory(string $group = 'default')
{
+ $this->ensureTable();
+
$query = $this->db->table($this->table)
->where('group', $group)
->where('namespace', $this->namespace)
@@ -602,6 +678,8 @@ class MigrationRunner
*/
protected function getVersion()
{
+ $this->ensureTable();
+
$row = $this->db->table($this->table)
->select('version')
->where('group', $this->group)
@@ -675,14 +753,14 @@ class MigrationRunner
* Ensures that we have created our migrations table
* in the database.
*/
- protected function ensureTable()
+ public function ensureTable()
{
- if ($this->db->tableExists($this->table))
+ if ($this->tableChecked || $this->db->tableExists($this->table))
{
return;
}
- $forge = \Config\Database::forge();
+ $forge = \Config\Database::forge($this->db);
$forge->addField([
'version' => [
@@ -713,6 +791,8 @@ class MigrationRunner
]);
$forge->createTable($this->table, true);
+
+ $this->tableChecked = true;
}
//--------------------------------------------------------------------
diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php
index 42e1b512b7..478df55703 100644
--- a/system/Database/Postgre/Builder.php
+++ b/system/Database/Postgre/Builder.php
@@ -105,7 +105,7 @@ class Builder extends BaseBuilder
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') + {$value}"]);
- return $this->db->query($sql, $this->binds);
+ return $this->db->query($sql, $this->binds, false);
}
//--------------------------------------------------------------------
@@ -124,7 +124,7 @@ class Builder extends BaseBuilder
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') - {$value}"]);
- return $this->db->query($sql, $this->binds);
+ return $this->db->query($sql, $this->binds, false);
}
//--------------------------------------------------------------------
@@ -162,7 +162,14 @@ class Builder extends BaseBuilder
$table = $this->QBFrom[0];
- $set = $this->binds;
+ $set = $this->binds;
+
+ // We need to grab out the actual values from
+ // the way binds are stored with escape flag.
+ array_walk($set, function (&$item) {
+ $item = $item[0];
+ });
+
$keys = array_keys($set);
$values = array_values($set);
diff --git a/system/Database/Query.php b/system/Database/Query.php
index 5d51899306..8c41042a3a 100644
--- a/system/Database/Query.php
+++ b/system/Database/Query.php
@@ -129,17 +129,32 @@ class Query implements QueryInterface
/**
* Sets the raw query string to use for this statement.
*
- * @param string $sql
- * @param array $binds
+ * @param string $sql
+ * @param array $binds
+ * @param boolean $setEscape
*
* @return mixed
*/
- public function setQuery(string $sql, $binds = null)
+ public function setQuery(string $sql, $binds = null, bool $setEscape = true)
{
$this->originalQueryString = $sql;
if (! is_null($binds))
{
+ if (! is_array($binds))
+ {
+ $binds = [$binds];
+ }
+
+ if ($setEscape)
+ {
+ array_walk($binds, function (&$item) {
+ $item = [
+ $item,
+ true,
+ ];
+ });
+ }
$this->binds = $binds;
}
@@ -407,19 +422,18 @@ class Query implements QueryInterface
foreach ($binds as $placeholder => $value)
{
- $escapedValue = $this->db->escape($value);
+ // $value[1] contains the boolean whether should be escaped or not
+ $escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
// In order to correctly handle backlashes in saved strings
// we will need to preg_quote, so remove the wrapping escape characters
// otherwise it will get escaped.
- if (is_array($value))
+ if (is_array($value[0]))
{
$escapedValue = '(' . implode(',', $escapedValue) . ')';
}
$replacers[":{$placeholder}:"] = $escapedValue;
-
- // $sql = preg_replace('|:' . $placeholder . '(?!\w)|', $escapedValue, $sql);
}
$sql = strtr($sql, $replacers);
@@ -460,7 +474,7 @@ class Query implements QueryInterface
do
{
$c --;
- $escapedValue = $this->db->escape($binds[$c]);
+ $escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c[0]];
if (is_array($escapedValue))
{
$escapedValue = '(' . implode(',', $escapedValue) . ')';
diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php
index 9ffe98fc63..757adbeef4 100644
--- a/system/Database/SQLite3/Builder.php
+++ b/system/Database/SQLite3/Builder.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php
index 367945d171..45684728a1 100644
--- a/system/Database/SQLite3/Connection.php
+++ b/system/Database/SQLite3/Connection.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
@@ -307,6 +307,7 @@ class Connection extends BaseConnection implements ConnectionInterface
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
+
if (empty($query))
{
return [];
@@ -319,7 +320,8 @@ class Connection extends BaseConnection implements ConnectionInterface
$retval[$i]->type = $query[$i]->type;
$retval[$i]->max_length = null;
$retval[$i]->default = $query[$i]->dflt_value;
- $retval[$i]->primary_key = isset($query[$i]->pk) ? (int)$query[$i]->pk : 0;
+ $retval[$i]->primary_key = isset($query[$i]->pk) ? (bool)$query[$i]->pk : false;
+ $retval[$i]->nullable = isset($query[$i]->notnull) ? ! (bool)$query[$i]->notnull : false;
}
return $retval;
diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php
index a6f9a58bb2..c37f5c08fa 100644
--- a/system/Database/SQLite3/Forge.php
+++ b/system/Database/SQLite3/Forge.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
@@ -152,12 +152,29 @@ class Forge extends \CodeIgniter\Database\Forge
*/
protected function _alterTable($alter_type, $table, $field)
{
- if (in_array($alter_type, ['DROP', 'CHANGE'], true))
+ switch ($alter_type)
{
- return false;
- }
+ case 'DROP':
+ $sqlTable = new Table($this->db, $this);
- return parent::_alterTable($alter_type, $table, $field);
+ $sqlTable->fromTable($table)
+ ->dropColumn($field)
+ ->run();
+
+ return '';
+ break;
+ case 'CHANGE':
+ $sqlTable = new Table($this->db, $this);
+
+ $sqlTable->fromTable($table)
+ ->modifyColumn($field)
+ ->run();
+
+ return null;
+ break;
+ default:
+ return parent::_alterTable($alter_type, $table, $field);
+ }
}
//--------------------------------------------------------------------
diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php
index b0dd1a9a83..ff194aa897 100644
--- a/system/Database/SQLite3/PreparedQuery.php
+++ b/system/Database/SQLite3/PreparedQuery.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php
index 5737bf3529..c32dec51a2 100644
--- a/system/Database/SQLite3/Result.php
+++ b/system/Database/SQLite3/Result.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php
new file mode 100644
index 0000000000..1c450eb6cf
--- /dev/null
+++ b/system/Database/SQLite3/Table.php
@@ -0,0 +1,343 @@
+db = $db;
+ $this->forge = $forge;
+ }
+
+ /**
+ * Reads an existing database table and
+ * collects all of the information needed to
+ * recreate this table.
+ *
+ * @param string $table
+ *
+ * @return \CodeIgniter\Database\SQLite3\Table
+ */
+ public function fromTable(string $table)
+ {
+ $this->prefixedTableName = $table;
+
+ // Remove the prefix, if any, since it's
+ // already been added by the time we get here...
+ $prefix = $this->db->DBPrefix;
+ if (! empty($prefix))
+ {
+ if (strpos($table, $prefix) === 0)
+ {
+ $table = substr($table, strlen($prefix));
+ }
+ }
+
+ if (! $this->db->tableExists($this->prefixedTableName))
+ {
+ throw DataException::forTableNotFound($this->prefixedTableName);
+ }
+
+ $this->tableName = $table;
+
+ $this->fields = $this->formatFields($this->db->getFieldData($table));
+
+ $this->keys = array_merge($this->keys, $this->formatKeys($this->db->getIndexData($table)));
+
+ $this->foreignKeys = $this->db->getForeignKeyData($table);
+
+ return $this;
+ }
+
+ /**
+ * Called after `fromTable` and any actions, like `dropColumn`, etc,
+ * to finalize the action. It creates a temp table, creates the new
+ * table with modifications, and copies the data over to the new table.
+ *
+ * @return boolean
+ */
+ public function run(): bool
+ {
+ $this->db->query('PRAGMA foreign_keys = OFF');
+
+ $this->db->transStart();
+
+ $this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
+
+ $this->forge->reset();
+
+ $this->createTable();
+
+ $this->copyData();
+
+ $this->forge->dropTable("temp_{$this->tableName}");
+
+ $success = $this->db->transComplete();
+
+ $this->db->query('PRAGMA foreign_keys = ON');
+
+ return $success;
+ }
+
+ /**
+ * Drops a column from the table.
+ *
+ * @param string $column
+ *
+ * @return \CodeIgniter\Database\SQLite3\Table
+ */
+ public function dropColumn(string $column)
+ {
+ unset($this->fields[$column]);
+
+ return $this;
+ }
+
+ /**
+ * Modifies a field, including changing data type,
+ * renaming, etc.
+ *
+ * @param array $field
+ *
+ * @return \CodeIgniter\Database\SQLite3\Table
+ */
+ public function modifyColumn(array $field)
+ {
+ $field = $field[0];
+
+ $oldName = $field['name'];
+ unset($field['name']);
+
+ $this->fields[$oldName] = $field;
+
+ return $this;
+ }
+
+ /**
+ * Creates the new table based on our current fields.
+ */
+ protected function createTable()
+ {
+ $this->dropIndexes();
+ $this->db->resetDataCache();
+
+ // Handle any modified columns.
+ $fields = [];
+ foreach ($this->fields as $name => $field)
+ {
+ if (isset($field['new_name']))
+ {
+ $fields[$field['new_name']] = $field;
+ continue;
+ }
+
+ $fields[$name] = $field;
+ }
+
+ $this->forge->addField($fields);
+
+ // Unique/Index keys
+ if (is_array($this->keys))
+ {
+ foreach ($this->keys as $key)
+ {
+ switch ($key['type'])
+ {
+ case 'primary':
+ $this->forge->addPrimaryKey($key['fields']);
+ break;
+ case 'unique':
+ $this->forge->addUniqueKey($key['fields']);
+ break;
+ case 'index':
+ $this->forge->addKey($key['fields']);
+ break;
+ }
+ }
+ }
+
+ // Foreign Keys
+
+ return $this->forge->createTable($this->tableName);
+ }
+
+ /**
+ * Copies data from our old table to the new one,
+ * taking care map data correctly based on any columns
+ * that have been renamed.
+ */
+ protected function copyData()
+ {
+ $exFields = [];
+ $newFields = [];
+
+ foreach ($this->fields as $name => $details)
+ {
+ // Are we modifying the column?
+ if (isset($details['new_name']))
+ {
+ $newFields[] = $details['new_name'];
+ }
+ else
+ {
+ $newFields[] = $name;
+ }
+
+ $exFields[] = $name;
+ }
+
+ $exFields = implode(', ', $exFields);
+ $newFields = implode(', ', $newFields);
+
+ $this->db->query("INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}");
+ }
+
+ /**
+ * Converts fields retrieved from the database to
+ * the format needed for creating fields with Forge.
+ *
+ * @param array|boolean $fields
+ *
+ * @return array
+ */
+ protected function formatFields($fields)
+ {
+ if (! is_array($fields))
+ {
+ return $fields;
+ }
+
+ $return = [];
+
+ foreach ($fields as $field)
+ {
+ $return[$field->name] = [
+ 'type' => $field->type,
+ 'default' => $field->default,
+ 'nullable' => $field->nullable,
+ ];
+
+ if ($field->primary_key)
+ {
+ $this->keys[$field->name] = [
+ 'fields' => [$field->name],
+ 'type' => 'primary',
+ ];
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Converts keys retrieved from the database to
+ * the format needed to create later.
+ *
+ * @param $keys
+ *
+ * @return mixed
+ */
+ protected function formatKeys($keys)
+ {
+ if (! is_array($keys))
+ {
+ return $keys;
+ }
+
+ $return = [];
+
+ foreach ($keys as $name => $key)
+ {
+ $return[$name] = [
+ 'fields' => $key->fields,
+ 'type' => 'index',
+ ];
+ }
+
+ return $return;
+ }
+
+ /**
+ * Attempts to drop all indexes and constraints
+ * from the database for this table.
+ */
+ protected function dropIndexes()
+ {
+ if (! is_array($this->keys) || ! count($this->keys))
+ {
+ return;
+ }
+
+ foreach ($this->keys as $name => $key)
+ {
+ if ($key['type'] === 'primary' || $key['type'] === 'unique')
+ {
+ continue;
+ }
+
+ $this->db->query("DROP INDEX IF EXISTS '{$name}'");
+ }
+ }
+}
diff --git a/system/Database/SQLite3/Utils.php b/system/Database/SQLite3/Utils.php
index 23eaec17ab..5de29fb742 100644
--- a/system/Database/SQLite3/Utils.php
+++ b/system/Database/SQLite3/Utils.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php
index 7947d18bd3..eb61cef308 100644
--- a/system/Database/Seeder.php
+++ b/system/Database/Seeder.php
@@ -154,6 +154,9 @@ class Seeder
throw new \InvalidArgumentException('The specified Seeder is not a valid file: ' . $path);
}
+ // Assume the class has the correct namespace
+ $class = APP_NAMESPACE . '\Database\Seeds\\' . $class;
+
if (! class_exists($class, false))
{
require_once $path;
diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php
index 0ea00b6fcc..7942357616 100644
--- a/system/Debug/Exceptions.php
+++ b/system/Debug/Exceptions.php
@@ -37,6 +37,7 @@
*/
use CodeIgniter\API\ResponseTrait;
+use Config\Paths;
/**
* Exceptions manager
@@ -259,7 +260,8 @@ class Exceptions
$path = $this->viewPath;
if (empty($path))
{
- $path = APPPATH . 'Views/errors/';
+ $paths = new Paths();
+ $path = $paths->viewDirectory . '/errors/';
}
$path = is_cli()
diff --git a/system/Debug/Toolbar/Collectors/Logs.php b/system/Debug/Toolbar/Collectors/Logs.php
index 2f6603e22e..5e0c7e9eff 100644
--- a/system/Debug/Toolbar/Collectors/Logs.php
+++ b/system/Debug/Toolbar/Collectors/Logs.php
@@ -84,15 +84,8 @@ class Logs extends BaseCollector
*/
public function display(): array
{
- $logs = $this->collectLogs();
-
- if (empty($logs) || ! is_array($logs))
- {
- $logs = [];
- }
-
return [
- 'logs' => $logs,
+ 'logs' => $this->collectLogs(),
];
}
@@ -131,11 +124,10 @@ class Logs extends BaseCollector
{
if (! is_null($this->data))
{
- return;
+ return $this->data;
}
- $logger = Services::logger(true);
- $this->data = $logger->logCache;
+ return $this->data = Services::logger(true)->logCache ?? [];
}
//--------------------------------------------------------------------
diff --git a/system/Entity.php b/system/Entity.php
index eb257062b2..66e5db20e4 100644
--- a/system/Entity.php
+++ b/system/Entity.php
@@ -1,6 +1,7 @@
_original[$key] === null && $value === null)
+ if ($onlyChanged && ! $this->hasPropertyChanged($key, $value))
{
continue;
}
@@ -192,6 +193,54 @@ class Entity
//--------------------------------------------------------------------
+ /**
+ * Converts the properties of this class into an array. Unlike toArray()
+ * this will not cast the data or use any magic accessors. It simply
+ * returns the raw data for use when saving to the model, etc.
+ *
+ * @param boolean $onlyChanged
+ *
+ * @return array
+ */
+ public function toRawArray(bool $onlyChanged = false): array
+ {
+ $return = [];
+
+ $properties = get_object_vars($this);
+
+ foreach ($properties as $key => $value)
+ {
+ if (substr($key, 0, 1) === '_')
+ {
+ continue;
+ }
+
+ if ($onlyChanged && ! $this->hasPropertyChanged($key, $value))
+ {
+ continue;
+ }
+
+ $return[$key] = $this->$key;
+ }
+
+ return $return;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Checks a property to see if it has changed since the entity was created.
+ *
+ * @param string $key
+ * @param null $value
+ *
+ * @return boolean
+ */
+ protected function hasPropertyChanged(string $key, $value = null)
+ {
+ return ! (($this->_original[$key] === null && $value === null) || $this->_original[$key] === $value);
+ }
+
/**
* Magic method to allow retrieval of protected and private
* class properties either by their name, or through a `getCamelCasedProperty()`
@@ -267,24 +316,37 @@ class Entity
$value = $this->mutateDate($value);
}
- // Array casting requires that we serialize the value
- // when setting it so that it can easily be stored
- // back to the database.
- if (array_key_exists($key, $this->_options['casts']) && $this->_options['casts'][$key] === 'array')
+ $isNullable = false;
+ $castTo = false;
+
+ if (array_key_exists($key, $this->_options['casts']))
{
- $value = serialize($value);
+ $isNullable = substr($this->_options['casts'][$key], 0, 1) === '?';
+ $castTo = $isNullable ? substr($this->_options['casts'][$key], 1) : $this->_options['casts'][$key];
}
- // JSON casting requires that we JSONize the value
- // when setting it so that it can easily be stored
- // back to the database.
- if (function_exists('json_encode') && array_key_exists($key, $this->_options['casts']) && ($this->_options['casts'][$key] === 'json' || $this->_options['casts'][$key] === 'json-array'))
+ if (! $isNullable || ! is_null($value))
{
- $value = json_encode($value);
+ // Array casting requires that we serialize the value
+ // when setting it so that it can easily be stored
+ // back to the database.
+ if ($castTo === 'array')
+ {
+ $value = serialize($value);
+ }
+
+ // JSON casting requires that we JSONize the value
+ // when setting it so that it can easily be stored
+ // back to the database.
+ if (($castTo === 'json' || $castTo === 'json-array') && function_exists('json_encode'))
+ {
+ $value = json_encode($value);
+ }
}
// if a set* method exists for this key,
// use that method to insert this value.
+ // *) should be outside $isNullable check - SO maybe wants to do sth with null value automatically
$method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
if (method_exists($this, $method))
{
@@ -428,13 +490,13 @@ class Entity
protected function castAs($value, string $type)
{
- if(substr($type,0,1) === '?')
+ if (substr($type, 0, 1) === '?')
{
- if($value === null)
+ if ($value === null)
{
return null;
}
- $type = substr($type,1);
+ $type = substr($type, 1);
}
switch($type)
diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php
index da76c9c4f8..ecd98ea045 100644
--- a/system/Exceptions/ConfigException.php
+++ b/system/Exceptions/ConfigException.php
@@ -14,11 +14,6 @@ class ConfigException extends CriticalError
*/
protected $code = 3;
- public static function forMissingMigrationsTable()
- {
- throw new static(lang('Migrations.missingTable'));
- }
-
public static function forInvalidMigrationType(string $type = null)
{
throw new static(lang('Migrations.invalidType', [$type]));
diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php
new file mode 100644
index 0000000000..c9a47d55ed
--- /dev/null
+++ b/system/Filters/CSRF.php
@@ -0,0 +1,100 @@
+isCLI())
+ {
+ return;
+ }
+
+ $security = Services::security();
+
+ try
+ {
+ $security->CSRFVerify($request);
+ }
+ catch (SecurityException $e)
+ {
+ if (config('App')->CSRFRedirect && ! $request->isAJAX())
+ {
+ return redirect()->back()->with('error', $e->getMessage());
+ }
+
+ throw $e;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * We don't have anything to do here.
+ *
+ * @param RequestInterface|\CodeIgniter\HTTP\IncomingRequest $request
+ * @param ResponseInterface|\CodeIgniter\HTTP\Response $response
+ *
+ * @return mixed
+ */
+ public function after(RequestInterface $request, ResponseInterface $response)
+ {
+ }
+
+ //--------------------------------------------------------------------
+}
diff --git a/system/Filters/DebugToolbar.php b/system/Filters/DebugToolbar.php
new file mode 100644
index 0000000000..9f83bb6e35
--- /dev/null
+++ b/system/Filters/DebugToolbar.php
@@ -0,0 +1,74 @@
+prepare();
+ }
+
+ //--------------------------------------------------------------------
+}
diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php
index 2bd2f18350..7b6a41654a 100644
--- a/system/Filters/Filters.php
+++ b/system/Filters/Filters.php
@@ -119,7 +119,7 @@ class Filters
*/
public function run(string $uri, $position = 'before')
{
- $this->initialize($uri);
+ $this->initialize(strtolower($uri));
foreach ($this->filters[$position] as $alias => $rules)
{
@@ -350,7 +350,7 @@ class Filters
foreach ($rules as $path)
{
// Prep it for regex
- $path = str_replace('/*', '*', $path);
+ $path = strtolower(str_replace('/*', '*', $path));
$path = trim(str_replace('*', '.+', $path), '/ ');
// Path doesn't match the URI? continue on...
@@ -388,7 +388,7 @@ class Filters
foreach ($rules as $path)
{
// Prep it for regex
- $path = str_replace('/*', '*', $path);
+ $path = strtolower(str_replace('/*', '*', $path));
$path = trim(str_replace('*', '.+', $path), '/ ');
// Path doesn't match the URI? continue on...
@@ -434,7 +434,7 @@ class Filters
return;
}
- $uri = trim($uri, '/ ');
+ $uri = strtolower(trim($uri, '/ '));
$matches = [];
@@ -446,7 +446,7 @@ class Filters
foreach ($settings['before'] as $path)
{
// Prep it for regex
- $path = str_replace('/*', '*', $path);
+ $path = strtolower(str_replace('/*', '*', $path));
$path = trim(str_replace('*', '.+', $path), '/ ');
if (preg_match('#' . $path . '#', $uri) !== 1)
@@ -467,7 +467,7 @@ class Filters
foreach ($settings['after'] as $path)
{
// Prep it for regex
- $path = str_replace('/*', '*', $path);
+ $path = strtolower(str_replace('/*', '*', $path));
$path = trim(str_replace('*', '.+', $path), '/ ');
if (preg_match('#' . $path . '#', $uri) !== 1)
@@ -479,6 +479,7 @@ class Filters
}
$this->filters['after'] = array_merge($this->filters['after'], $matches);
+ $matches = [];
}
}
}
diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php
new file mode 100644
index 0000000000..e0c55f340a
--- /dev/null
+++ b/system/Filters/Honeypot.php
@@ -0,0 +1,78 @@
+hasContent($request))
+ {
+ throw HoneypotException::isBot();
+ }
+ }
+
+ /**
+ * Attach a honypot to the current response.
+ *
+ * @param CodeIgniter\HTTP\RequestInterface $request
+ * @param CodeIgniter\HTTP\ResponseInterface $response
+ * @return mixed
+ */
+ public function after(RequestInterface $request, ResponseInterface $response)
+ {
+ $honeypot = Services::honeypot(new \Config\Honeypot());
+ $honeypot->attachHoneypot($response);
+ }
+
+}
diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php
index 49a5d8f99e..4406ee42ef 100644
--- a/system/HTTP/ContentSecurityPolicy.php
+++ b/system/HTTP/ContentSecurityPolicy.php
@@ -683,7 +683,7 @@ class ContentSecurityPolicy
$this->styleSrc[] = 'nonce-' . $nonce;
- return "nonce={$nonce}";
+ return "nonce=\"{$nonce}\"";
}, $body
);
@@ -694,7 +694,7 @@ class ContentSecurityPolicy
$this->scriptSrc[] = 'nonce-' . $nonce;
- return "nonce={$nonce}";
+ return "nonce=\"{$nonce}\"";
}, $body
);
@@ -799,12 +799,6 @@ class ContentSecurityPolicy
*/
protected function addToHeader(string $name, $values = null)
{
- if (empty($values))
- {
- $this->tempHeaders[$name] = null;
- return;
- }
-
if (is_string($values))
{
$values = [$values => 0];
diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php
index 2c1ba5ef70..5c61f76301 100644
--- a/system/HTTP/Files/UploadedFile.php
+++ b/system/HTTP/Files/UploadedFile.php
@@ -265,20 +265,20 @@ class UploadedFile extends File implements UploadedFileInterface
*/
public function getErrorString()
{
- static $errors = [
- UPLOAD_ERR_OK => 'The file uploaded with success.',
- UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive.',
- UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.',
- UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.',
- UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
- UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.',
- UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.',
- UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.',
+ $errors = [
+ UPLOAD_ERR_OK => lang('HTTP.uploadErrOk'),
+ UPLOAD_ERR_INI_SIZE => lang('HTTP.uploadErrIniSize'),
+ UPLOAD_ERR_FORM_SIZE => lang('HTTP.uploadErrFormSize'),
+ UPLOAD_ERR_PARTIAL => lang('HTTP.uploadErrPartial'),
+ UPLOAD_ERR_NO_FILE => lang('HTTP.uploadErrNoFile'),
+ UPLOAD_ERR_CANT_WRITE => lang('HTTP.uploadErrCantWrite'),
+ UPLOAD_ERR_NO_TMP_DIR => lang('HTTP.uploadErrNoTmpDir'),
+ UPLOAD_ERR_EXTENSION => lang('HTTP.uploadErrExtension')
];
$error = is_null($this->error) ? UPLOAD_ERR_OK : $this->error;
- return sprintf($errors[$error] ?? 'The file "%s" was not uploaded due to an unknown error.', $this->getName());
+ return sprintf($errors[$error] ?? lang('HTTP.uploadErrUnknown'), $this->getName());
}
//--------------------------------------------------------------------
diff --git a/system/HTTP/Header.php b/system/HTTP/Header.php
index e8e702b385..49ed3c1c97 100644
--- a/system/HTTP/Header.php
+++ b/system/HTTP/Header.php
@@ -91,7 +91,7 @@ class Header
/**
* Gets the raw value of the header. This may return either a string
- * of an array, depending on whether the header has mutliple values or not.
+ * of an array, depending on whether the header has multiple values or not.
*
* @return array|null|string
*/
diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php
index d5f414cb04..27840e604d 100644
--- a/system/HTTP/Request.php
+++ b/system/HTTP/Request.php
@@ -409,7 +409,7 @@ class Request extends Message implements RequestInterface
}
}
- if (empty($value))
+ if (!isset($value))
{
$value = $this->globals[$method][$index] ?? null;
}
diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php
index 9a6576cfc5..28e929dc55 100644
--- a/system/HTTP/URI.php
+++ b/system/HTTP/URI.php
@@ -760,7 +760,7 @@ class URI
// URL Decode the value to protect
// from double-encoding a URL.
// Especially useful with the Pager.
- $parts[$key] = $this->decode($value);
+ $parts[$this->decode($key)] = $this->decode($value);
}
$this->query = $parts;
diff --git a/system/Helpers/date_helper.php b/system/Helpers/date_helper.php
index 7a950883ef..1eebabb609 100644
--- a/system/Helpers/date_helper.php
+++ b/system/Helpers/date_helper.php
@@ -47,7 +47,7 @@ if (! function_exists('now'))
*
* @return integer
*/
- function now(string $timezone = null)
+ function now(string $timezone = null): int
{
$timezone = empty($timezone) ? app_timezone() : $timezone;
diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php
index 5855850e8c..4f21effbc5 100644
--- a/system/Helpers/form_helper.php
+++ b/system/Helpers/form_helper.php
@@ -65,6 +65,12 @@ if (! function_exists('form_open'))
$action = site_url($action);
}
+ if(is_array($attributes) && array_key_exists('csrf_id', $attributes))
+ {
+ $csrfId = $attributes['csrf_id'];
+ unset($attributes['csrf_id']);
+ }
+
$attributes = stringify_attributes($attributes);
if (stripos($attributes, 'method=') === false)
@@ -82,17 +88,16 @@ if (! function_exists('form_open'))
// Add CSRF field if enabled, but leave it out for GET requests and requests to external websites
$before = Services::filters()->getFilters()['before'];
- if ((in_array('csrf', $before) || array_key_exists('csrf', $before)) && strpos($action, base_url()) !== false && ! stripos($form, 'method="get"')
- )
+ if ((in_array('csrf', $before) || array_key_exists('csrf', $before)) && strpos($action, base_url()) !== false && ! stripos($form, 'method="get"'))
{
- $hidden[csrf_token()] = csrf_hash();
+ $form .= csrf_field($csrfId ?? null);
}
if (is_array($hidden))
{
foreach ($hidden as $name => $value)
{
- $form .= ' ' . "\n";
+ $form .= form_hidden($name, $value);
}
}
@@ -167,7 +172,7 @@ if (! function_exists('form_hidden'))
if (! is_array($value))
{
- $form .= ' \n";
+ $form .= ' \n";
}
else
{
diff --git a/system/Helpers/html_helper.php b/system/Helpers/html_helper.php
index e4d8fbefa9..8002bc1f2c 100755
--- a/system/Helpers/html_helper.php
+++ b/system/Helpers/html_helper.php
@@ -156,10 +156,9 @@ if (! function_exists('img'))
$src = ['src' => $src];
}
- //If there is no alt attribute defined, set it to an empty string.
if (! isset($src['alt']))
{
- $src['alt'] = '';
+ $src['alt'] = $attributes['alt'] ?? '';
}
$img = ' ';
}
}
diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php
index 7511cb0497..805f8a9f9e 100644
--- a/system/Helpers/number_helper.php
+++ b/system/Helpers/number_helper.php
@@ -45,7 +45,7 @@ if (! function_exists('number_to_size'))
* @param integer $precision
* @param string $locale
*
- * @return string
+ * @return boolean|string
*/
function number_to_size($num, int $precision = 1, string $locale = null)
{
@@ -178,7 +178,7 @@ if (! function_exists('number_to_currency'))
*
* @return string
*/
- function number_to_currency($num, string $currency, string $locale = null)
+ function number_to_currency($num, string $currency, string $locale = null): string
{
return format_number($num, 1, $locale, [
'type' => NumberFormatter::CURRENCY,
@@ -202,7 +202,7 @@ if (! function_exists('format_number'))
*
* @return string
*/
- function format_number($num, int $precision = 1, string $locale = null, array $options = [])
+ function format_number($num, int $precision = 1, string $locale = null, array $options = []): string
{
// Locale is either passed in here, negotiated with client, or grabbed from our config file.
$locale = $locale ?? \CodeIgniter\Config\Services::request()->getLocale();
@@ -259,14 +259,14 @@ if (! function_exists('number_to_roman'))
*
* @param integer $num it will convert to int
*
- * @return string
+ * @return string|null
*/
function number_to_roman($num)
{
$num = (int) $num;
if ($num < 1 || $num > 3999)
{
- return;
+ return null;
}
$_number_to_roman = function ($num, $th) use (&$_number_to_roman) {
diff --git a/system/Helpers/security_helper.php b/system/Helpers/security_helper.php
index 047ea7face..7e67cf29b7 100644
--- a/system/Helpers/security_helper.php
+++ b/system/Helpers/security_helper.php
@@ -45,7 +45,7 @@ if (! function_exists('sanitize_filename'))
*
* @return string
*/
- function sanitize_filename(string $filename)
+ function sanitize_filename(string $filename): string
{
return Services::security()->sanitizeFilename($filename);
}
@@ -61,7 +61,7 @@ if (! function_exists('strip_image_tags'))
* @param string $str
* @return string
*/
- function strip_image_tags(string $str)
+ function strip_image_tags(string $str): string
{
return preg_replace([
'# #i',
diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php
index 962f4d2ceb..52f61b2afc 100755
--- a/system/Helpers/text_helper.php
+++ b/system/Helpers/text_helper.php
@@ -150,7 +150,7 @@ if (! function_exists('ascii_to_entities'))
{
/*
If the $temp array has a value but we have moved on, then it seems only
- fair that we output that entity and restart $temp before continuing. -Paul
+ fair that we output that entity and restart $temp before continuing.
*/
if (count($temp) === 1)
{
@@ -281,7 +281,7 @@ if (! function_exists('word_censor'))
// \w, \b and a few others do not match on a unicode character
// set for performance reasons. As a result words like über
// will not match on a word boundary. Instead, we'll assume that
- // a bad word will be bookeneded by any of these characters.
+ // a bad word will be bookended by any of these characters.
$delim = '[-_\'\"`(){}<>\[\]|!?@#%&,.:;^~*+=\/ 0-9\n\r\t]';
foreach ($censored as $badword)
@@ -402,7 +402,7 @@ if (! function_exists('highlight_phrase'))
*
* @param string $str the text string
* @param string $phrase the phrase you'd like to highlight
- * @param string $tag_open the openging tag to precede the phrase with
+ * @param string $tag_open the opening tag to precede the phrase with
* @param string $tag_close the closing tag to end the phrase with
*
* @return string
@@ -460,7 +460,7 @@ if (! function_exists('word_wrap'))
* Anything placed between {unwrap}{/unwrap} will not be word wrapped, nor
* will URLs.
*
- * @param string $str the text string
+ * @param string $str the text string
* @param integer $charlim = 76 the number of characters to wrap at
*
* @return string
@@ -604,9 +604,9 @@ if (! function_exists('strip_slashes'))
*
* Removes slashes contained in a string or in an array
*
- * @param mixed string or array
+ * @param mixed $str string or array
*
- * @return mixed string or array
+ * @return mixed string or array
*/
function strip_slashes($str)
{
@@ -632,7 +632,7 @@ if (! function_exists('strip_quotes'))
*
* Removes single and double quotes from a string
*
- * @param string
+ * @param string $str
*
* @return string
*/
@@ -651,7 +651,7 @@ if (! function_exists('quotes_to_entities'))
*
* Converts single and double quotes to entities
*
- * @param string
+ * @param string $str
*
* @return string
*/
@@ -677,7 +677,7 @@ if (! function_exists('reduce_double_slashes'))
*
* http://www.some-site.com/index.php
*
- * @param string
+ * @param string $str
*
* @return string
*/
@@ -712,7 +712,7 @@ if (! function_exists('reduce_multiples'))
{
$str = preg_replace('#' . preg_quote($character, '#') . '{2,}#', $character, $str);
- return ($trim === true) ? trim($str, $character) : $str;
+ return ($trim) ? trim($str, $character) : $str;
}
}
@@ -814,7 +814,7 @@ if (! function_exists('alternator'))
$args = func_get_args();
- return $args[($i ++ % count($args))];
+ return $args[($i++ % count($args))];
}
}
@@ -829,13 +829,13 @@ if (! function_exists('excerpt'))
*
* @param string $text String to search the phrase
* @param string $phrase Phrase that will be searched for.
- * @param integer $radius The amount of characters returned arround the phrase.
+ * @param integer $radius The amount of characters returned around the phrase.
* @param string $ellipsis Ending that will be appended
*
* @return string
*
* If no $phrase is passed, will generate an excerpt of $radius characters
- * from the begining of $text.
+ * from the beginning of $text.
*/
function excerpt(string $text, string $phrase = null, int $radius = 100, string $ellipsis = '...'): string
{
diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php
index 399f0f5c63..4fbaaaedc7 100644
--- a/system/Helpers/url_helper.php
+++ b/system/Helpers/url_helper.php
@@ -334,7 +334,7 @@ if (! function_exists('mailto'))
*/
function mailto($email, string $title = '', $attributes = ''): string
{
- if ($title === '')
+ if (trim($title) === '')
{
$title = $email;
}
@@ -360,7 +360,7 @@ if (! function_exists('safe_mailto'))
*/
function safe_mailto($email, string $title = '', $attributes = ''): string
{
- if ($title === '')
+ if (trim($title) === '')
{
$title = $email;
}
diff --git a/system/Helpers/xml_helper.php b/system/Helpers/xml_helper.php
index 183c3c2c6a..3848eced9f 100644
--- a/system/Helpers/xml_helper.php
+++ b/system/Helpers/xml_helper.php
@@ -73,7 +73,7 @@ if (! function_exists('xml_convert'))
''',
'-',
];
- $str = str_replace($original, $replacements, $str);
+ $str = str_replace($original, $replacement, $str);
// Decode the temp markers back to entities
$str = preg_replace('/' . $temp . '(\d+);/', '\\1;', $str);
diff --git a/system/Language/Language.php b/system/Language/Language.php
index adef1046ed..b5b261b357 100644
--- a/system/Language/Language.php
+++ b/system/Language/Language.php
@@ -190,17 +190,6 @@ class Language
*/
protected function parseLine(string $line, string $locale): array
{
- // If there's no possibility of a filename being in the string
- // simply return the string, and they can parse the replacement
- // without it being in a file.
- if (strpos($line, '.') === false)
- {
- return [
- null,
- $line,
- ];
- }
-
$file = substr($line, 0, strpos($line, '.'));
$line = substr($line, strlen($file) + 1);
diff --git a/system/Language/en/Database.php b/system/Language/en/Database.php
index d3241b92a9..d42d39b6a1 100644
--- a/system/Language/en/Database.php
+++ b/system/Language/en/Database.php
@@ -24,4 +24,5 @@ return [
'failGetForeignKeyData' => 'Failed to get foreign key data from database.',
'parseStringFail' => 'Parsing key string failed.',
'featureUnavailable' => 'This feature is not available for the database you are using.',
+ 'tableNotFound' => 'Table `{0}` was not found in the current database.',
];
diff --git a/system/Language/en/Email.php b/system/Language/en/Email.php
deleted file mode 100644
index b14aec01be..0000000000
--- a/system/Language/en/Email.php
+++ /dev/null
@@ -1,37 +0,0 @@
- 'The email validation method must be passed an array.',
- '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.',
- 'sendFailureSendmail' => 'Unable to send email using PHP Sendmail. Your server might not be configured to send mail using this method.',
- 'sendFailureSmtp' => 'Unable to send email using PHP SMTP. Your server might not be configured to send mail using this method.',
- '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.',
- '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}',
- '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/HTTP.php b/system/Language/en/HTTP.php
index 9836903020..eb0b29cdd4 100644
--- a/system/Language/en/HTTP.php
+++ b/system/Language/en/HTTP.php
@@ -64,4 +64,14 @@ return [
'alreadyMoved' => 'The uploaded file has already been moved.',
'invalidFile' => 'The original file is not a valid file.',
'moveFailed' => 'Could not move file {0} to {1} ({2})',
+
+ 'uploadErrOk' => 'The file uploaded with success.',
+ 'uploadErrIniSize' => 'The file "%s" exceeds your upload_max_filesize ini directive.',
+ 'uploadErrFormSize' => 'The file "%s" exceeds the upload limit defined in your form.',
+ 'uploadErrPartial' => 'The file "%s" was only partially uploaded.',
+ 'uploadErrNoFile' => 'No file was uploaded.',
+ 'uploadErrCantWrite' => 'The file "%s" could not be written on disk.',
+ 'uploadErrNoTmpDir' => 'File could not be uploaded: missing temporary directory.',
+ 'uploadErrExtension' => 'File upload was stopped by a PHP extension.',
+ 'uploadErrUnknown' => 'The file "%s" was not uploaded due to an unknown error.'
];
diff --git a/system/Language/en/View.php b/system/Language/en/View.php
index 46aa94cd7b..8f179ee56b 100644
--- a/system/Language/en/View.php
+++ b/system/Language/en/View.php
@@ -14,7 +14,7 @@
*/
return [
- 'invalidCellMethod' => '{class}::{method} is not a valid method."',
+ 'invalidCellMethod' => '{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.',
diff --git a/system/Log/Handlers/FileHandler.php b/system/Log/Handlers/FileHandler.php
index 0e022bdfc8..f65f73badc 100644
--- a/system/Log/Handlers/FileHandler.php
+++ b/system/Log/Handlers/FileHandler.php
@@ -36,6 +36,8 @@
* @filesource
*/
+use CodeIgniter\Log\Exceptions\LogException;
+
/**
* Log error messages to file system
*/
@@ -138,7 +140,10 @@ class FileHandler extends BaseHandler implements HandlerInterface
{
if (($result = fwrite($fp, substr($msg, $written))) === false)
{
+ // if we get this far, we'll never see this during travis-ci
+ // @codeCoverageIgnoreStart
break;
+ // @codeCoverageIgnoreEnd
}
}
diff --git a/system/Log/Logger.php b/system/Log/Logger.php
index aec6adbde2..075318305a 100644
--- a/system/Log/Logger.php
+++ b/system/Log/Logger.php
@@ -511,7 +511,7 @@ class Logger implements LoggerInterface
* Cleans the paths of filenames by replacing APPPATH, SYSTEMPATH, FCPATH
* with the actual var. i.e.
*
- * /var/www/site/application/Controllers/Home.php
+ * /var/www/site/app/Controllers/Home.php
* becomes:
* APPPATH/Controllers/Home.php
*
diff --git a/system/Model.php b/system/Model.php
index 8f62ab84d3..6e31d2887f 100644
--- a/system/Model.php
+++ b/system/Model.php
@@ -149,7 +149,7 @@ class Model
//--------------------------------------------------------------------
/**
- * The column used for insert timestampes
+ * The column used for insert timestamps
*
* @var string
*/
@@ -343,8 +343,6 @@ class Model
$this->tempReturnType = $this->returnType;
$this->tempUseSoftDeletes = $this->useSoftDeletes;
- $this->reset();
-
return $row['data'];
}
@@ -454,6 +452,7 @@ class Model
* @param array|object $data
*
* @return boolean
+ * @throws \ReflectionException
*/
public function save($data)
{
@@ -462,7 +461,12 @@ class Model
// them as an array.
if (is_object($data) && ! $data instanceof \stdClass)
{
- $data = static::classToArray($data, $this->dateFormat);
+ $data = static::classToArray($data, $this->primaryKey, $this->dateFormat);
+ }
+
+ if (empty($data))
+ {
+ return true;
}
if (is_object($data) && isset($data->{$this->primaryKey}))
@@ -481,23 +485,28 @@ class Model
return $response;
}
- //--------------------------------------------------------------------
-
/**
* Takes a class an returns an array of it's public and protected
* properties as an array suitable for use in creates and updates.
*
* @param string|object $data
+ * @param string|null $primaryKey
* @param string $dateFormat
*
* @return array
* @throws \ReflectionException
*/
- public static function classToArray($data, string $dateFormat = 'datetime'): array
+ public static function classToArray($data, $primaryKey = null, string $dateFormat = 'datetime'): array
{
- if (method_exists($data, 'toArray'))
+ if (method_exists($data, 'toRawArray'))
{
- $properties = $data->toArray(true, false);
+ $properties = $data->toRawArray(true);
+
+ // Always grab the primary key otherwise updates will fail.
+ if (! empty($properties) && ! empty($primaryKey) && ! in_array($primaryKey, $properties))
+ {
+ $properties[$primaryKey] = $data->{$primaryKey};
+ }
}
else
{
@@ -569,6 +578,7 @@ class Model
* @param boolean $returnID Whether insert ID should be returned or not.
*
* @return integer|string|boolean
+ * @throws \ReflectionException
*/
public function insert($data = null, bool $returnID = true)
{
@@ -588,7 +598,7 @@ class Model
// them as an array.
if (is_object($data) && ! $data instanceof \stdClass)
{
- $data = static::classToArray($data, $this->dateFormat);
+ $data = static::classToArray($data, $this->primaryKey, $this->dateFormat);
}
// If it's still a stdClass, go ahead and convert to
@@ -689,6 +699,7 @@ class Model
* @param array|object $data
*
* @return boolean
+ * @throws \ReflectionException
*/
public function update($id = null, $data = null)
{
@@ -711,7 +722,7 @@ class Model
// them as an array.
if (is_object($data) && ! $data instanceof \stdClass)
{
- $data = static::classToArray($data, $this->dateFormat);
+ $data = static::classToArray($data, $this->primaryKey, $this->dateFormat);
}
// If it's still a stdClass, go ahead and convert to
@@ -994,7 +1005,7 @@ class Model
throw DataException::forEmptyDataset('chunk');
}
- $rows = $rows->getResult();
+ $rows = $rows->getResult($this->tempReturnType);
$offset += $size;
@@ -1254,27 +1265,63 @@ class Model
$data = (array) $data;
}
+ $rules = $this->validationRules;
+
// ValidationRules can be either a string, which is the group name,
// or an array of rules.
- if (is_string($this->validationRules))
+ if (is_string($rules))
{
- $valid = $this->validation->run($data, $this->validationRules, $this->DBGroup);
+ $rules = $this->validation->loadRuleGroup($rules);
}
- else
- {
- // 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, null, $this->DBGroup);
+ $rules = $this->cleanValidationRules($rules, $data);
+
+ // If no data existed that needs validation
+ // our job is done here.
+ if (empty($rules))
+ {
+ return true;
}
+ // Replace any placeholders (i.e. {id}) in the rules with
+ // the value found in $data, if exists.
+ $rules = $this->fillPlaceholders($rules, $data);
+
+ $this->validation->setRules($rules, $this->validationMessages);
+ $valid = $this->validation->run($data, null, $this->DBGroup);
+
return (bool) $valid;
}
//--------------------------------------------------------------------
+ /**
+ * Removes any rules that apply to fields that have not been set
+ * currently so that rules don't block updating when only updating
+ * a partial row.
+ *
+ * @param array $rules
+ *
+ * @return array
+ */
+ protected function cleanValidationRules($rules, array $data = null)
+ {
+ if (empty($data))
+ {
+ return [];
+ }
+
+ foreach ($rules as $field => $rule)
+ {
+ if (! array_key_exists($field, $data))
+ {
+ unset($rules[$field]);
+ }
+ }
+
+ return $rules;
+ }
+
/**
* Replace any placeholders within the rules with the values that
* match the 'key' of any properties being set. For example, if
@@ -1312,6 +1359,13 @@ class Model
{
foreach ($rule as &$row)
{
+ // Should only be an `errors` array
+ // which doesn't take placeholders.
+ if (is_array($row))
+ {
+ continue;
+ }
+
$row = strtr($row, $replacements);
}
continue;
@@ -1436,7 +1490,7 @@ class Model
*/
public function __get(string $name)
{
- if (in_array($name, ['primaryKey', 'table', 'returnType']))
+ if (in_array($name, ['primaryKey', 'table', 'returnType', 'DBGroup']))
{
return $this->{$name};
}
diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php
index 63cd32b6fc..dbe44facd7 100644
--- a/system/Router/RouteCollection.php
+++ b/system/Router/RouteCollection.php
@@ -223,7 +223,7 @@ class RouteCollection implements RouteCollectionInterface
* Constructor
*
* @param FileLocator $locator
- * @param Config/Modules $moduleConfig
+ * @param \Config\Modules $moduleConfig
*/
public function __construct(FileLocator $locator, $moduleConfig)
{
@@ -393,7 +393,6 @@ class RouteCollection implements RouteCollectionInterface
/**
* Will attempt to discover any additional routes, either through
* the local PSR4 namespaces, or through selected Composer packages.
- * (Composer coming soon...)
*/
protected function discoverRoutes()
{
@@ -406,9 +405,6 @@ class RouteCollection implements RouteCollectionInterface
// so route files can access it.
$routes = $this;
- /*
- * Discover Local Files
- */
if ($this->moduleConfig->shouldDiscover('routes'))
{
$files = $this->fileLocator->search('Config/Routes.php');
@@ -425,10 +421,6 @@ class RouteCollection implements RouteCollectionInterface
}
}
- /*
- * Discover Composer files (coming soon)
- */
-
$this->didDiscover = true;
}
@@ -871,17 +863,17 @@ class RouteCollection implements RouteCollectionInterface
$this->delete($name . '/' . $id, $new_name . '::delete/$1', $options);
}
- // Web Safe?
+ // Web Safe? delete needs checking before update because of method name
if (isset($options['websafe']))
{
- if (in_array('update', $methods))
- {
- $this->post($name . '/' . $id, $new_name . '::update/$1', $options);
- }
if (in_array('delete', $methods))
{
$this->post($name . '/' . $id . '/delete', $new_name . '::delete/$1', $options);
}
+ if (in_array('update', $methods))
+ {
+ $this->post($name . '/' . $id, $new_name . '::update/$1', $options);
+ }
}
return $this;
@@ -1240,7 +1232,8 @@ class RouteCollection implements RouteCollectionInterface
*/
protected function create(string $verb, string $from, $to, array $options = null)
{
- $prefix = is_null($this->group) ? '' : $this->group . '/';
+ $overwrite = false;
+ $prefix = is_null($this->group) ? '' : $this->group . '/';
$from = filter_var($prefix . $from, FILTER_SANITIZE_STRING);
@@ -1257,10 +1250,12 @@ class RouteCollection implements RouteCollectionInterface
if (! empty($options['hostname']))
{
// @todo determine if there's a way to whitelist hosts?
- if (strtolower($_SERVER['HTTP_HOST']) !== strtolower($options['hostname']))
+ if (isset($_SERVER['HTTP_HOST']) && strtolower($_SERVER['HTTP_HOST']) !== strtolower($options['hostname']))
{
return;
}
+
+ $overwrite = true;
}
// Limiting to subdomains?
@@ -1272,6 +1267,8 @@ class RouteCollection implements RouteCollectionInterface
{
return;
}
+
+ $overwrite = true;
}
// Are we offsetting the binds?
@@ -1316,11 +1313,11 @@ class RouteCollection implements RouteCollectionInterface
$name = $options['as'] ?? $from;
// Don't overwrite any existing 'froms' so that auto-discovered routes
- // do not overwrite any application/Config/Routes settings. The app
+ // do not overwrite any app/Config/Routes settings. The app
// routes should always be the "source of truth".
// this works only because discovered routes are added just prior
// to attempting to route the request.
- if (isset($this->routes[$verb][$name]))
+ if (isset($this->routes[$verb][$name]) && ! $overwrite)
{
return;
}
@@ -1350,6 +1347,12 @@ class RouteCollection implements RouteCollectionInterface
*/
private function checkSubdomains($subdomains)
{
+ // CLI calls can't be on subdomain.
+ if (! isset($_SERVER['HTTP_HOST']))
+ {
+ return false;
+ }
+
if (is_null($this->currentSubdomain))
{
$this->currentSubdomain = $this->determineCurrentSubdomain();
diff --git a/system/Router/Router.php b/system/Router/Router.php
index b865936882..ae674fc4c3 100644
--- a/system/Router/Router.php
+++ b/system/Router/Router.php
@@ -469,7 +469,7 @@ class Router implements RouterInterface
{
$val = preg_replace('#^' . $key . '$#', $val, $uri);
}
- elseif (strpos($key, '/') !== false)
+ elseif (strpos($val, '/') !== false)
{
$val = str_replace('/', '\\', $val);
}
@@ -561,6 +561,8 @@ class Router implements RouterInterface
*/
protected function validateRequest(array $segments)
{
+ $segments = array_filter($segments);
+
$c = count($segments);
$directory_override = isset($this->directory);
@@ -571,8 +573,7 @@ class Router implements RouterInterface
$test = $this->directory . ucfirst($this->translateURIDashes === true ? str_replace('-', '_', $segments[0]) : $segments[0]
);
- if (! is_file(APPPATH . 'Controllers/' . $test . '.php') && $directory_override === false && is_dir(APPPATH . 'Controllers/' . $this->directory . ucfirst($segments[0]))
- )
+ if (! is_file(APPPATH . 'Controllers/' . $test . '.php') && $directory_override === false && is_dir(APPPATH . 'Controllers/' . $this->directory . ucfirst($segments[0])))
{
$this->setDirectory(array_shift($segments), true);
continue;
diff --git a/system/Session/Handlers/BaseHandler.php b/system/Session/Handlers/BaseHandler.php
index 8154a8ebde..7605b4fa98 100644
--- a/system/Session/Handlers/BaseHandler.php
+++ b/system/Session/Handlers/BaseHandler.php
@@ -209,5 +209,4 @@ abstract class BaseHandler implements \SessionHandlerInterface
return false;
}
-
}
diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php
index 5604cfd34c..d14c01df90 100644
--- a/system/Session/Handlers/FileHandler.php
+++ b/system/Session/Handlers/FileHandler.php
@@ -73,6 +73,16 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
*/
protected $fileNew;
+ /**
+ * @var boolean
+ */
+ protected $matchIP = false;
+
+ /**
+ * @var string
+ */
+ protected $sessionIDRegex;
+
//--------------------------------------------------------------------
/**
@@ -91,14 +101,19 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
}
else
{
- $sessionPath = rtrim(ini_get('session.save_path'), '/\\');
+ $sessionPath = rtrim(ini_get('session.save_path'), '/\\');
+
if (! $sessionPath)
- {
+ {
$sessionPath = WRITEPATH . 'session';
}
- $this->savePath = $sessionPath;
+ $this->savePath = $sessionPath;
}
+
+ $this->matchIP = $config->sessionMatchIP;
+
+ $this->configureSessionIDRegex();
}
//--------------------------------------------------------------------
@@ -130,8 +145,8 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
$this->savePath = $savePath;
$this->filePath = $this->savePath . '/'
- . $name // we'll use the session cookie name as a prefix to avoid collisions
- . ($this->matchIP ? md5($this->ipAddress) : '');
+ . $name // we'll use the session cookie name as a prefix to avoid collisions
+ . ($this->matchIP ? md5($this->ipAddress) : '');
return true;
}
@@ -219,7 +234,7 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
{
// If the two IDs don't match, we have a session_regenerate_id() call
// and we need to close the old handle and open a new one
- if ($sessionID !== $this->sessionID && ( ! $this->close() || $this->read($sessionID) === false))
+ if ($sessionID !== $this->sessionID && (! $this->close() || $this->read($sessionID) === false))
{
return false;
}
@@ -302,13 +317,15 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
{
if ($this->close())
{
- return is_file($this->filePath . $session_id) ? (unlink($this->filePath . $session_id) && $this->destroyCookie()) : true;
+ return is_file($this->filePath . $session_id)
+ ? (unlink($this->filePath . $session_id) && $this->destroyCookie()) : true;
}
elseif ($this->filePath !== null)
{
clearstatcache();
- return is_file($this->filePath . $session_id) ? (unlink($this->filePath . $session_id) && $this->destroyCookie()) : true;
+ return is_file($this->filePath . $session_id)
+ ? (unlink($this->filePath . $session_id) && $this->destroyCookie()) : true;
}
return false;
@@ -336,20 +353,28 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
$ts = time() - $maxlifetime;
+ $pattern = $this->matchIP === true
+ ? '[0-9a-f]{32}'
+ : '';
+
$pattern = sprintf(
- '/^%s[0-9a-f]{%d}$/', preg_quote($this->cookieName, '/'), ($this->matchIP === true ? 72 : 40)
+ '#\A%s' . $pattern . $this->sessionIDRegex . '\z#',
+ preg_quote($this->cookieName)
);
while (($file = readdir($directory)) !== false)
{
// If the filename doesn't match this pattern, it's either not a session file or is not ours
- if (! preg_match($pattern, $file) || ! is_file($this->savePath . '/' . $file) || ($mtime = filemtime($this->savePath . '/' . $file)) === false || $mtime > $ts
+ if (! preg_match($pattern, $file)
+ || ! is_file($this->savePath . DIRECTORY_SEPARATOR . $file)
+ || ($mtime = filemtime($this->savePath . DIRECTORY_SEPARATOR . $file)) === false
+ || $mtime > $ts
)
{
continue;
}
- unlink($this->savePath . '/' . $file);
+ unlink($this->savePath . DIRECTORY_SEPARATOR . $file);
}
closedir($directory);
@@ -358,4 +383,36 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface
}
//--------------------------------------------------------------------
+
+ /**
+ * Configure Session ID regular expression
+ */
+ protected function configureSessionIDRegex()
+ {
+ $bitsPerCharacter = (int)ini_get('session.sid_bits_per_character');
+ $SIDLength = (int)ini_get('session.sid_length');
+
+ if (($bits = $SIDLength * $bitsPerCharacter) < 160)
+ {
+ // Add as many more characters as necessary to reach at least 160 bits
+ $SIDLength += (int)ceil((160 % $bits) / $bitsPerCharacter);
+ ini_set('session.sid_length', $SIDLength);
+ }
+
+ // Yes, 4,5,6 are the only known possible values as of 2016-10-27
+ switch ($bitsPerCharacter)
+ {
+ case 4:
+ $this->sessionIDRegex = '[0-9a-f]';
+ break;
+ case 5:
+ $this->sessionIDRegex = '[0-9a-v]';
+ break;
+ case 6:
+ $this->sessionIDRegex = '[0-9a-zA-Z,-]';
+ break;
+ }
+
+ $this->sessionIDRegex .= '{' . $SIDLength . '}';
+ }
}
diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php
index c40a22c2b4..d5c9c110c2 100644
--- a/system/Session/Handlers/MemcachedHandler.php
+++ b/system/Session/Handlers/MemcachedHandler.php
@@ -94,6 +94,11 @@ class MemcachedHandler extends BaseHandler implements \SessionHandlerInterface
{
$this->keyPrefix .= $this->ipAddress . ':';
}
+
+ if(!empty($this->keyPrefix))
+ {
+ ini_set('memcached.sess_prefix', $this->keyPrefix);
+ }
$this->sessionExpiration = $config->sessionExpiration;
}
@@ -184,7 +189,7 @@ class MemcachedHandler extends BaseHandler implements \SessionHandlerInterface
return $session_data;
}
- return false;
+ return '';
}
//--------------------------------------------------------------------
diff --git a/system/Session/Session.php b/system/Session/Session.php
index a542c998d2..3780c2e0f3 100644
--- a/system/Session/Session.php
+++ b/system/Session/Session.php
@@ -42,7 +42,7 @@ use Psr\Log\LoggerAwareTrait;
* Implementation of CodeIgniter session container.
*
* Session configuration is done through session variables and cookie related
- * variables in application/config/App.php
+ * variables in app/config/App.php
*/
class Session implements SessionInterface
{
@@ -52,7 +52,7 @@ class Session implements SessionInterface
/**
* Instance of the driver to use.
*
- * @var HandlerInterface
+ * @var \CodeIgniter\Log\Handlers\HandlerInterface
*/
protected $driver;
@@ -303,6 +303,11 @@ class Session implements SessionInterface
{
ini_set('session.gc_maxlifetime', (int) $this->sessionExpiration);
}
+
+ if(!empty($this->sessionSavePath))
+ {
+ ini_set('session.save_path', $this->sessionSavePath);
+ }
// Security is king
ini_set('session.use_trans_sid', 0);
diff --git a/tests/_support/Helpers/ControllerResponse.php b/system/Test/ControllerResponse.php
similarity index 62%
rename from tests/_support/Helpers/ControllerResponse.php
rename to system/Test/ControllerResponse.php
index 4c64a524a0..393fb879c0 100644
--- a/tests/_support/Helpers/ControllerResponse.php
+++ b/system/Test/ControllerResponse.php
@@ -1,6 +1,41 @@
-request))
{
- $this->request = new IncomingRequest($this->appConfig, $this->uri, $this->body);
+ $this->request = new IncomingRequest($this->appConfig, $this->uri, $this->body, new UserAgent());
}
if (empty($this->response))
@@ -73,7 +110,7 @@ trait ControllerTester
* @param string $method
* @param array $params
*
- * @return \Tests\Support\Helpers\ControllerResponse
+ * @return \CodeIgniter\Test\ControllerResponse|\InvalidArgumentException
*/
public function execute(string $method, ...$params)
{
diff --git a/tests/_support/DOM/DOMParser.php b/system/Test/DOMParser.php
similarity index 78%
rename from tests/_support/DOM/DOMParser.php
rename to system/Test/DOMParser.php
index 3a190faf43..55bd10eff9 100644
--- a/tests/_support/DOM/DOMParser.php
+++ b/system/Test/DOMParser.php
@@ -1,4 +1,40 @@
-required($data[$str] ?? null);
+ $present = $this->required($str ?? '');
- if ($present === true)
+ if ($present)
{
return true;
}
@@ -356,9 +356,9 @@ class Rules
// If the field is present we can safely assume that
// the field is here, no matter whether the corresponding
// search field is present or not.
- $present = $this->required($data[$str] ?? null);
+ $present = $this->required($str ?? '');
- if ($present === true)
+ if ($present)
{
return true;
}
diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php
index 56f602bd5b..864736a8ea 100644
--- a/system/Validation/Validation.php
+++ b/system/Validation/Validation.php
@@ -373,22 +373,21 @@ class Validation implements ValidationInterface
*/
public function setRules(array $rules, array $errors = []): ValidationInterface
{
- $this->rules = $rules;
+ $this->customErrors = $errors;
- if (empty($errors))
+ foreach ($rules as $field => &$rule)
{
- foreach ($rules as $field => $setup)
+ if (is_array($rule))
{
- if (isset($setup['errors']))
+ if (array_key_exists('errors', $rule))
{
- $this->customErrors[$field] = $setup['errors'];
+ $this->customErrors[$field] = $rule['errors'];
+ unset($rule['errors']);
}
}
}
- else
- {
- $this->customErrors = $errors;
- }
+
+ $this->rules = $rules;
return $this;
}
@@ -540,12 +539,14 @@ class Validation implements ValidationInterface
* for {group}_errors for an array of custom error messages.
*
* @param string|null $group
+ *
+ * @return array|ValidationException|null
*/
- protected function loadRuleGroup(string $group = null)
+ public function loadRuleGroup(string $group = null)
{
if (empty($group))
{
- return;
+ return null;
}
if (! isset($this->config->$group))
@@ -568,6 +569,8 @@ class Validation implements ValidationInterface
{
$this->customErrors = $this->config->$errorName;
}
+
+ return $this->rules;
}
//--------------------------------------------------------------------
diff --git a/system/View/Filters.php b/system/View/Filters.php
index 22edbd258c..d626b545db 100644
--- a/system/View/Filters.php
+++ b/system/View/Filters.php
@@ -309,7 +309,7 @@ class Filters
*
* @return string
*/
- public static function round($value, $precision = 2, $type = 'common')
+ public static function round($value, $precision = 2, $type = 'common'): string
{
if (! is_numeric($precision))
{
diff --git a/system/View/Parser.php b/system/View/Parser.php
index 06adf83530..b6de221c17 100644
--- a/system/View/Parser.php
+++ b/system/View/Parser.php
@@ -530,7 +530,7 @@ class Parser extends View
extract($this->data);
try
{
- $result = eval('?>' . $template . '' . $template . 'sections, and no other valid output
+ // is allowed in $output so we'll overwrite it.
+ if (! is_null($this->layout))
+ {
+ $layoutView = $this->layout;
+ $this->layout = null;
+ $output = $this->render($layoutView, $options, $saveData);
+ }
+
$this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']);
if (CI_DEBUG && (! isset($options['debug']) || $options['debug'] === true))
@@ -376,6 +409,82 @@ class View implements RendererInterface
//--------------------------------------------------------------------
+ /**
+ * Specifies that the current view should extend an existing layout.
+ *
+ * @param string $layout
+ *
+ * @return void
+ */
+ public function extend(string $layout)
+ {
+ $this->layout = $layout;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Starts holds content for a section within the layout.
+ *
+ * @param string $name
+ */
+ public function section(string $name)
+ {
+ $this->currentSection = $name;
+
+ ob_start();
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ *
+ *
+ * @throws \Zend\Escaper\Exception\RuntimeException
+ */
+ public function endSection()
+ {
+ $contents = ob_get_clean();
+
+ if (empty($this->currentSection))
+ {
+ throw new \RuntimeException('View themes, no current section.');
+ }
+
+ // Ensure an array exists so we can store multiple entries for this.
+ if (! array_key_exists($this->currentSection, $this->sections))
+ {
+ $this->sections[$this->currentSection] = [];
+ }
+ $this->sections[$this->currentSection][] = $contents;
+
+ $this->currentSection = null;
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * Renders a section's contents.
+ *
+ * @param string $sectionName
+ */
+ public function renderSection(string $sectionName)
+ {
+ if (! isset($this->sections[$sectionName]))
+ {
+ echo '';
+
+ return;
+ }
+
+ foreach ($this->sections[$sectionName] as $contents)
+ {
+ echo $contents;
+ }
+ }
+
+ //--------------------------------------------------------------------
+
/**
* Returns the performance data that might have been collected
* during the execution. Used primarily in the Debug Toolbar.
diff --git a/system/bootstrap.php b/system/bootstrap.php
index 5222941cee..320deac586 100644
--- a/system/bootstrap.php
+++ b/system/bootstrap.php
@@ -91,7 +91,10 @@ if (! defined('TESTPATH'))
* GRAB OUR CONSTANTS & COMMON
* ---------------------------------------------------------------
*/
-require_once APPPATH . 'Config/Constants.php';
+if (! defined('APP_NAMESPACE'))
+{
+ require_once APPPATH . 'Config/Constants.php';
+}
require_once SYSTEMPATH . 'Common.php';
@@ -105,8 +108,13 @@ require_once SYSTEMPATH . 'Common.php';
* that the config files can use the path constants.
*/
+if (! class_exists(Config\Autoload::class, false))
+{
+ require_once APPPATH . 'Config/Autoload.php';
+ require_once APPPATH . 'Config/Modules.php';
+}
+
require_once SYSTEMPATH . 'Autoloader/Autoloader.php';
-require_once APPPATH . 'Config/Autoload.php';
require_once SYSTEMPATH . 'Config/BaseService.php';
require_once APPPATH . 'Config/Services.php';
@@ -117,7 +125,7 @@ if (! class_exists('CodeIgniter\Services', false))
}
$loader = CodeIgniter\Services::autoloader();
-$loader->initialize(new Config\Autoload());
+$loader->initialize(new Config\Autoload(), new Config\Modules());
$loader->register(); // Register the loader with the SPL autoloader stack.
// Now load Composer's if it's available
diff --git a/tests/README.md b/tests/README.md
index 0856e484fb..9ece18fc26 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,22 +1,38 @@
# Running System Tests
-This is the preliminary quick-start to CodeIgniter testing. Its intent is to describe what it takes to get your system setup and ready to run the system tests. It is not intended to be a full description of the test features that you can use to test your application, since that can be found in the documentation.
+This is the quick-start to CodeIgniter testing. Its intent is to describe what
+it takes to get your system setup and ready to run the system tests.
+It is not intended to be a full description of the test features that you can
+use to test your application, since that can be found in the documentation.
## Requirements
-It is recommended to use the latest version of PHPUnit. At the time of this writing we are running version 5.3. Support for this has been built into the **composer.json** file that ships with CodeIgniter, and can easily be installed via [Composer](https://getcomposer.org/) if you don't already have it installed globally.
+It is recommended to use the latest version of PHPUnit. At the time of this
+writing we are running version 7.5.1. Support for this has been built into the
+**composer.json** file that ships with CodeIgniter, and can easily be installed
+via [Composer](https://getcomposer.org/) if you don't already have it installed globally.
> composer install
-If running under OS X or Linux, you will want to create a symbolic link to make running tests a touch nicer.
+If running under OS X or Linux, you can create a symbolic link to make running tests a touch nicer.
> ln -s ./vendor/bin/phpunit ./phpunit
+You also need to install [XDebug](https://xdebug.org/index.php) in order
+for the unit tests to successfully complete.
+
## Setup
-A number of the tests that are ran during the test suite are ran against a running database. In order to setup the database used here, edit the details for the `tests` database group in **application/Config/Database.php**. Make sure that you provide a database engine that is currently running, and have already created a table that you can use only for these tests, as it will be wiped and refreshed often while running the test suite.
+A number of the tests that are ran during the test suite are ran against a running database.
+In order to setup the database used here, edit the details for the `tests` database
+group in **app/Config/Database.php**. Make sure that you provide a database engine
+that is currently running, and have already created a table that you can use only
+for these tests, as it will be wiped and refreshed often while running the test suite.
-If you want to run the tests without running the live database tests, you can exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml**, call it **phpunit.xml**, and un-comment the line within the testsuite that excludes the **tests/system/Database/Live** directory. This will make the tests run quite a bit faster.
+If you want to run the tests without running the live database tests,
+you can exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml**,
+call it **phpunit.xml**, and un-comment the line within the testsuite that excludes
+the **tests/system/Database/Live** directory. This will make the tests run quite a bit faster.
## Running the tests
@@ -24,7 +40,8 @@ The entire test suite can be ran by simply typing one command from the command l
> ./phpunit
-You can limit tests to those within a single test directory by specifying the directory name after phpunit. All core tests are stored under **tests/system**.
+You can limit tests to those within a single test directory by specifying the
+directory name after phpunit. All core tests are stored under **tests/system**.
> ./phpunit tests/system/HTTP/
@@ -38,10 +55,27 @@ You can run the tests without running the live database tests.
## Generating Code Coverage
-To generate coverage information, including HTML reports you can view in your browser, you can use the following command:
+To generate coverage information, including HTML reports you can view in your browser,
+you can use the following command:
- > ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/
+ > ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
-This runs all of the tests again, collecting information about how many lines, functions, and files are tested, and the percent of the code that is covered by the tests. It is collected in two formats: a simple text file that provides an overview, as well as comprehensive collection of HTML files that show the status of every line of code in the project.
+This runs all of the tests again, collecting information about how many lines,
+functions, and files are tested, and the percent of the code that is covered by the tests.
+It is collected in two formats: a simple text file that provides an overview,
+as well as comprehensive collection of HTML files that show the status of every line of code in the project.
+
+The text file can be found at **tests/coverage.txt**.
+The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser.
+
+## PHPUnit XML Configuration
+
+The repository has a ``phpunit.xml.dist`` file in the project root, used for
+PHPUnit configuration. This is used as a default configuration if you
+do not have your own ``phpunit.xml`` in the project root.
+
+The normal practice would be to copy ``phpunit.xml.dist`` to ``phpunit.xml``
+(which is git ignored), and to tailor yours as you see fit.
+For instance, you might wish to exclude database tests
+or automatically generate HTML code coverage reports.
-The text file can be found at **tests/coverage.txt**. The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser.
diff --git a/tests/_support/Database/MockConnection.php b/tests/_support/Database/MockConnection.php
index 1f63e672e5..e784053361 100644
--- a/tests/_support/Database/MockConnection.php
+++ b/tests/_support/Database/MockConnection.php
@@ -21,13 +21,13 @@ class MockConnection extends BaseConnection
//--------------------------------------------------------------------
- public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Database\\Query')
+ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, $queryClass = 'CodeIgniter\\Database\\Query')
{
$queryClass = str_replace('Connection', 'Query', get_class($this));
$query = new $queryClass($this);
- $query->setQuery($sql, $binds);
+ $query->setQuery($sql, $binds, $setEscapeFlags);
if (! empty($this->swapPre) && ! empty($this->DBPrefix))
{
diff --git a/tests/_support/Database/SupportMigrations/001_Some_migration.php b/tests/_support/Database/SupportMigrations/001_Some_migration.php
new file mode 100644
index 0000000000..2b98498726
--- /dev/null
+++ b/tests/_support/Database/SupportMigrations/001_Some_migration.php
@@ -0,0 +1,24 @@
+forge->addField([
+ 'key' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 255,
+ ],
+ ]);
+ $this->forge->createTable('foo', true);
+
+ $this->db->table('foo')->insert([
+ 'key' => 'foobar',
+ ]);
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('foo');
+ }
+}
diff --git a/tests/_support/Models/ValidErrorsModel.php b/tests/_support/Models/ValidErrorsModel.php
new file mode 100644
index 0000000000..95a2193511
--- /dev/null
+++ b/tests/_support/Models/ValidErrorsModel.php
@@ -0,0 +1,30 @@
+ [
+ 'required',
+ 'min_length[10]',
+ 'errors' => [
+ 'min_length' => 'Minimum Length Error',
+ ]
+ ],
+ 'token' => 'in_list[{id}]',
+ ];
+}
diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php
index d87780913b..27e1cf60e5 100644
--- a/tests/system/Autoloader/AutoloaderTest.php
+++ b/tests/system/Autoloader/AutoloaderTest.php
@@ -1,6 +1,7 @@
discoverInComposer = false;
$config->classmap = [
'UnnamespacedClass' => SUPPORTPATH . 'Autoloader/UnnamespacedClass.php',
@@ -30,7 +33,7 @@ class AutoloaderTest extends \CIUnitTestCase
];
$this->loader = new Autoloader();
- $this->loader->initialize($config)->register();
+ $this->loader->initialize($config, $moduleConfig)->register();
}
public function testLoadStoredClass()
@@ -42,11 +45,13 @@ class AutoloaderTest extends \CIUnitTestCase
{
$this->expectException(\InvalidArgumentException::class);
- $config = new Autoload();
- $config->classmap = [];
- $config->psr4 = [];
+ $config = new Autoload();
+ $config->classmap = [];
+ $config->psr4 = [];
+ $moduleConfig = new Modules();
+ $moduleConfig->discoverInComposer = false;
- (new Autoloader())->initialize($config);
+ (new Autoloader())->initialize($config, $moduleConfig);
}
//--------------------------------------------------------------------
@@ -69,7 +74,7 @@ class AutoloaderTest extends \CIUnitTestCase
{
$getShared = false;
$auto_loader = \CodeIgniter\Config\Services::autoloader($getShared);
- $auto_loader->initialize(new Autoload());
+ $auto_loader->initialize(new Autoload(), new Modules());
$auto_loader->register();
// look for Home controller, as that should be in base repo
$actual = $auto_loader->loadClass('App\Controllers\Home');
@@ -123,12 +128,14 @@ class AutoloaderTest extends \CIUnitTestCase
*/
public function testInitializeException()
{
- $config = new Autoload();
- $config->classmap = [];
- $config->psr4 = [];
+ $config = new Autoload();
+ $config->classmap = [];
+ $config->psr4 = [];
+ $moduleConfig = new Modules();
+ $moduleConfig->discoverInComposer = false;
$this->loader = new Autoloader();
- $this->loader->initialize($config);
+ $this->loader->initialize($config, $moduleConfig);
}
public function testAddNamespaceWorks()
@@ -213,4 +220,17 @@ class AutoloaderTest extends \CIUnitTestCase
}
//--------------------------------------------------------------------
+
+ public function testFindsComposerRoutes()
+ {
+ $config = new Autoload();
+ $moduleConfig = new Modules();
+ $moduleConfig->discoverInComposer = true;
+
+ $this->loader = new Autoloader();
+ $this->loader->initialize($config, $moduleConfig);
+
+ $namespaces = $this->loader->getNamespace();
+ $this->assertArrayHasKey('Zend\\Escaper', $namespaces);
+ }
}
diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php
index 50761d8152..b44f169ba2 100644
--- a/tests/system/Autoloader/FileLocatorTest.php
+++ b/tests/system/Autoloader/FileLocatorTest.php
@@ -1,5 +1,7 @@
initialize(new \Config\Autoload());
+ $autoloader->initialize(new \Config\Autoload(), new Modules());
$autoloader->addNamespace([
'Unknown' => '/i/do/not/exist',
'Tests/Support' => TESTPATH . '_support/',
@@ -207,8 +209,8 @@ class FileLocatorTest extends \CIUnitTestCase
{
$files = $this->locator->listFiles('Filters/');
- $expectedWin = APPPATH . 'Filters\DebugToolbar.php';
- $expectedLin = APPPATH . 'Filters/DebugToolbar.php';
+ $expectedWin = SYSTEMPATH . 'Filters\DebugToolbar.php';
+ $expectedLin = SYSTEMPATH . 'Filters/DebugToolbar.php';
$this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files));
$expectedWin = SYSTEMPATH . 'Filters\Filters.php';
diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php
index 72381d4c1e..b6be79eee7 100644
--- a/tests/system/Cache/Handlers/RedisHandlerTest.php
+++ b/tests/system/Cache/Handlers/RedisHandlerTest.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-2019 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
@@ -29,7 +29,7 @@
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
- * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright 2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 3.0.0
diff --git a/tests/system/Config/ConfigTest.php b/tests/system/Config/ConfigTest.php
index bde3332180..2a44773e17 100644
--- a/tests/system/Config/ConfigTest.php
+++ b/tests/system/Config/ConfigTest.php
@@ -1,17 +1,17 @@
assertInstanceOf(Email::class, $Config);
- $this->assertInstanceOf(Email::class, $NamespaceConfig);
+ $this->assertInstanceOf(DocTypes::class, $Config);
+ $this->assertInstanceOf(DocTypes::class, $NamespaceConfig);
}
public function testCreateInvalidInstance()
@@ -23,8 +23,8 @@ class ConfigTest extends \CIUnitTestCase
public function testCreateSharedInstance()
{
- $Config = Config::get('Email' );
- $Config2 = Config::get('Config\\Email');
+ $Config = Config::get('DocTypes' );
+ $Config2 = Config::get('Config\\DocTypes');
$this->assertTrue($Config === $Config2);
}
diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php
index 363f64e9ff..4d806a3bf1 100644
--- a/tests/system/Database/BaseQueryTest.php
+++ b/tests/system/Database/BaseQueryTest.php
@@ -253,4 +253,41 @@ class QueryTest extends \CIUnitTestCase
}
//--------------------------------------------------------------------
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1705
+ */
+ public function testSetQueryBindsWithSetEscapeTrue()
+ {
+ $query = new Query($this->db);
+
+ $query->setQuery('UPDATE user_table SET `x` = NOW() WHERE `id` = :id:', ['id' => 22], true);
+
+ $expected = 'UPDATE user_table SET `x` = NOW() WHERE `id` = 22';
+
+ $this->assertEquals($expected, $query->getQuery());
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1705
+ */
+ public function testSetQueryBindsWithSetEscapeFalse()
+ {
+ $query = new Query($this->db);
+
+ // The only time setQuery is called with setEscape = false
+ // is when the query builder has already stored the escaping info...
+ $binds = [
+ 'id' => [
+ 22,
+ 1,
+ ],
+ ];
+
+ $query->setQuery('UPDATE user_table SET `x` = NOW() WHERE `id` = :id:', $binds, false);
+
+ $expected = 'UPDATE user_table SET `x` = NOW() WHERE `id` = 22';
+
+ $this->assertEquals($expected, $query->getQuery());
+ }
}
diff --git a/tests/system/Database/Builder/AliasTest.php b/tests/system/Database/Builder/AliasTest.php
index f11f6d76a7..fea421492d 100644
--- a/tests/system/Database/Builder/AliasTest.php
+++ b/tests/system/Database/Builder/AliasTest.php
@@ -49,4 +49,34 @@ class AliasTest extends \CIUnitTestCase
}
//--------------------------------------------------------------------
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1599
+ */
+ public function testAliasLeftJoinWithShortTableName()
+ {
+ $this->setPrivateProperty($this->db, 'DBPrefix', 'db_');
+ $builder = $this->db->table('jobs');
+
+ $builder->join('users as u', 'u.id = jobs.id', 'left');
+
+ $expectedSQL = 'SELECT * FROM "db_jobs" LEFT JOIN "db_users" as "u" ON "u"."id" = "db_jobs"."id"';
+
+ $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1599
+ */
+ public function testAliasLeftJoinWithLongTableName()
+ {
+ $this->setPrivateProperty($this->db, 'DBPrefix', 'db_');
+ $builder = $this->db->table('jobs');
+
+ $builder->join('users as u', 'users.id = jobs.id', 'left');
+
+ $expectedSQL = 'SELECT * FROM "db_jobs" LEFT JOIN "db_users" as "u" ON "db_users"."id" = "db_jobs"."id"';
+
+ $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
+ }
}
diff --git a/tests/system/Database/Builder/DeleteTest.php b/tests/system/Database/Builder/DeleteTest.php
index 42140cac2b..2c61669373 100644
--- a/tests/system/Database/Builder/DeleteTest.php
+++ b/tests/system/Database/Builder/DeleteTest.php
@@ -24,7 +24,12 @@ class DeleteTest extends \CIUnitTestCase
$answer = $builder->delete(['id' => 1], null, true, true);
$expectedSQL = 'DELETE FROM "jobs" WHERE "id" = :id:';
- $expectedBinds = ['id' => 1];
+ $expectedBinds = [
+ 'id' => [
+ 1,
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $answer));
$this->assertEquals($expectedBinds, $builder->getBinds());
diff --git a/tests/system/Database/Builder/GroupTest.php b/tests/system/Database/Builder/GroupTest.php
index a3188a0bd0..bf957e5438 100644
--- a/tests/system/Database/Builder/GroupTest.php
+++ b/tests/system/Database/Builder/GroupTest.php
@@ -56,7 +56,7 @@ class GroupTest extends \CIUnitTestCase
->having('id >', 3)
->orHaving('SUM(id) > 2');
- $expectedSQL = 'SELECT "name" FROM "user" GROUP BY "name" HAVING "id" > :id: OR SUM(id) > 2';
+ $expectedSQL = 'SELECT "name" FROM "user" GROUP BY "name" HAVING "id" > 3 OR SUM(id) > 2';
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}
@@ -73,7 +73,7 @@ class GroupTest extends \CIUnitTestCase
->groupEnd()
->where('name', 'Darth');
- $expectedSQL = 'SELECT * FROM "user" WHERE ( "id" > :id: AND "name" != :name: ) AND "name" = :name0:';
+ $expectedSQL = 'SELECT * FROM "user" WHERE ( "id" > 3 AND "name" != \'Luke\' ) AND "name" = \'Darth\'';
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}
@@ -90,7 +90,7 @@ class GroupTest extends \CIUnitTestCase
->where('name !=', 'Luke')
->groupEnd();
- $expectedSQL = 'SELECT * FROM "user" WHERE "name" = :name: OR ( "id" > :id: AND "name" != :name0: )';
+ $expectedSQL = 'SELECT * FROM "user" WHERE "name" = \'Darth\' OR ( "id" > 3 AND "name" != \'Luke\' )';
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}
@@ -107,7 +107,7 @@ class GroupTest extends \CIUnitTestCase
->where('name !=', 'Luke')
->groupEnd();
- $expectedSQL = 'SELECT * FROM "user" WHERE "name" = :name: AND NOT ( "id" > :id: AND "name" != :name0: )';
+ $expectedSQL = 'SELECT * FROM "user" WHERE "name" = \'Darth\' AND NOT ( "id" > 3 AND "name" != \'Luke\' )';
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}
@@ -124,7 +124,7 @@ class GroupTest extends \CIUnitTestCase
->where('name !=', 'Luke')
->groupEnd();
- $expectedSQL = 'SELECT * FROM "user" WHERE "name" = :name: OR NOT ( "id" > :id: AND "name" != :name0: )';
+ $expectedSQL = 'SELECT * FROM "user" WHERE "name" = \'Darth\' OR NOT ( "id" > 3 AND "name" != \'Luke\' )';
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
}
diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php
index 65327fcb4c..6512a3628d 100644
--- a/tests/system/Database/Builder/InsertTest.php
+++ b/tests/system/Database/Builder/InsertTest.php
@@ -28,8 +28,17 @@ class InsertTest extends \CIUnitTestCase
];
$builder->insert($insertData, true, true);
- $expectedSQL = 'INSERT INTO "jobs" ("id", "name") VALUES (:id:, :name:)';
- $expectedBinds = $insertData;
+ $expectedSQL = 'INSERT INTO "jobs" ("id", "name") VALUES (1, \'Grocery Sales\')';
+ $expectedBinds = [
+ 'id' => [
+ 1,
+ true,
+ ],
+ 'name' => [
+ 'Grocery Sales',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledInsert()));
$this->assertEquals($expectedBinds, $builder->getBinds());
@@ -97,7 +106,7 @@ class InsertTest extends \CIUnitTestCase
//--------------------------------------------------------------------
- public function testInsertBatchThrowsExceptionOnEmptData()
+ public function testInsertBatchThrowsExceptionOnEmptyData()
{
$builder = $this->db->table('jobs');
diff --git a/tests/system/Database/Builder/LikeTest.php b/tests/system/Database/Builder/LikeTest.php
index 4788046e44..697982338d 100644
--- a/tests/system/Database/Builder/LikeTest.php
+++ b/tests/system/Database/Builder/LikeTest.php
@@ -24,8 +24,13 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'veloper');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'";
- $expectedBinds = ['name' => '%veloper%'];
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE '%veloper%' ESCAPE '!'";
+ $expectedBinds = [
+ 'name' => [
+ '%veloper%',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
@@ -39,8 +44,13 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'veloper', 'none');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'";
- $expectedBinds = ['name' => 'veloper'];
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE 'veloper' ESCAPE '!'";
+ $expectedBinds = [
+ 'name' => [
+ 'veloper',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
@@ -54,8 +64,13 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'veloper', 'before');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'";
- $expectedBinds = ['name' => '%veloper'];
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE '%veloper' ESCAPE '!'";
+ $expectedBinds = [
+ 'name' => [
+ '%veloper',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
@@ -69,8 +84,13 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'veloper', 'after');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'";
- $expectedBinds = ['name' => 'veloper%'];
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE 'veloper%' ESCAPE '!'";
+ $expectedBinds = [
+ 'name' => [
+ 'veloper%',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
@@ -84,10 +104,16 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'veloper')->orLike('name', 'ian');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!' OR \"name\" LIKE :name0: ESCAPE '!'";
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE '%veloper%' ESCAPE '!' OR \"name\" LIKE '%ian%' ESCAPE '!'";
$expectedBinds = [
- 'name' => '%veloper%',
- 'name0' => '%ian%',
+ 'name' => [
+ '%veloper%',
+ true,
+ ],
+ 'name0' => [
+ '%ian%',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -102,8 +128,13 @@ class LikeTest extends \CIUnitTestCase
$builder->notLike('name', 'veloper');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" NOT LIKE :name: ESCAPE '!'";
- $expectedBinds = ['name' => '%veloper%'];
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" NOT LIKE '%veloper%' ESCAPE '!'";
+ $expectedBinds = [
+ 'name' => [
+ '%veloper%',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
@@ -117,10 +148,16 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'veloper')->orNotLike('name', 'ian');
- $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!' OR \"name\" NOT LIKE :name0: ESCAPE '!'";
+ $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE '%veloper%' ESCAPE '!' OR \"name\" NOT LIKE '%ian%' ESCAPE '!'";
$expectedBinds = [
- 'name' => '%veloper%',
- 'name0' => '%ian%',
+ 'name' => [
+ '%veloper%',
+ true,
+ ],
+ 'name0' => [
+ '%ian%',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -138,8 +175,13 @@ class LikeTest extends \CIUnitTestCase
$builder->like('name', 'VELOPER', 'both', null, true);
- $expectedSQL = "SELECT * FROM \"job\" WHERE LOWER(name) LIKE :name: ESCAPE '!'";
- $expectedBinds = ['name' => '%veloper%'];
+ $expectedSQL = "SELECT * FROM \"job\" WHERE LOWER(name) LIKE '%veloper%' ESCAPE '!'";
+ $expectedBinds = [
+ 'name' => [
+ '%veloper%',
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
$this->assertSame($expectedBinds, $builder->getBinds());
diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php
index c7b6599603..af3498e073 100644
--- a/tests/system/Database/Builder/UpdateTest.php
+++ b/tests/system/Database/Builder/UpdateTest.php
@@ -25,10 +25,16 @@ class UpdateTest extends \CIUnitTestCase
$builder->where('id', 1)->update(['name' => 'Programmer'], null, null, true);
- $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "id" = :id:';
+ $expectedSQL = 'UPDATE "jobs" SET "name" = \'Programmer\' WHERE "id" = 1';
$expectedBinds = [
- 'id' => 1,
- 'name' => 'Programmer',
+ 'id' => [
+ 1,
+ true,
+ ],
+ 'name' => [
+ 'Programmer',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
@@ -43,10 +49,16 @@ class UpdateTest extends \CIUnitTestCase
$builder->update(['name' => 'Programmer'], ['id' => 1], 5, true);
- $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "id" = :id: LIMIT 5';
+ $expectedSQL = 'UPDATE "jobs" SET "name" = \'Programmer\' WHERE "id" = 1 LIMIT 5';
$expectedBinds = [
- 'id' => 1,
- 'name' => 'Programmer',
+ 'id' => [
+ 1,
+ true,
+ ],
+ 'name' => [
+ 'Programmer',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
@@ -61,10 +73,16 @@ class UpdateTest extends \CIUnitTestCase
$builder->set('name', 'Programmer')->where('id', 1)->update(null, null, null, true);
- $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "id" = :id:';
+ $expectedSQL = 'UPDATE "jobs" SET "name" = \'Programmer\' WHERE "id" = 1';
$expectedBinds = [
- 'id' => 1,
- 'name' => 'Programmer',
+ 'id' => [
+ 1,
+ true,
+ ],
+ 'name' => [
+ 'Programmer',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
@@ -177,10 +195,16 @@ WHERE "id" IN(2,3)';
$builder->update(['name' => 'foobar'], ['name' => 'Programmer'], null, true);
- $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "name" = :name0:';
+ $expectedSQL = 'UPDATE "jobs" SET "name" = \'foobar\' WHERE "name" = \'Programmer\'';
$expectedBinds = [
- 'name' => 'foobar',
- 'name0' => 'Programmer',
+ 'name' => [
+ 'foobar',
+ true,
+ ],
+ 'name0' => [
+ 'Programmer',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
@@ -198,10 +222,16 @@ WHERE "id" IN(2,3)';
->where('name', 'Programmer')
->update(null, null, null, true);
- $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "name" = :name0:';
+ $expectedSQL = 'UPDATE "jobs" SET "name" = \'foobar\' WHERE "name" = \'Programmer\'';
$expectedBinds = [
- 'name' => 'foobar',
- 'name0' => 'Programmer',
+ 'name' => [
+ 'foobar',
+ true,
+ ],
+ 'name0' => [
+ 'Programmer',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
@@ -218,10 +248,16 @@ WHERE "id" IN(2,3)';
$builder->where('name', 'Programmer')
->update(['name' => 'foobar'], null, null, true);
- $expectedSQL = 'UPDATE "jobs" SET "name" = :name0: WHERE "name" = :name:';
+ $expectedSQL = 'UPDATE "jobs" SET "name" = \'foobar\' WHERE "name" = \'Programmer\'';
$expectedBinds = [
- 'name' => 'Programmer',
- 'name0' => 'foobar',
+ 'name' => [
+ 'Programmer',
+ true,
+ ],
+ 'name0' => [
+ 'foobar',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
@@ -239,8 +275,13 @@ WHERE "id" IN(2,3)';
->where('id', 2)
->update(null, null, null, true);
- $expectedSQL = 'UPDATE "mytable" SET field = field+1 WHERE "id" = :id:';
- $expectedBinds = ['id' => 2];
+ $expectedSQL = 'UPDATE "mytable" SET field = field+1 WHERE "id" = 2';
+ $expectedBinds = [
+ 'id' => [
+ 2,
+ true,
+ ],
+ ];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate()));
$this->assertEquals($expectedBinds, $builder->getBinds());
diff --git a/tests/system/Database/Builder/WhereTest.php b/tests/system/Database/Builder/WhereTest.php
index a2c2076568..0e2dbbaa55 100644
--- a/tests/system/Database/Builder/WhereTest.php
+++ b/tests/system/Database/Builder/WhereTest.php
@@ -21,8 +21,13 @@ class WhereTest extends \CIUnitTestCase
{
$builder = $this->db->table('users');
- $expectedSQL = 'SELECT * FROM "users" WHERE "id" = :id:';
- $expectedBinds = ['id' => 3];
+ $expectedSQL = 'SELECT * FROM "users" WHERE "id" = 3';
+ $expectedBinds = [
+ 'id' => [
+ 3,
+ true,
+ ],
+ ];
$builder->where('id', 3);
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -35,8 +40,13 @@ class WhereTest extends \CIUnitTestCase
{
$builder = $this->db->table('users');
- $expectedSQL = 'SELECT * FROM "users" WHERE id = :id:';
- $expectedBinds = ['id' => 3];
+ $expectedSQL = 'SELECT * FROM "users" WHERE id = 3';
+ $expectedBinds = [
+ 'id' => [
+ 3,
+ false,
+ ],
+ ];
$builder->where('id', 3, false);
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -49,8 +59,13 @@ class WhereTest extends \CIUnitTestCase
{
$builder = $this->db->table('users');
- $expectedSQL = 'SELECT * FROM "users" WHERE "id" != :id:';
- $expectedBinds = ['id' => 3];
+ $expectedSQL = 'SELECT * FROM "users" WHERE "id" != 3';
+ $expectedBinds = [
+ 'id' => [
+ 3,
+ true,
+ ],
+ ];
$builder->where('id !=', 3);
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -68,10 +83,16 @@ class WhereTest extends \CIUnitTestCase
'name !=' => 'Accountant',
];
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" = :id: AND "name" != :name:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" = 2 AND "name" != \'Accountant\'';
$expectedBinds = [
- 'id' => 2,
- 'name' => 'Accountant',
+ 'id' => [
+ 2,
+ true,
+ ],
+ 'name' => [
+ 'Accountant',
+ true,
+ ],
];
$builder->where($where);
@@ -104,10 +125,16 @@ class WhereTest extends \CIUnitTestCase
$builder->where('name !=', 'Accountant')
->orWhere('id >', 3);
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" != :name: OR "id" > :id:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" != \'Accountant\' OR "id" > 3';
$expectedBinds = [
- 'name' => 'Accountant',
- 'id' => 3,
+ 'name' => [
+ 'Accountant',
+ true,
+ ],
+ 'id' => [
+ 3,
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -123,10 +150,16 @@ class WhereTest extends \CIUnitTestCase
$builder->where('name', 'Accountant')
->orWhere('name', 'foobar');
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = :name: OR "name" = :name0:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = \'Accountant\' OR "name" = \'foobar\'';
$expectedBinds = [
- 'name' => 'Accountant',
- 'name0' => 'foobar',
+ 'name' => [
+ 'Accountant',
+ true,
+ ],
+ 'name0' => [
+ 'foobar',
+ true,
+ ],
];
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
@@ -141,11 +174,14 @@ class WhereTest extends \CIUnitTestCase
$builder->whereIn('name', ['Politician', 'Accountant']);
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" IN :name:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" IN (\'Politician\',\'Accountant\')';
$expectedBinds = [
'name' => [
- 'Politician',
- 'Accountant',
+ [
+ 'Politician',
+ 'Accountant',
+ ],
+ true,
],
];
@@ -161,11 +197,14 @@ class WhereTest extends \CIUnitTestCase
$builder->whereNotIn('name', ['Politician', 'Accountant']);
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" NOT IN :name:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" NOT IN (\'Politician\',\'Accountant\')';
$expectedBinds = [
'name' => [
- 'Politician',
- 'Accountant',
+ [
+ 'Politician',
+ 'Accountant',
+ ],
+ true,
],
];
@@ -181,12 +220,18 @@ class WhereTest extends \CIUnitTestCase
$builder->where('id', 2)->orWhereIn('name', ['Politician', 'Accountant']);
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" = :id: OR "name" IN :name:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" = 2 OR "name" IN (\'Politician\',\'Accountant\')';
$expectedBinds = [
- 'id' => 2,
+ 'id' => [
+ 2,
+ true,
+ ],
'name' => [
- 'Politician',
- 'Accountant',
+ [
+ 'Politician',
+ 'Accountant',
+ ],
+ true,
],
];
@@ -202,12 +247,18 @@ class WhereTest extends \CIUnitTestCase
$builder->where('id', 2)->orWhereNotIn('name', ['Politician', 'Accountant']);
- $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" = :id: OR "name" NOT IN :name:';
+ $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" = 2 OR "name" NOT IN (\'Politician\',\'Accountant\')';
$expectedBinds = [
- 'id' => 2,
+ 'id' => [
+ 2,
+ true,
+ ],
'name' => [
- 'Politician',
- 'Accountant',
+ [
+ 'Politician',
+ 'Accountant',
+ ],
+ true,
],
];
diff --git a/tests/system/Database/Live/AliasTest.php b/tests/system/Database/Live/AliasTest.php
index f4d1f588c8..c7affea1f3 100644
--- a/tests/system/Database/Live/AliasTest.php
+++ b/tests/system/Database/Live/AliasTest.php
@@ -2,6 +2,9 @@
use CodeIgniter\Test\CIDatabaseTestCase;
+/**
+ * @group DatabaseLive
+ */
class AliasTest extends CIDatabaseTestCase
{
protected $refresh = true;
diff --git a/tests/system/Database/Live/ConnectTest.php b/tests/system/Database/Live/ConnectTest.php
index 0d0587b9f6..17e44f1e71 100644
--- a/tests/system/Database/Live/ConnectTest.php
+++ b/tests/system/Database/Live/ConnectTest.php
@@ -1,10 +1,12 @@
group1 = $config->default;
$this->group2 = $config->default;
- $this->group1['strictOn'] = false;
- $this->group2['strictOn'] = true;
+ $this->group1['DBDriver'] = 'MySQLi';
+ $this->group2['DBDriver'] = 'Postgre';
}
public function testConnectWithMultipleCustomGroups()
@@ -39,6 +41,36 @@ class ConnectTest extends CIDatabaseTestCase
$this->assertEquals(3, count($instances));
}
- //--------------------------------------------------------------------
+ public function testConnectReturnsProvidedConnection()
+ {
+ $config = config('Database');
+ // This will be the tests database
+ $db = Database::connect();
+ $this->assertEquals($config->tests['DBDriver'], $this->getPrivateProperty($db, 'DBDriver'));
+
+ // Get an instance of the system's default db so we have something to test with.
+ $db1 = Database::connect($this->group1);
+ $this->assertEquals('MySQLi', $this->getPrivateProperty($db1, 'DBDriver'));
+
+ // If a connection is passed into connect, it should simply be returned to us...
+ $db2 = Database::connect($db1);
+ $this->assertSame($db1, $db2);
+ }
+
+ public function testConnectWorksWithGroupName()
+ {
+ $config = config('Database');
+
+ $db = Database::connect('tests');
+ $this->assertEquals($config->tests['DBDriver'], $this->getPrivateProperty($db, 'DBDriver'));
+
+ $config = config('Database');
+ $config->default['DBDriver'] = 'MySQLi';
+ Config::injectMock('Database', $config);
+
+ $db1 = Database::connect('default');
+ $this->assertNotInstanceOf(\CodeIgniter\Database\SQLite3\Connection::class, $db1);
+ $this->assertEquals('MySQLi', $this->getPrivateProperty($db1, 'DBDriver'));
+ }
}
diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php
index 0516bc81ff..26f1cc1da7 100644
--- a/tests/system/Database/Live/ForgeTest.php
+++ b/tests/system/Database/Live/ForgeTest.php
@@ -207,6 +207,15 @@ class ForgeTest extends CIDatabaseTestCase
$this->assertEquals($keys['db_forge_test_1_code_active']->fields, ['code', 'active']);
$this->assertEquals($keys['db_forge_test_1_code_active']->type, 'UNIQUE');
}
+ elseif ($this->db->DBDriver === 'SQLite3')
+ {
+ $this->assertEquals($keys['sqlite_autoindex_db_forge_test_1_1']->name, 'sqlite_autoindex_db_forge_test_1_1');
+ $this->assertEquals($keys['sqlite_autoindex_db_forge_test_1_1']->fields, ['id']);
+ $this->assertEquals($keys['db_forge_test_1_code_company']->name, 'db_forge_test_1_code_company');
+ $this->assertEquals($keys['db_forge_test_1_code_company']->fields, ['code', 'company']);
+ $this->assertEquals($keys['db_forge_test_1_code_active']->name, 'db_forge_test_1_code_active');
+ $this->assertEquals($keys['db_forge_test_1_code_active']->fields, ['code', 'active']);
+ }
$this->forge->dropTable('forge_test_1', true);
}
@@ -370,4 +379,76 @@ class ForgeTest extends CIDatabaseTestCase
$this->assertInstanceOf(Forge::class, $forge);
}
+
+ public function testDropColumn()
+ {
+ $this->forge->dropTable('forge_test_two', true);
+
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'INTEGER',
+ 'constraint' => 11,
+ 'unsigned' => false,
+ 'auto_increment' => true,
+ ],
+ 'name' => [
+ 'type' => 'varchar',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->createTable('forge_test_two');
+
+ $this->assertTrue($this->db->fieldExists('name', 'forge_test_two'));
+
+ $this->forge->dropColumn('forge_test_two', 'name');
+
+ $this->db->resetDataCache();
+
+ $this->assertFalse($this->db->fieldExists('name', 'forge_test_two'));
+
+ $this->forge->dropTable('forge_test_two', true);
+ }
+
+ public function testModifyColumnRename()
+ {
+ $this->forge->dropTable('forge_test_three', true);
+
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'INTEGER',
+ 'constraint' => 11,
+ 'unsigned' => false,
+ 'auto_increment' => true,
+ ],
+ 'name' => [
+ 'type' => 'varchar',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
+ ]);
+
+ $this->forge->addKey('id', true);
+ $this->forge->createTable('forge_test_three');
+
+ $this->assertTrue($this->db->fieldExists('name', 'forge_test_three'));
+
+ $this->forge->modifyColumn('forge_test_three', [
+ 'name' => [
+ 'name' => 'altered',
+ 'type' => 'varchar',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
+ ]);
+
+ $this->db->resetDataCache();
+
+ $this->assertFalse($this->db->fieldExists('name', 'forge_test_three'));
+ $this->assertTrue($this->db->fieldExists('altered', 'forge_test_three'));
+
+ $this->forge->dropTable('forge_test_three', true);
+ }
}
diff --git a/tests/system/Database/Live/ModelTest.php b/tests/system/Database/Live/ModelTest.php
index ff45a2aef7..580ecf2179 100644
--- a/tests/system/Database/Live/ModelTest.php
+++ b/tests/system/Database/Live/ModelTest.php
@@ -1,10 +1,13 @@
model = new Model($this->db);
}
+ public function tearDown()
+ {
+ parent::tearDown();
+
+ Services::reset();
+ Config::reset();
+ }
+
//--------------------------------------------------------------------
public function testFindReturnsRow()
@@ -61,7 +73,8 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new JobModel($this->db);
- $jobs = $model->asArray()->find();
+ $jobs = $model->asArray()
+ ->find();
$this->assertCount(4, $jobs);
@@ -78,7 +91,8 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new JobModel($this->db);
- $job = $model->asArray()->find(4);
+ $job = $model->asArray()
+ ->find(4);
$this->assertInternalType('array', $job);
}
@@ -89,7 +103,8 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new JobModel($this->db);
- $job = $model->asObject()->find(4);
+ $job = $model->asObject()
+ ->find(4);
$this->assertInternalType('object', $job);
}
@@ -98,15 +113,19 @@ class ModelTest extends CIDatabaseTestCase
public function testFindRespectsSoftDeletes()
{
- $this->db->table('user')->where('id', 4)->update(['deleted' => 1]);
+ $this->db->table('user')
+ ->where('id', 4)
+ ->update(['deleted' => 1]);
$model = new UserModel($this->db);
- $user = $model->asObject()->find(4);
+ $user = $model->asObject()
+ ->find(4);
$this->assertEmpty($user);
- $user = $model->withDeleted()->find(4);
+ $user = $model->withDeleted()
+ ->find(4);
// fix for PHP7.2
$count = is_array($user) ? count($user) : 1;
@@ -123,7 +142,8 @@ class ModelTest extends CIDatabaseTestCase
$model->find(1);
// Binds should be reset to 0 after each one
- $binds = $model->builder()->getBinds();
+ $binds = $model->builder()
+ ->getBinds();
$this->assertCount(0, $binds);
$query = $model->getLastQuery();
@@ -167,7 +187,9 @@ class ModelTest extends CIDatabaseTestCase
public function testFindAllRespectsSoftDeletes()
{
- $this->db->table('user')->where('id', 4)->update(['deleted' => 1]);
+ $this->db->table('user')
+ ->where('id', 4)
+ ->update(['deleted' => 1]);
$model = new UserModel($this->db);
@@ -175,7 +197,8 @@ class ModelTest extends CIDatabaseTestCase
$this->assertCount(3, $user);
- $user = $model->withDeleted()->findAll();
+ $user = $model->withDeleted()
+ ->findAll();
$this->assertCount(4, $user);
}
@@ -186,7 +209,8 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new UserModel();
- $user = $model->where('id >', 2)->first();
+ $user = $model->where('id >', 2)
+ ->first();
// fix for PHP7.2
$count = is_array($user) ? count($user) : 1;
@@ -198,7 +222,9 @@ class ModelTest extends CIDatabaseTestCase
public function testFirstRespectsSoftDeletes()
{
- $this->db->table('user')->where('id', 1)->update(['deleted' => 1]);
+ $this->db->table('user')
+ ->where('id', 1)
+ ->update(['deleted' => 1]);
$model = new UserModel();
@@ -209,7 +235,8 @@ class ModelTest extends CIDatabaseTestCase
$this->assertEquals(1, $count);
$this->assertEquals(2, $user->id);
- $user = $model->withDeleted()->first();
+ $user = $model->withDeleted()
+ ->first();
$this->assertEquals(1, $user->id);
}
@@ -218,14 +245,16 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new SecondaryModel();
- $this->db->table('secondary')->insert([
- 'key' => 'foo',
- 'value' => 'bar',
- ]);
- $this->db->table('secondary')->insert([
- 'key' => 'bar',
- 'value' => 'baz',
- ]);
+ $this->db->table('secondary')
+ ->insert([
+ 'key' => 'foo',
+ 'value' => 'bar',
+ ]);
+ $this->db->table('secondary')
+ ->insert([
+ 'key' => 'bar',
+ 'value' => 'baz',
+ ]);
$record = $model->first();
@@ -243,7 +272,8 @@ class ModelTest extends CIDatabaseTestCase
$data->name = 'Magician';
$data->description = 'Makes peoples things dissappear.';
- $model->protect(false)->save($data);
+ $model->protect(false)
+ ->save($data);
$this->seeInDatabase('job', ['name' => 'Magician']);
}
@@ -259,7 +289,8 @@ class ModelTest extends CIDatabaseTestCase
'description' => 'That thing you do.',
];
- $result = $model->protect(false)->save($data);
+ $result = $model->protect(false)
+ ->save($data);
$this->seeInDatabase('job', ['name' => 'Apprentice']);
}
@@ -276,7 +307,8 @@ class ModelTest extends CIDatabaseTestCase
'description' => 'That thing you do.',
];
- $result = $model->protect(false)->save($data);
+ $result = $model->protect(false)
+ ->save($data);
$this->seeInDatabase('job', ['name' => 'Apprentice']);
$this->assertTrue($result);
@@ -293,7 +325,8 @@ class ModelTest extends CIDatabaseTestCase
$data->name = 'Engineer';
$data->description = 'A fancier term for Developer.';
- $result = $model->protect(false)->save($data);
+ $result = $model->protect(false)
+ ->save($data);
$this->seeInDatabase('job', ['name' => 'Engineer']);
$this->assertTrue($result);
@@ -311,7 +344,8 @@ class ModelTest extends CIDatabaseTestCase
$data->description = 'A fancier term for Developer.';
$data->random_thing = 'Something wicked'; // If not protected, this would kill the script.
- $result = $model->protect(true)->save($data);
+ $result = $model->protect(true)
+ ->save($data);
$this->assertTrue($result);
}
@@ -379,7 +413,8 @@ class ModelTest extends CIDatabaseTestCase
$this->seeInDatabase('job', ['name' => 'Developer']);
- $model->where('id', 1)->delete();
+ $model->where('id', 1)
+ ->delete();
$this->dontSeeInDatabase('job', ['name' => 'Developer']);
}
@@ -390,11 +425,14 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new UserModel();
- $this->db->table('user')->where('id', 1)->update(['deleted' => 1]);
+ $this->db->table('user')
+ ->where('id', 1)
+ ->update(['deleted' => 1]);
$model->purgeDeleted();
- $users = $model->withDeleted()->findAll();
+ $users = $model->withDeleted()
+ ->findAll();
$this->assertCount(3, $users);
}
@@ -405,9 +443,12 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new UserModel($this->db);
- $this->db->table('user')->where('id', 1)->update(['deleted' => 1]);
+ $this->db->table('user')
+ ->where('id', 1)
+ ->update(['deleted' => 1]);
- $users = $model->onlyDeleted()->findAll();
+ $users = $model->onlyDeleted()
+ ->findAll();
$this->assertCount(1, $users);
}
@@ -434,6 +475,7 @@ class ModelTest extends CIDatabaseTestCase
$model = new ValidModel($this->db);
$data = [
+ 'name' => null,
'description' => 'some great marketing stuff',
];
@@ -481,7 +523,102 @@ class ModelTest extends CIDatabaseTestCase
'description' => 'some great marketing stuff',
];
- $this->assertInternalType('numeric', $model->skipValidation(true)->insert($data));
+ $this->assertInternalType('numeric', $model->skipValidation(true)
+ ->insert($data));
+ }
+
+ public function testCleanValidationRemovesAllWhenNoDataProvided()
+ {
+ $model = new Model($this->db);
+ $cleaner = $this->getPrivateMethodInvoker($model, 'cleanValidationRules');
+
+ $rules = [
+ 'name' => 'required',
+ 'foo' => 'bar',
+ ];
+
+ $rules = $cleaner($rules, null);
+
+ $this->assertEmpty($rules);
+ }
+
+ public function testCleanValidationRemovesOnlyForFieldsNotProvided()
+ {
+ $model = new Model($this->db);
+ $cleaner = $this->getPrivateMethodInvoker($model, 'cleanValidationRules');
+
+ $rules = [
+ 'name' => 'required',
+ 'foo' => 'required',
+ ];
+
+ $data = [
+ 'foo' => 'bar',
+ ];
+
+ $rules = $cleaner($rules, $data);
+
+ $this->assertTrue(array_key_exists('foo', $rules));
+ $this->assertFalse(array_key_exists('name', $rules));
+ }
+
+ public function testCleanValidationReturnsAllWhenAllExist()
+ {
+ $model = new Model($this->db);
+ $cleaner = $this->getPrivateMethodInvoker($model, 'cleanValidationRules');
+
+ $rules = [
+ 'name' => 'required',
+ 'foo' => 'required',
+ ];
+
+ $data = [
+ 'foo' => 'bar',
+ 'name' => null,
+ ];
+
+ $rules = $cleaner($rules, $data);
+
+ $this->assertTrue(array_key_exists('foo', $rules));
+ $this->assertTrue(array_key_exists('name', $rules));
+ }
+
+ public function testValidationPassesWithMissingFields()
+ {
+ $model = new ValidModel();
+
+ $data = [
+ 'foo' => 'bar',
+ ];
+
+ $result = $model->validate($data);
+
+ $this->assertTrue($result);
+ }
+
+ public function testValidationWithGroupName()
+ {
+ $config = new \Config\Validation();
+ $config->grouptest = [
+ 'name' => [
+ 'required',
+ 'min_length[3]',
+ ],
+ 'token' => 'in_list[{id}]',
+ ];
+
+ $data = [
+ 'name' => 'abc',
+ 'id' => 13,
+ 'token' => 13,
+ ];
+
+ \CodeIgniter\Config\Config::injectMock('Validation', $config);
+
+ $model = new ValidModel($this->db);
+ $this->setPrivateProperty($model, 'validationRules', 'grouptest');
+
+ $this->assertTrue($model->validate($data));
}
//--------------------------------------------------------------------
@@ -490,7 +627,8 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new EntityModel($this->db);
- $entity = $model->where('name', 'Developer')->first();
+ $entity = $model->where('name', 'Developer')
+ ->first();
$this->assertInstanceOf(SimpleEntity::class, $entity);
$this->assertEquals('Developer', $entity->name);
@@ -593,7 +731,8 @@ class ModelTest extends CIDatabaseTestCase
'email' => 'foo@example.com',
'name' => 'Foo Bar',
'country' => 'US',
- ])->insert();
+ ])
+ ->insert();
$this->seeInDatabase('user', [
'email' => 'foo@example.com',
@@ -616,7 +755,8 @@ class ModelTest extends CIDatabaseTestCase
$model->set([
'name' => 'Fred Flintstone',
- ])->update($userId);
+ ])
+ ->update($userId);
$this->seeInDatabase('user', [
'id' => $userId,
@@ -643,7 +783,8 @@ class ModelTest extends CIDatabaseTestCase
->where('id', $userId)
->set([
'name' => 'Fred Flintstone',
- ])->update();
+ ])
+ ->update();
$this->seeInDatabase('user', [
'id' => $userId,
@@ -768,7 +909,9 @@ class ModelTest extends CIDatabaseTestCase
$model = new EntityModel();
- $job = $model->select('id, name')->where('name', 'Rocket Scientist')->first();
+ $job = $model->select('id, name')
+ ->where('name', 'Rocket Scientist')
+ ->first();
$this->assertNull($job->description);
$this->assertEquals('Rocket Scientist', $job->name);
@@ -786,17 +929,19 @@ class ModelTest extends CIDatabaseTestCase
{
$model = new SecondaryModel();
- $this->db->table('secondary')->insert([
- 'key' => 'foo',
- 'value' => 'bar',
- ]);
+ $this->db->table('secondary')
+ ->insert([
+ 'key' => 'foo',
+ 'value' => 'bar',
+ ]);
$this->dontSeeInDatabase('secondary', [
'key' => 'bar',
'value' => 'baz',
]);
- $model->where('key', 'foo')->update(null, ['key' => 'bar', 'value' => 'baz']);
+ $model->where('key', 'foo')
+ ->update(null, ['key' => 'bar', 'value' => 'baz']);
$this->seeInDatabase('secondary', [
'key' => 'bar',
@@ -814,8 +959,100 @@ class ModelTest extends CIDatabaseTestCase
// testSeeder has 4 users....
$this->assertEquals(4, $model->countAllResults());
- $model->where('name', 'Derek Jones')->delete();
+ $model->where('name', 'Derek Jones')
+ ->delete();
$this->assertEquals(3, $model->countAllResults());
}
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1584
+ */
+ public function testUpdateWithValidation()
+ {
+ $model = new ValidModel($this->db);
+
+ $data = [
+ 'description' => 'This is a first test!',
+ 'name' => 'valid',
+ 'id' => 42,
+ 'token' => 42,
+ ];
+
+ $id = $model->insert($data);
+
+ $this->assertTrue((bool)$id);
+
+ $data['description'] = 'This is a second test!';
+ unset($data['name']);
+
+ $result = $model->update($id, $data);
+ $this->assertTrue($result);
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1717
+ */
+ public function testRequiredWithValidationEmptyString()
+ {
+ $model = new ValidModel($this->db);
+
+ $data = [
+ 'name' => '',
+ ];
+
+ $this->assertFalse($model->insert($data));
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1717
+ */
+ public function testRequiredWithValidationNull()
+ {
+ $model = new ValidModel($this->db);
+
+ $data = [
+ 'name' => null,
+ ];
+
+ $this->assertFalse($model->insert($data));
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1717
+ */
+ public function testRequiredWithValidationTrue()
+ {
+ $model = new ValidModel($this->db);
+
+ $data = [
+ 'name' => 'foobar',
+ 'description' => 'just becaues we have to',
+ ];
+
+ $this->assertTrue($model->insert($data) !== false);
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1574
+ */
+ public function testValidationIncludingErrors()
+ {
+ $model = new ValidErrorsModel($this->db);
+
+ $data = [
+ 'description' => 'This is a first test!',
+ 'name' => 'valid',
+ 'id' => 42,
+ 'token' => 42,
+ ];
+
+ $id = $model->insert($data);
+
+ $this->assertFalse((bool)$id);
+
+ $errors = $model->errors();
+
+ $this->assertEquals('Minimum Length Error', $model->errors()['name']);
+ }
}
diff --git a/tests/system/Database/Live/SQLite/AlterTableTest.php b/tests/system/Database/Live/SQLite/AlterTableTest.php
new file mode 100644
index 0000000000..186cfc7bb6
--- /dev/null
+++ b/tests/system/Database/Live/SQLite/AlterTableTest.php
@@ -0,0 +1,205 @@
+ 'SQLite3',
+ 'database' => ':memory:',
+ ];
+
+ $this->db = db_connect($config);
+ $this->forge = Database::forge($config);
+ $this->table = new Table($this->db, $this->forge);
+ }
+
+ public function tearDown()
+ {
+ parent::tearDown();
+
+ $this->forge->dropTable('foo', true);
+ }
+
+ /**
+ * @expectedException \CodeIgniter\Database\Exceptions\DataException
+ * @expectedExceptionMessage Table `foo` was not found in the current database.
+ */
+ public function testFromTableThrowsOnNoTable()
+ {
+ $this->table->fromTable('foo');
+ }
+
+ public function testFromTableFillsDetails()
+ {
+ $this->createTable('foo');
+
+ $this->assertTrue($this->db->tableExists('foo'));
+
+ $this->table->fromTable('foo');
+
+ $fields = $this->getPrivateProperty($this->table, 'fields');
+
+ $this->assertCount(3, $fields);
+ $this->assertTrue(array_key_exists('id', $fields));
+ $this->assertNull($fields['id']['default']);
+ $this->assertTrue($fields['id']['nullable']);
+ $this->assertEquals('integer', strtolower($fields['id']['type']));
+
+ $this->assertTrue(array_key_exists('name', $fields));
+ $this->assertNull($fields['name']['default']);
+ $this->assertFalse($fields['name']['nullable']);
+ $this->assertEquals('varchar', strtolower($fields['name']['type']));
+
+ $this->assertTrue(array_key_exists('email', $fields));
+ $this->assertNull($fields['email']['default']);
+ $this->assertTrue($fields['email']['nullable']);
+ $this->assertEquals('varchar', strtolower($fields['email']['type']));
+
+ $keys = $this->getPrivateProperty($this->table, 'keys');
+
+ $this->assertCount(3, $keys);
+ $this->assertTrue(array_key_exists('foo_name', $keys));
+ $this->assertEquals(['fields' => ['name'], 'type' => 'index'], $keys['foo_name']);
+ $this->assertTrue(array_key_exists('id', $keys));
+ $this->assertEquals(['fields' => ['id'], 'type' => 'primary'], $keys['id']);
+ $this->assertTrue(array_key_exists('id', $keys));
+ $this->assertEquals(['fields' => ['id'], 'type' => 'primary'], $keys['id']);
+ }
+
+ public function testDropColumnSuccess()
+ {
+ $this->createTable('foo');
+
+ $result = $this->table
+ ->fromTable('foo')
+ ->dropColumn('name')
+ ->run();
+
+ $this->assertTrue($result);
+
+ $columns = $this->db->getFieldNames('foo');
+
+ $this->assertFalse(in_array('name', $columns));
+ $this->assertTrue(in_array('id', $columns));
+ $this->assertTrue(in_array('email', $columns));
+ }
+
+ public function testDropColumnMaintainsKeys()
+ {
+ $this->createTable('foo');
+
+ $oldKeys = $this->db->getIndexData('foo');
+
+ $this->assertTrue(array_key_exists('foo_name', $oldKeys));
+ $this->assertTrue(array_key_exists('foo_email', $oldKeys));
+
+ $result = $this->table
+ ->fromTable('foo')
+ ->dropColumn('name')
+ ->run();
+
+ $newKeys = $this->db->getIndexData('foo');
+
+ $this->assertFalse(array_key_exists('foo_name', $newKeys));
+ $this->assertTrue(array_key_exists('foo_email', $newKeys));
+
+ $this->assertTrue($result);
+ }
+
+ public function testModifyColumnSuccess()
+ {
+ $this->createTable('janky');
+
+ $result = $this->table
+ ->fromTable('janky')
+ ->modifyColumn([
+ [
+ 'name' => 'name',
+ 'new_name' => 'serial',
+ 'type' => 'int',
+ 'constraint' => 11,
+ 'null' => true,
+ ],
+ ])
+ ->run();
+
+ $this->assertTrue($result);
+
+ $this->assertFalse($this->db->fieldExists('name', 'janky'));
+ $this->assertTrue($this->db->fieldExists('serial', 'janky'));
+ }
+
+ public function testProcessCopiesOldData()
+ {
+ $this->createTable('foo');
+
+ $this->db->table('foo')->insert([
+ 'id' => 1,
+ 'name' => 'George Clinton',
+ 'email' => 'funkalicious@example.com',
+ ]);
+
+ $this->seeInDatabase('foo', ['name' => 'George Clinton']);
+
+ $result = $this->table
+ ->fromTable('foo')
+ ->dropColumn('name')
+ ->run();
+
+ $this->dontSeeInDatabase('foo', ['name' => 'George Clinton']);
+ $this->seeInDatabase('foo', ['email' => 'funkalicious@example.com']);
+ }
+
+ protected function createTable(string $tableName = 'foo')
+ {
+ $this->forge->addField([
+ 'id' => [
+ 'type' => 'integer',
+ 'constraint' => 11,
+ 'unsigned' => true,
+ 'auto_increment' => true,
+ ],
+ 'name' => [
+ 'type' => 'varchar',
+ 'constraint' => 255,
+ 'null' => false,
+ ],
+ 'email' => [
+ 'type' => 'varchar',
+ 'constraint' => 255,
+ 'null' => true,
+ ],
+ ]);
+ $this->forge->addPrimaryKey('id');
+ $this->forge->addKey('name');
+ $this->forge->addUniqueKey('email');
+ $this->forge->createTable($tableName);
+ }
+}
diff --git a/tests/system/Database/Live/SelectTest.php b/tests/system/Database/Live/SelectTest.php
index d4c6331eb0..d6d45cb22e 100644
--- a/tests/system/Database/Live/SelectTest.php
+++ b/tests/system/Database/Live/SelectTest.php
@@ -135,4 +135,42 @@ class SelectTest extends CIDatabaseTestCase
}
//--------------------------------------------------------------------
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1226
+ */
+ public function testSelectWithMultipleWheresOnSameColumn()
+ {
+ $users = $this->db->table('user')
+ ->where('id', 1)
+ ->orWhereIn('id', [2, 3])
+ ->get()
+ ->getResultArray();
+
+ $this->assertCount(3, $users);
+
+ foreach ($users as $user)
+ {
+ $this->assertTrue(in_array($user['id'], [1, 2, 3]));
+ }
+ }
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1226
+ */
+ public function testSelectWithMultipleWheresOnSameColumnAgain()
+ {
+ $users = $this->db->table('user')
+ ->whereIn('id', [1, 2])
+ ->orWhere('id', 3)
+ ->get()
+ ->getResultArray();
+
+ $this->assertCount(3, $users);
+
+ foreach ($users as $user)
+ {
+ $this->assertTrue(in_array($user['id'], [1, 2, 3]));
+ }
+ }
}
diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php
new file mode 100644
index 0000000000..b494936821
--- /dev/null
+++ b/tests/system/Database/Migrations/MigrationRunnerTest.php
@@ -0,0 +1,337 @@
+root = vfsStream::setup('root');
+ $this->start = $this->root->url() . '/';
+ $this->config = new Migrations();
+ $this->config->enabled = true;
+ }
+
+ /**
+ * @expectedException \CodeIgniter\Exceptions\ConfigException
+ */
+ public function testThrowsOnInvalidMigrationType()
+ {
+ $config = $this->config;
+ $config->type = 'narwhal';
+
+ $runner = new MigrationRunner($config);
+ }
+
+ public function testLoadsDefaultDatabaseWhenNoneSpecified()
+ {
+ $dbConfig = new \Config\Database();
+ $runner = new MigrationRunner($this->config);
+
+ $db = $this->getPrivateProperty($runner, 'db');
+
+ $this->assertInstanceOf(BaseConnection::class, $db);
+ $this->assertEquals($dbConfig->tests['database'], $this->getPrivateProperty($db, 'database'));
+ $this->assertEquals($dbConfig->tests['DBDriver'], $this->getPrivateProperty($db, 'DBDriver'));
+ }
+
+ public function testGetCliMessages()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $messages = [
+ 'foo',
+ 'bar',
+ ];
+
+ $this->setPrivateProperty($runner, 'cliMessages', $messages);
+
+ $this->assertEquals($messages, $runner->getCliMessages());
+ }
+
+ public function testGetHistory()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $tableMaker = $this->getPrivateMethodInvoker($runner, 'ensureTable');
+ $tableMaker();
+
+ $history = [
+ 'version' => 'abc123',
+ 'name' => 'changesomething',
+ 'group' => 'default',
+ 'namespace' => 'App',
+ 'time' => time(),
+ ];
+
+ $this->hasInDatabase('migrations', $history);
+
+ $this->assertEquals($history, $runner->getHistory()[0]);
+ }
+
+ public function testGetHistoryReturnsEmptyArrayWithNoResults()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $tableMaker = $this->getPrivateMethodInvoker($runner, 'ensureTable');
+ $tableMaker();
+
+ $this->assertEquals([], $runner->getHistory());
+ }
+
+ public function testGetMigrationNumber()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $method = $this->getPrivateMethodInvoker($runner, 'getMigrationNumber');
+
+ $this->assertEquals('0123456', $method('0123456_Foo'));
+ }
+
+ public function testGetMigrationNumberReturnsZeroIfNoneFound()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $method = $this->getPrivateMethodInvoker($runner, 'getMigrationNumber');
+
+ $this->assertEquals('0', $method('Foo'));
+ }
+
+ public function testSetSilentStoresValue()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $runner->setSilent(true);
+ $this->assertTrue($this->getPrivateProperty($runner, 'silent'));
+
+ $runner->setSilent(false);
+ $this->assertFalse($this->getPrivateProperty($runner, 'silent'));
+ }
+
+ public function testSetNameStoresValue()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $runner->setName('foo');
+ $this->assertEquals('foo', $this->getPrivateProperty($runner, 'name'));
+ }
+
+ public function testSetGroupStoresValue()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $runner->setGroup('foo');
+ $this->assertEquals('foo', $this->getPrivateProperty($runner, 'group'));
+ }
+
+ public function testSetNamespaceStoresValue()
+ {
+ $runner = new MigrationRunner($this->config);
+
+ $runner->setNamespace('foo');
+ $this->assertEquals('foo', $this->getPrivateProperty($runner, 'namespace'));
+ }
+
+ public function testFindMigrationsReturnsEmptyArrayWithNoneFound()
+ {
+ $config = $this->config;
+ $config->type = 'timestamp';
+ $runner = new MigrationRunner($config);
+
+ $runner->setPath($this->start);
+
+ $this->assertEquals([], $runner->findMigrations());
+ }
+
+ public function testFindMigrationsSuccessTimestamp()
+ {
+ $config = $this->config;
+ $config->type = 'timestamp';
+ $runner = new MigrationRunner($config);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::newFile('20180124102301_some_migration.php')->at($this->root);
+ vfsStream::newFile('20180124082302_another_migration.php')->at($this->root); // should be first
+ vfsStream::newFile('20180124082303_another_migration.py')->at($this->root); // shouldn't be included
+ vfsStream::newFile('201801240823_another_migration.py')->at($this->root); // shouldn't be included
+
+ $mig1 = (object)[
+ 'name' => 'some_migration',
+ 'path' => 'vfs://root//20180124102301_some_migration.php',
+ 'version' => '20180124102301',
+ ];
+ $mig2 = (object)[
+ 'name' => 'another_migration',
+ 'path' => 'vfs://root//20180124082302_another_migration.php',
+ 'version' => '20180124082302',
+ ];
+
+ $migrations = $runner->findMigrations();
+
+ $this->assertCount(2, $migrations);
+ $this->assertEquals($mig2, array_shift($migrations));
+ $this->assertEquals($mig1, array_shift($migrations));
+ }
+
+ public function testFindMigrationsSuccessOrder()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $runner = new MigrationRunner($config);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::newFile('002_some_migration.php')->at($this->root);
+ vfsStream::newFile('001_another_migration.php')->at($this->root); // should be first
+ vfsStream::newFile('003_another_migration.py')->at($this->root); // shouldn't be included
+ vfsStream::newFile('004_another_migration.py')->at($this->root); // shouldn't be included
+
+ $mig1 = (object)[
+ 'name' => 'some_migration',
+ 'path' => 'vfs://root//002_some_migration.php',
+ 'version' => '002',
+ ];
+ $mig2 = (object)[
+ 'name' => 'another_migration',
+ 'path' => 'vfs://root//001_another_migration.php',
+ 'version' => '001',
+ ];
+
+ $migrations = $runner->findMigrations();
+
+ $this->assertEquals($mig2, array_shift($migrations));
+ $this->assertEquals($mig1, array_shift($migrations));
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ * @expectedExceptionMessage There is a gap in the migration sequence near version number: 002
+ */
+ public function testVersionThrowsMigrationGapException()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $runner = new MigrationRunner($config);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::newFile('002_some_migration.php')->at($this->root);
+
+ $version = $runner->version(0);
+
+ $this->assertEquals($version, '002');
+ }
+
+ public function testVersionReturnsTrueWhenNothingToDo()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $runner = new MigrationRunner($config);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::newFile('001_some_migration.php')->at($this->root);
+
+ $version = $runner->version(0);
+
+ $this->assertTrue($version);
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ * @expectedExceptionMessage The migration class "App\Database\Migrations\Migration_some_migration" could not be found.
+ */
+ public function testVersionWithNoClassInFile()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $runner = new MigrationRunner($config);
+ $runner->setSilent(false);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::newFile('001_some_migration.php')->at($this->root);
+
+ $version = $runner->version(1);
+
+ $this->assertEquals('001', $version);
+ }
+
+ public function testVersionReturnsUpDownSuccess()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $runner = new MigrationRunner($config);
+ $runner->setSilent(false);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::copyFromFileSystem(
+ TESTPATH . '_support/Database/SupportMigrations',
+ $this->root
+ );
+
+ $version = $runner->version(1);
+
+ $this->assertEquals('001', $version);
+ $this->seeInDatabase('foo', ['key' => 'foobar']);
+
+ $version = $runner->version(0);
+
+ $this->assertTrue($version);
+ $this->assertFalse(db_connect()->tableExists('foo'));
+ }
+
+ public function testLatestSuccess()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $runner = new MigrationRunner($config);
+ $runner->setSilent(false);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::copyFromFileSystem(
+ TESTPATH . '_support/Database/SupportMigrations',
+ $this->root
+ );
+
+ $version = $runner->latest();
+
+ $this->assertEquals('001', $version);
+ $this->assertTrue(db_connect()->tableExists('foo'));
+ }
+
+ public function testCurrentSuccess()
+ {
+ $config = $this->config;
+ $config->type = 'sequential';
+ $config->currentVersion = 1;
+ $runner = new MigrationRunner($config);
+ $runner->setSilent(false);
+
+ $runner = $runner->setPath($this->start);
+
+ vfsStream::copyFromFileSystem(
+ TESTPATH . '_support/Database/SupportMigrations',
+ $this->root
+ );
+
+ $version = $runner->current();
+
+ $this->assertEquals('001', $version);
+ $this->assertTrue(db_connect()->tableExists('foo'));
+ }
+}
diff --git a/tests/system/EntityTest.php b/tests/system/EntityTest.php
index c279893f7a..45d5cd7381 100644
--- a/tests/system/EntityTest.php
+++ b/tests/system/EntityTest.php
@@ -174,7 +174,7 @@ class EntityTest extends \CIUnitTestCase
$time = $entity->created_at;
$this->assertInstanceOf(Time::class, $time);
- $this->assertEquals(date('Y-m-d H:i:s', $stamp), $time->format('Y-m-d H:i:s'));
+ $this->assertCloseEnoughString(date('Y-m-d H:i:s', $stamp), $time->format('Y-m-d H:i:s'));
}
public function testDateMutationFromDatetime()
@@ -186,7 +186,7 @@ class EntityTest extends \CIUnitTestCase
$time = $entity->created_at;
$this->assertInstanceOf(Time::class, $time);
- $this->assertEquals($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
+ $this->assertCloseEnoughString($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
}
public function testDateMutationFromTime()
@@ -198,7 +198,7 @@ class EntityTest extends \CIUnitTestCase
$time = $entity->created_at;
$this->assertInstanceOf(Time::class, $time);
- $this->assertEquals($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
+ $this->assertCloseEnoughString($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
}
public function testDateMutationStringToTime()
@@ -223,7 +223,7 @@ class EntityTest extends \CIUnitTestCase
$time = $this->getPrivateProperty($entity, 'created_at');
$this->assertInstanceOf(Time::class, $time);
- $this->assertEquals(date('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
+ $this->assertCloseEnoughString(date('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
}
public function testDateMutationDatetimeToTime()
@@ -236,7 +236,7 @@ class EntityTest extends \CIUnitTestCase
$time = $this->getPrivateProperty($entity, 'created_at');
$this->assertInstanceOf(Time::class, $time);
- $this->assertEquals($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
+ $this->assertCloseEnoughString($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
}
public function testDateMutationTimeToTime()
@@ -249,7 +249,7 @@ class EntityTest extends \CIUnitTestCase
$time = $this->getPrivateProperty($entity, 'created_at');
$this->assertInstanceOf(Time::class, $time);
- $this->assertEquals($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
+ $this->assertCloseEnoughString($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
}
//--------------------------------------------------------------------
@@ -385,9 +385,8 @@ class EntityTest extends \CIUnitTestCase
$this->assertEquals(['foo' => 'bar'], $entity->seventh);
}
-
//--------------------------------------------------------------------
-
+
public function testCastNullable()
{
$entity = $this->getCastNullableEntity();
@@ -461,6 +460,46 @@ class EntityTest extends \CIUnitTestCase
]);
}
+ public function testAsArrayOnlyChanged()
+ {
+ $entity = $this->getEntity();
+
+ $entity->bar = 'foo';
+
+ $result = $entity->toArray(true);
+
+ $this->assertEquals($result, [
+ 'bar' => 'bar:foo:bar',
+ ]);
+ }
+
+ public function testToRawArray()
+ {
+ $entity = $this->getEntity();
+
+ $result = $entity->toRawArray();
+
+ $this->assertEquals($result, [
+ 'foo' => null,
+ 'bar' => null,
+ 'default' => 'sumfin',
+ 'created_at' => null,
+ ]);
+ }
+
+ public function testToRawArrayOnlyChanged()
+ {
+ $entity = $this->getEntity();
+
+ $entity->bar = 'foo';
+
+ $result = $entity->toRawArray(true);
+
+ $this->assertEquals($result, [
+ 'bar' => 'bar:foo',
+ ]);
+ }
+
//--------------------------------------------------------------------
public function testFilledConstruction()
@@ -594,25 +633,23 @@ class EntityTest extends \CIUnitTestCase
}
};
}
-
-
-
+
protected function getCastNullableEntity()
{
return new class extends Entity
{
- protected $string_null = null;
+ protected $string_null = null;
protected $string_empty = null;
protected $integer_null = null;
- protected $integer_0 = null;
+ protected $integer_0 = null;
// 'bar' is db column, 'foo' is internal representation
protected $_options = [
'casts' => [
- 'string_null' => '?string',
- 'string_empty' => 'string',
- 'integner_null' => '?integer',
- 'integer_0' => 'integer'
+ 'string_null' => '?string',
+ 'string_empty' => 'string',
+ 'integner_null' => '?integer',
+ 'integer_0' => 'integer',
],
'dates' => [],
'datamap' => [],
@@ -620,5 +657,4 @@ class EntityTest extends \CIUnitTestCase
};
}
-
}
diff --git a/tests/system/Filters/CSRFTest.php b/tests/system/Filters/CSRFTest.php
new file mode 100644
index 0000000000..697c245348
--- /dev/null
+++ b/tests/system/Filters/CSRFTest.php
@@ -0,0 +1,46 @@
+config = new \Config\Filters();
+ }
+
+ //--------------------------------------------------------------------
+ public function testNormal()
+ {
+ $this->config->globals = [
+ 'before' => ['csrf'],
+ 'after' => [],
+ ];
+
+ $this->request = Services::request(null, false);
+ $this->response = Services::response();
+
+ $filters = new Filters($this->config, $this->request, $this->response);
+ $uri = 'admin/foo/bar';
+
+ // we expect CSRF requests to be ignored in CLI
+ $expected = $this->request;
+ $request = $filters->run($uri, 'before');
+ $this->assertEquals($expected, $request);
+ }
+
+}
diff --git a/tests/system/Filters/DebugToolbarTest.php b/tests/system/Filters/DebugToolbarTest.php
new file mode 100644
index 0000000000..5d8f7a774a
--- /dev/null
+++ b/tests/system/Filters/DebugToolbarTest.php
@@ -0,0 +1,52 @@
+request = Services::request();
+ $this->response = Services::response();
+ }
+
+ //--------------------------------------------------------------------
+
+ public function testDebugToolbarFilter()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+
+ $config = new FilterConfig();
+ $config->globals = [
+ 'before' => ['toolbar'], // not normal; exercising its before()
+ 'after' => ['toolbar'],
+ ];
+
+ $filter = new DebugToolbar();
+
+ $expectedBefore = $this->request;
+ $expectedAfter = $this->response;
+
+ // nothing should change here, since we have no before logic
+ $filter->before($this->request);
+ $this->assertEquals($expectedBefore, $this->request);
+
+ // nothing should change here, since we are running in the CLI
+ $filter->after($this->request, $this->response);
+ $this->assertEquals($expectedAfter, $this->response);
+ }
+
+}
diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php
index ae820f60af..a6bdbf175f 100644
--- a/tests/system/Filters/FiltersTest.php
+++ b/tests/system/Filters/FiltersTest.php
@@ -1,6 +1,7 @@
enableFilter('goggle', 'before');
}
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1664
+ */
+ public function testMatchesURICaseInsensitively()
+ {
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+
+ $config = [
+ 'globals' => [
+ 'before' => [
+ 'foo' => ['except' => 'Admin/*'],
+ 'bar'
+ ],
+ 'after' => [
+ 'foo' => ['except' => 'Admin/*'],
+ 'baz'
+ ],
+ ],
+ 'filters' => [
+ 'frak' => [
+ 'before' => ['Admin/*'],
+ 'after' => ['Admin/*'],
+ ],
+ ],
+ ];
+ $filters = new Filters((object) $config, $this->request, $this->response);
+ $uri = 'admin/foo/bar';
+
+ $expected = [
+ 'before' => [
+ 'bar',
+ 'frak',
+ ],
+ 'after' => [
+ 'baz',
+ 'frak',
+ ],
+ ];
+
+ $this->assertEquals($expected, $filters->initialize($uri)->getFilters());
+ }
+
}
diff --git a/tests/system/Filters/HoneypotTest.php b/tests/system/Filters/HoneypotTest.php
new file mode 100644
index 0000000000..29dfd4f052
--- /dev/null
+++ b/tests/system/Filters/HoneypotTest.php
@@ -0,0 +1,119 @@
+config = new \Config\Filters();
+ $this->honey = new \Config\Honeypot();
+
+ unset($_POST[$this->honey->name]);
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_POST[$this->honey->name] = 'hey';
+ }
+
+ //--------------------------------------------------------------------
+ public function testBeforeTriggered()
+ {
+ $this->config->globals = [
+ 'before' => ['honeypot'],
+ 'after' => [],
+ ];
+
+ $this->request = Services::request(null, false);
+ $this->response = Services::response();
+
+ $filters = new Filters($this->config, $this->request, $this->response);
+ $uri = 'admin/foo/bar';
+
+ $this->expectException(HoneypotException::class);
+ $request = $filters->run($uri, 'before');
+ }
+
+ //--------------------------------------------------------------------
+ public function testBeforeClean()
+ {
+ $this->config->globals = [
+ 'before' => ['honeypot'],
+ 'after' => [],
+ ];
+
+ unset($_POST[$this->honey->name]);
+ $this->request = Services::request(null, false);
+ $this->response = Services::response();
+
+ $expected = $this->request;
+
+ $filters = new Filters($this->config, $this->request, $this->response);
+ $uri = 'admin/foo/bar';
+
+ $request = $filters->run($uri, 'before');
+ $this->assertEquals($expected, $request);
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function testAfter()
+ {
+ $this->config->globals = [
+ 'before' => [],
+ 'after' => ['honeypot'],
+ ];
+
+ $this->request = Services::request(null, false);
+ $this->response = Services::response();
+
+ $filters = new Filters($this->config, $this->request, $this->response);
+ $uri = 'admin/foo/bar';
+
+ $this->response->setBody('');
+ $this->response = $filters->run($uri, 'after');
+ $this->assertContains($this->honey->name, $this->response->getBody());
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function testAfterNotApplicable()
+ {
+ $this->config->globals = [
+ 'before' => [],
+ 'after' => ['honeypot'],
+ ];
+
+ $this->request = Services::request(null, false);
+ $this->response = Services::response();
+
+ $filters = new Filters($this->config, $this->request, $this->response);
+ $uri = 'admin/foo/bar';
+
+ $this->response->setBody('
');
+ $this->response = $filters->run($uri, 'after');
+ $this->assertNotContains($this->honey->name, $this->response->getBody());
+ }
+
+}
diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php
index cd861ad008..958bfa5f45 100644
--- a/tests/system/HTTP/ContentSecurityPolicyTest.php
+++ b/tests/system/HTTP/ContentSecurityPolicyTest.php
@@ -22,9 +22,9 @@ class ContentSecurityPolicyTest extends \CIUnitTestCase
$this->csp = $this->response->CSP;
}
- protected function work()
+ protected function work(string $parm = 'Hello')
{
- $body = 'Hello';
+ $body = $parm;
$this->response->setBody($body);
$this->response->setCookie('foo', 'bar');
@@ -408,4 +408,53 @@ class ContentSecurityPolicyTest extends \CIUnitTestCase
$this->assertContains('upgrade-insecure-requests;', $result);
}
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function testBodyEmpty()
+ {
+ $this->prepare();
+ $body = '';
+ $this->response->setBody($body);
+ $this->csp->finalize($this->response);
+ $this->assertEquals($body, $this->response->getBody());
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function testBodyScriptNonce()
+ {
+ $this->prepare();
+ $body = 'Blah blah {csp-script-nonce} blah blah';
+ $this->response->setBody($body);
+ $this->csp->addScriptSrc('cdn.cloudy.com');
+
+ $result = $this->work($body);
+
+ $this->assertContains('nonce=', $this->response->getBody());
+ $result = $this->getHeaderEmitted('Content-Security-Policy');
+ $this->assertContains('nonce-', $result);
+ }
+
+ /**
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ */
+ public function testBodyStyleNonce()
+ {
+ $this->prepare();
+ $body = 'Blah blah {csp-style-nonce} blah blah';
+ $this->response->setBody($body);
+ $this->csp->addStyleSrc('cdn.cloudy.com');
+
+ $result = $this->work($body);
+
+ $this->assertContains('nonce=', $this->response->getBody());
+ $result = $this->getHeaderEmitted('Content-Security-Policy');
+ $this->assertContains('nonce-', $result);
+ }
+
}
diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php
index 269815a9ec..a3a0952b84 100644
--- a/tests/system/HTTP/IncomingRequestTest.php
+++ b/tests/system/HTTP/IncomingRequestTest.php
@@ -381,4 +381,12 @@ class IncomingRequestTest extends \CIUnitTestCase
$this->assertEquals(124, $gotit->getSize());
}
+ //--------------------------------------------------------------------
+
+ public function testSpoofing()
+ {
+ $this->request->setMethod('WINK');
+ $this->assertEquals('wink', $this->request->getMethod());
+ }
+
}
diff --git a/tests/system/Helpers/FilesystemHelperTest.php b/tests/system/Helpers/FilesystemHelperTest.php
index 5216f06a56..33fb2bc562 100644
--- a/tests/system/Helpers/FilesystemHelperTest.php
+++ b/tests/system/Helpers/FilesystemHelperTest.php
@@ -349,7 +349,7 @@ class FilesystemHelperTest extends \CIUnitTestCase
public function testRealPathResolved()
{
- $this->assertEquals(SUPPORTPATH . 'Helpers/', set_realpath(SUPPORTPATH . 'Files/../Helpers', true));
+ $this->assertEquals(SUPPORTPATH . 'Models/', set_realpath(SUPPORTPATH . 'Files/../Models', true));
}
}
diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php
index d9069f59cc..4c97b07062 100644
--- a/tests/system/Helpers/FormHelperTest.php
+++ b/tests/system/Helpers/FormHelperTest.php
@@ -35,7 +35,7 @@ class FormHelperTest extends \CIUnitTestCase
$Name = csrf_token();
$expected = <<
-
+
EOH;
}
@@ -73,7 +73,7 @@ EOH;
$Name = csrf_token();
$expected = <<
-
+
EOH;
}
@@ -110,7 +110,7 @@ EOH;
$Name = csrf_token();
$expected = <<
-
+
EOH;
}
@@ -147,8 +147,8 @@ EOH;
$Name = csrf_token();
$expected = <<
-
-
+
+
EOH;
}
@@ -156,7 +156,8 @@ EOH;
{
$expected = <<
-
+
+
EOH;
}
@@ -225,7 +226,7 @@ EOH;
$Name = csrf_token();
$expected = <<
-
+
EOH;
}
@@ -253,7 +254,7 @@ EOH;
{
$expected = << \n
+ \n
EOH;
$this->assertEquals($expected, form_hidden('username', 'johndoe'));
}
@@ -266,7 +267,7 @@ EOH;
];
$expected = <<
+
EOH;
$this->assertEquals($expected, form_hidden($data, null));
@@ -280,7 +281,7 @@ EOH;
];
$expected = <<
+
EOH;
$this->assertEquals($expected, form_hidden('name', $data));
diff --git a/tests/system/Helpers/NumberHelperTest.php b/tests/system/Helpers/NumberHelperTest.php
index e7e7f5d40f..cc4c94d20f 100755
--- a/tests/system/Helpers/NumberHelperTest.php
+++ b/tests/system/Helpers/NumberHelperTest.php
@@ -11,7 +11,7 @@ final class NumberHelperTest extends \CIUnitTestCase
helper('number');
}
- public function test_roman_number()
+ public function testRomanNumber()
{
$this->assertEquals('XCVI', number_to_roman(96));
$this->assertEquals('MMDCCCXCV', number_to_roman(2895));
@@ -27,12 +27,12 @@ final class NumberHelperTest extends \CIUnitTestCase
$this->assertEquals(null, number_to_roman(4000));
}
- public function test_format_number()
+ public function testFormatNumber()
{
$this->assertEquals('123,456', format_number(123456, 0, 'en_US'));
}
- public function test_format_number_with_precision()
+ public function testFormatNumberWithPrecision()
{
$this->assertEquals('123,456.8', format_number(123456.789, 1, 'en_US'));
$this->assertEquals('123,456.79', format_number(123456.789, 2, 'en_US'));
@@ -47,62 +47,62 @@ final class NumberHelperTest extends \CIUnitTestCase
$this->assertEquals('<<123,456.79>>', format_number(123456.789, 2, 'en_US', $options));
}
- public function test_number_to_size()
+ public function testNumberToSize()
{
$this->assertEquals('456 Bytes', number_to_size(456, 1, 'en_US'));
}
- public function test_kb_format()
+ public function testKbFormat()
{
$this->assertEquals('4.5 KB', number_to_size(4567, 1, 'en_US'));
}
- public function test_kb_format_medium()
+ public function testKbFormatMedium()
{
$this->assertEquals('44.6 KB', number_to_size(45678, 1, 'en_US'));
}
- public function test_kb_format_large()
+ public function testKbFormatLarge()
{
$this->assertEquals('446.1 KB', number_to_size(456789, 1, 'en_US'));
}
- public function test_mb_format()
+ public function testMbFormat()
{
$this->assertEquals('3.3 MB', number_to_size(3456789, 1, 'en_US'));
}
- public function test_gb_format()
+ public function testGbFormat()
{
$this->assertEquals('1.8 GB', number_to_size(1932735283.2, 1, 'en_US'));
}
- public function test_tb_format()
+ public function testTbFormat()
{
$this->assertEquals('112,283.3 TB', number_to_size(123456789123456789, 1, 'en_US'));
}
- public function test_thousands()
+ public function testThousands()
{
$this->assertEquals('123 thousand', number_to_amount('123,000', 0, 'en_US'));
}
- public function test_millions()
+ public function testMillions()
{
$this->assertEquals('123.4 million', number_to_amount('123,400,000', 1, 'en_US'));
}
- public function test_billions()
+ public function testBillions()
{
$this->assertEquals('123.46 billion', number_to_amount('123,456,000,000', 2, 'en_US'));
}
- public function test_trillions()
+ public function testTrillions()
{
$this->assertEquals('123.457 trillion', number_to_amount('123,456,700,000,000', 3, 'en_US'));
}
- public function test_quadrillions()
+ public function testQuadrillions()
{
$this->assertEquals('123.5 quadrillion', number_to_amount('123,456,700,000,000,000', 1, 'en_US'));
}
@@ -110,7 +110,7 @@ final class NumberHelperTest extends \CIUnitTestCase
/**
* @group single
*/
- public function test_currency_current_locale()
+ public function testCurrencyCurrentLocale()
{
$this->assertEquals('$1,234.56', number_to_currency(1234.56, 'USD', 'en_US'));
$this->assertEquals('£1,234.56', number_to_currency(1234.56, 'GBP', 'en_GB'));
diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php
index 039b94c1ec..5b9fe67cbb 100755
--- a/tests/system/Helpers/TextHelperTest.php
+++ b/tests/system/Helpers/TextHelperTest.php
@@ -16,7 +16,7 @@ class TextHelperTest extends \CIUnitTestCase
// --------------------------------------------------------------------
- public function test_strip_slashes()
+ public function testStripSlashes()
{
$expected = [
"Is your name O'reilly?",
@@ -30,7 +30,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// --------------------------------------------------------------------
- public function test_strip_quotes()
+ public function testStripQuotes()
{
$strs = [
'"me oh my!"' => 'me oh my!',
@@ -43,7 +43,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// --------------------------------------------------------------------
- public function test_quotes_to_entities()
+ public function testQuotesToEntities()
{
$strs = [
'"me oh my!"' => '"me oh my!"',
@@ -56,7 +56,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// --------------------------------------------------------------------
- public function test_reduce_double_slashes()
+ public function testReduceDoubleSlashes()
{
$strs = [
'http://codeigniter.com' => 'http://codeigniter.com',
@@ -70,7 +70,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// --------------------------------------------------------------------
- public function test_reduce_multiples()
+ public function testReduceMultiples()
{
$strs = [
'Fred, Bill,, Joe, Jimmy' => 'Fred, Bill, Joe, Jimmy',
@@ -91,7 +91,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// --------------------------------------------------------------------
- public function test_random_string()
+ public function testRandomString()
{
$this->assertEquals(16, strlen(random_string('alnum', 16)));
$this->assertEquals(16, strlen(random_string('alpha', 16)));
@@ -108,7 +108,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// --------------------------------------------------------------------
- public function test_increment_string()
+ public function testIncrementString()
{
$this->assertEquals('my-test_1', increment_string('my-test'));
$this->assertEquals('my-test-1', increment_string('my-test', '-'));
@@ -123,7 +123,7 @@ class TextHelperTest extends \CIUnitTestCase
// Functions from text_helper_test.php
// -------------------------------------------------------------------
- public function test_word_limiter()
+ public function testWordLimiter()
{
$this->assertEquals('Once upon a time,…', word_limiter($this->_long_string, 4));
$this->assertEquals('Once upon a time,…', word_limiter($this->_long_string, 4, '…'));
@@ -133,7 +133,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_character_limiter()
+ public function testCharacterLimiter()
{
$this->assertEquals('Once upon a time, a…', character_limiter($this->_long_string, 20));
$this->assertEquals('Once upon a time, a…', character_limiter($this->_long_string, 20, '…'));
@@ -142,7 +142,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_ascii_to_entities()
+ public function testAsciiToEntities()
{
$strs = [
'“‘ “test” ' => '“‘ “test” ',
@@ -155,7 +155,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_entities_to_ascii()
+ public function testEntitiesToAscii()
{
$strs = [
'“‘ “test” ' => '“‘ “test” ',
@@ -181,7 +181,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_convert_accented_characters()
+ public function testConvertAccentedCharacters()
{
//$this->ci_vfs_clone('application/Config/ForeignChars.php');
$this->assertEquals('AAAeEEEIIOOEUUUeY', convert_accented_characters('ÀÂÄÈÊËÎÏÔŒÙÛÜŸ'));
@@ -189,7 +189,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_censored_words()
+ public function testCensoredWords()
{
$censored = [
'boob',
@@ -214,14 +214,14 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_highlight_code()
+ public function testHighlightCode()
{
$expect = "\n<?php var_dump ( \$this ); ?> \n \n
";
$this->assertEquals($expect, highlight_code(''));
}
// ------------------------------------------------------------------------
- public function test_highlight_phrase()
+ public function testHighlightPhrase()
{
$strs = [
'this is a phrase' => 'this is a phrase',
@@ -238,7 +238,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_ellipsize()
+ public function testEllipsize()
{
$strs = [
'0' => [
@@ -313,7 +313,7 @@ class TextHelperTest extends \CIUnitTestCase
}
// ------------------------------------------------------------------------
- public function test_default_word_wrap_charlim()
+ public function testDefaultWordWrapCharlim()
{
$string = 'Here is a longer string of text that will help us demonstrate the default charlim of this function.';
$this->assertEquals(strpos(word_wrap($string), "\n"), 73);
@@ -321,7 +321,7 @@ class TextHelperTest extends \CIUnitTestCase
// -----------------------------------------------------------------------
- public function test_excerpt()
+ public function testExcerpt()
{
$string = $this->_long_string;
$result = ' Once upon a time, a framework had no tests. It sad So some nice people began to write tests. The more time that went on, the happier it became. ...';
@@ -330,7 +330,7 @@ class TextHelperTest extends \CIUnitTestCase
// -----------------------------------------------------------------------
- public function test_excerpt_radius()
+ public function testExcerptRadius()
{
$string = $this->_long_string;
$phrase = 'began';
@@ -340,7 +340,7 @@ class TextHelperTest extends \CIUnitTestCase
// -----------------------------------------------------------------------
- public function test_alternator()
+ public function testAlternator()
{
$phrase = ' scream! ';
$result = '';
@@ -352,7 +352,7 @@ class TextHelperTest extends \CIUnitTestCase
$this->assertEquals('I scream! you scream! we scream! I scream! ', $result);
}
- public function test_empty_alternator()
+ public function testEmptyAlternator()
{
$phrase = ' scream! ';
$result = '';
diff --git a/tests/system/Helpers/XMLHelperTest.php b/tests/system/Helpers/XMLHelperTest.php
new file mode 100644
index 0000000000..cc0693cfb6
--- /dev/null
+++ b/tests/system/Helpers/XMLHelperTest.php
@@ -0,0 +1,32 @@
+Here is a so-so paragraph & an entity ({).';
+ $expected = '<p>Here is a so-so paragraph & an entity ({).</p>';
+ $this->assertEquals($expected, xml_convert($original));
+ }
+
+ // --------------------------------------------------------------------
+ public function testConvertProtected()
+ {
+ $original = 'Here is a so&so; paragraph & an entity ({).
';
+ $expected = '<p>Here is a so&so; paragraph & an entity ({).</p>';
+ $this->assertEquals($expected, xml_convert($original, true));
+ }
+
+}
diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php
index b7346f45b8..38bb8ebe7c 100644
--- a/tests/system/Honeypot/HoneypotTest.php
+++ b/tests/system/Honeypot/HoneypotTest.php
@@ -7,8 +7,6 @@ use CodeIgniter\Filters\Filters;
use CodeIgniter\Honeypot\Exceptions\HoneypotException;
use CodeIgniter\Test\CIUnitTestCase;
-require_once __DIR__ . '/fixtures/HoneyTrap.php';
-
/**
* @backupGlobals enabled
*/
@@ -90,7 +88,7 @@ class HoneypotTest extends CIUnitTestCase
public function testHoneypotFilterBefore()
{
$config = [
- 'aliases' => ['trap' => 'CodeIgniter\Honeypot\fixtures\HoneyTrap'],
+ 'aliases' => ['trap' => '\CodeIgniter\Filters\Honeypot'],
'globals' => [
'before' => ['trap'],
'after' => [],
@@ -107,7 +105,7 @@ class HoneypotTest extends CIUnitTestCase
public function testHoneypotFilterAfter()
{
$config = [
- 'aliases' => ['trap' => 'CodeIgniter\Honeypot\fixtures\HoneyTrap'],
+ 'aliases' => ['trap' => '\CodeIgniter\Filters\Honeypot'],
'globals' => [
'before' => [],
'after' => ['trap'],
diff --git a/tests/system/Honeypot/fixtures/HoneyTrap.php b/tests/system/Honeypot/fixtures/HoneyTrap.php
deleted file mode 100644
index 8cc3aceef9..0000000000
--- a/tests/system/Honeypot/fixtures/HoneyTrap.php
+++ /dev/null
@@ -1,43 +0,0 @@
-hasContent($request))
- {
- throw HoneypotException::isBot();
- }
- }
-
- /**
- * Attach a honypot to the current response.
- *
- * @param CodeIgniter\HTTP\RequestInterface $request
- * @param CodeIgniter\HTTP\ResponseInterface $response
- * @return mixed
- */
- public function after(RequestInterface $request, ResponseInterface $response)
- {
- $honeypot = new Honeypot(new \Config\Honeypot());
- $honeypot->attachHoneypot($response);
- }
-
-}
diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php
index 0db95986df..0a876c4a81 100644
--- a/tests/system/I18n/TimeTest.php
+++ b/tests/system/I18n/TimeTest.php
@@ -167,7 +167,7 @@ class TimeTest extends \CIUnitTestCase
{
$time = Time::createFromTime(10, 03, 05, 'Europe/London');
- $this->assertEquals(date('Y-m-d 10:03:05'), $time->toDateTimeString());
+ $this->assertCloseEnoughString(date('Y-m-d 10:03:05'), $time->toDateTimeString());
}
public function testCreateFromFormat()
@@ -177,7 +177,7 @@ class TimeTest extends \CIUnitTestCase
Time::setTestNow($now);
$time = Time::createFromFormat('F j, Y', 'January 15, 2017', 'America/Chicago');
- $this->assertEquals(date('2017-01-15 H:i:s', $now->getTimestamp()), $time->toDateTimeString());
+ $this->assertCloseEnoughString(date('2017-01-15 H:i:s', $now->getTimestamp()), $time->toDateTimeString());
Time::setTestNow();
}
diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php
index 963fc24f6b..2f1f94634c 100644
--- a/tests/system/Language/LanguageTest.php
+++ b/tests/system/Language/LanguageTest.php
@@ -236,7 +236,6 @@ class LanguageTest extends \CIUnitTestCase
['Cast'],
['Core'],
['Database'],
- ['Email'],
['Files'],
['Filters'],
['Format'],
diff --git a/tests/system/Log/FileHandlerTest.php b/tests/system/Log/FileHandlerTest.php
index fa64c5136b..c2282a7a88 100644
--- a/tests/system/Log/FileHandlerTest.php
+++ b/tests/system/Log/FileHandlerTest.php
@@ -1,4 +1,5 @@
-add('/objects/(:alphanum)', 'Admin::objectsList/$1', ['subdomain' => 'adm']);
+ $routes->add('/objects/(:alphanum)', 'App::objectsList/$1');
+
+ $expects = [
+ 'objects/([a-zA-Z0-9]+)' => '\Admin::objectsList/$1',
+ ];
+
+ $this->assertEquals($expects, $routes->getRoutes());
+ }
+
+ //--------------------------------------------------------------------
+
+ /**
+ * @see https://github.com/codeigniter4/CodeIgniter4/issues/1692
+ */
+ public function testWithSubdomainOrdered()
+ {
+ $routes = $this->getCollector();
+
+ $_SERVER['HTTP_HOST'] = 'adm.example.com';
+
+ $routes->add('/objects/(:alphanum)', 'App::objectsList/$1');
$routes->add('/objects/(:alphanum)', 'Admin::objectsList/$1', ['subdomain' => 'adm']);
$expects = [
diff --git a/tests/system/Test/DOMParserTest.php b/tests/system/Test/DOMParserTest.php
index 07c42c20f6..8bd9b1479b 100644
--- a/tests/system/Test/DOMParserTest.php
+++ b/tests/system/Test/DOMParserTest.php
@@ -1,7 +1,5 @@
validation = new Validation((object) $this->config, \Config\Services::renderer());
+ $this->validation = new Validation((object)$this->config, \Config\Services::renderer());
$this->validation->reset();
$_FILES = [];
@@ -51,7 +51,7 @@ class RulesTest extends \CIUnitTestCase
];
$this->validation->setRules([
- 'foo' => 'required',
+ 'foo' => 'required|alpha',
]);
$this->assertFalse($this->validation->run($data));
@@ -77,6 +77,7 @@ class RulesTest extends \CIUnitTestCase
public function testRequiredFalseString()
{
$data = [
+ 'foo' => null,
'bar' => 123,
];
@@ -160,7 +161,7 @@ class RulesTest extends \CIUnitTestCase
],
[
['foo' => 'required'],
- [],
+ ['foo' => null],
false,
],
[
@@ -387,11 +388,12 @@ class RulesTest extends \CIUnitTestCase
public function testIsUniqueFalse()
{
$db = Database::connect();
- $db->table('user')->insert([
- 'name' => 'Derek Travis',
- 'email' => 'derek@world.com',
- 'country' => 'Elbonia',
- ]);
+ $db->table('user')
+ ->insert([
+ 'name' => 'Derek Travis',
+ 'email' => 'derek@world.com',
+ 'country' => 'Elbonia',
+ ]);
$data = [
'email' => 'derek@world.com',
@@ -431,15 +433,15 @@ class RulesTest extends \CIUnitTestCase
{
$db = Database::connect();
$user = $db->table('user')
- ->insert([
- 'name' => 'Developer A',
- 'email' => 'deva@example.com',
- 'country' => 'Elbonia',
- ]);
+ ->insert([
+ 'name' => 'Developer A',
+ 'email' => 'deva@example.com',
+ 'country' => 'Elbonia',
+ ]);
$row = $db->table('user')
- ->limit(1)
- ->get()
- ->getRow();
+ ->limit(1)
+ ->get()
+ ->getRow();
$data = [
'email' => 'derek@world.co.uk',
diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php
index 67023d0aa6..53e33028a4 100644
--- a/tests/system/Validation/ValidationTest.php
+++ b/tests/system/Validation/ValidationTest.php
@@ -349,6 +349,25 @@ class ValidationTest extends \CIUnitTestCase
//--------------------------------------------------------------------
+ public function testSetRulesRemovesErrorsArray()
+ {
+ $rules = [
+ 'foo' => [
+ 'label' => 'Foo Bar',
+ 'rules' => 'min_length[10]',
+ 'errors' => [
+ 'min_length' => 'The {field} field is very short.',
+ ],
+ ],
+ ];
+
+ $this->validation->setRules($rules, []);
+
+ $this->validation->run(['foo' => 'abc']);
+
+ $this->assertEquals('The Foo Bar field is very short.', $this->validation->getError('foo'));
+ }
+
public function testInvalidRule()
{
$this->expectException(ValidationException::class);
diff --git a/tests/system/View/ParserPluginTest.php b/tests/system/View/ParserPluginTest.php
index ad3d429c82..5ab9e11881 100644
--- a/tests/system/View/ParserPluginTest.php
+++ b/tests/system/View/ParserPluginTest.php
@@ -72,6 +72,24 @@ class ParserPluginTest extends \CIUnitTestCase
$this->assertEquals($this->setHints($this->validator->showError('email')), $this->setHints($this->parser->renderString($template)));
}
+ public function testRoute()
+ {
+ // prime the pump
+ $routes = service('routes');
+ $routes->add('path/(:any)/to/(:num)', 'myController::goto/$1/$2');
+
+ $template = '{+ route myController::goto string 13 +}';
+
+ $this->assertEquals('/path/string/to/13', $this->parser->renderString($template));
+ }
+
+ public function testSiteURL()
+ {
+ $template = '{+ siteURL +}';
+
+ $this->assertEquals('http://example.com/index.php', $this->parser->renderString($template));
+ }
+
public function testValidationErrorsList()
{
$this->validator->setError('email', 'Invalid email address');
diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php
index 87c3d443be..c059b6a0cc 100644
--- a/tests/system/View/ParserTest.php
+++ b/tests/system/View/ParserTest.php
@@ -224,6 +224,31 @@ class ParserTest extends \CIUnitTestCase
$this->assertEquals("Super Heroes\nTom Dick Henry ", $parser->renderString($template));
}
+ public function testParseLoopObjectProperties()
+ {
+ $parser = new Parser($this->config, $this->viewsDir, $this->loader);
+ $obj1 = new stdClass();
+ $obj1->name = 'Tom';
+ $obj2 = new stdClass();
+ $obj2->name = 'Dick';
+ $obj3 = new stdClass();
+ $obj3->name = 'Henry';
+
+ $data = [
+ 'title' => 'Super Heroes',
+ 'powers' => [
+ $obj1,
+ $obj2,
+ $obj3,
+ ],
+ ];
+
+ $template = "{title}\n{powers}{name} {/powers}";
+
+ $parser->setData($data, 'html');
+ $this->assertEquals("Super Heroes\nTom Dick Henry ", $parser->renderString($template));
+ }
+
// --------------------------------------------------------------------
public function testParseLoopEntityProperties()
@@ -242,6 +267,7 @@ class ParserTest extends \CIUnitTestCase
],
];
}
+
};
$parser = new Parser($this->config, $this->viewsDir, $this->loader);
@@ -258,6 +284,44 @@ class ParserTest extends \CIUnitTestCase
$this->assertEquals("Super Heroes\n bar baz first second ", $parser->renderString($template));
}
+ public function testParseLoopEntityObjectProperties()
+ {
+ $power = new class extends \CodeIgniter\Entity
+ {
+
+ public $foo = 'bar';
+ protected $bar = 'baz';
+ protected $obj1 = null;
+ protected $obj2 = null;
+ public $bobbles = [];
+
+ public function __construct()
+ {
+ $this->obj1 = new stdClass();
+ $this->obj2 = new stdClass();
+ $this->obj1->name = 'first';
+ $this->obj2->name = 'second';
+ $this->bobbles = [
+ $this->obj1,
+ $this->obj2,
+ ];
+ }
+ };
+
+ $parser = new Parser($this->config, $this->viewsDir, $this->loader);
+ $data = [
+ 'title' => 'Super Heroes',
+ 'powers' => [
+ $power
+ ],
+ ];
+
+ $template = "{title}\n{powers} {foo} {bar} {bobbles}{name} {/bobbles}{/powers}";
+
+ $parser->setData($data, 'html');
+ $this->assertEquals("Super Heroes\n bar baz first second ", $parser->renderString($template));
+ }
+
// --------------------------------------------------------------------
public function testMismatchedVarPair()
diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php
index aa5056fa2d..bfc43d9f70 100644
--- a/tests/system/View/ViewTest.php
+++ b/tests/system/View/ViewTest.php
@@ -266,4 +266,33 @@ class ViewTest extends \CIUnitTestCase
$this->assertEquals(0, count($view->getPerformanceData()));
}
+ public function testRenderLayoutExtendsCorrectly()
+ {
+ $view = new View($this->config, $this->viewsDir, $this->loader);
+
+ $view->setVar('testString', 'Hello World');
+ $expected = "Open
\nHello World ";
+
+ $this->assertContains($expected, $view->render('extend'));
+ }
+
+ public function testRenderLayoutMakesDataAvailableToBoth()
+ {
+ $view = new View($this->config, $this->viewsDir, $this->loader);
+
+ $view->setVar('testString', 'Hello World');
+ $expected = "Open
\nHello World \nHello World
";
+
+ $this->assertContains($expected, $view->render('extend'));
+ }
+
+ public function testRenderLayoutSupportsMultipleOfSameSection()
+ {
+ $view = new View($this->config, $this->viewsDir, $this->loader);
+
+ $view->setVar('testString', 'Hello World');
+ $expected = "First
\nSecond
";
+
+ $this->assertContains($expected, $view->render('extend_two'));
+ }
}
diff --git a/tests/system/View/Views/extend.php b/tests/system/View/Views/extend.php
new file mode 100644
index 0000000000..e7447a5d4c
--- /dev/null
+++ b/tests/system/View/Views/extend.php
@@ -0,0 +1,5 @@
+= $this->extend('layout') ?>
+
+= $this->section('content') ?>
+= $testString ?>
+= $this->endSection() ?>
diff --git a/tests/system/View/Views/extend_two.php b/tests/system/View/Views/extend_two.php
new file mode 100644
index 0000000000..86647fe45d
--- /dev/null
+++ b/tests/system/View/Views/extend_two.php
@@ -0,0 +1,10 @@
+= $this->extend('layout') ?>
+
+= $this->section('content') ?>
+First
+= $this->endSection() ?>
+
+
+= $this->section('content') ?>
+Second
+= $this->endSection() ?>
diff --git a/tests/system/View/Views/layout.php b/tests/system/View/Views/layout.php
new file mode 100644
index 0000000000..853950b56a
--- /dev/null
+++ b/tests/system/View/Views/layout.php
@@ -0,0 +1,3 @@
+Open
+= $this->renderSection('content') ?>
+= $testString ?>
diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/citheme.css b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/citheme.css
index 192af2004d..1fba8ca5e7 100644
--- a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/citheme.css
+++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/citheme.css
@@ -70,4 +70,15 @@ div#pulldown-menu {
font-weight: 300;
font-family: Lucida Grande,Verdana,Geneva,sans-serif;
color: #aaaaaa;
+}
+
+/* override table width restrictions */
+.wy-table-responsive table td, .wy-table-responsive table th {
+ white-space: normal;
+}
+
+.wy-table-responsive {
+ margin-bottom: 24px;
+ max-width: 100%;
+ overflow: visible;
}
\ No newline at end of file
diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst
index 8fcbb32828..7c23b4ea32 100644
--- a/user_guide_src/source/changelogs/index.rst
+++ b/user_guide_src/source/changelogs/index.rst
@@ -12,6 +12,52 @@ Release Date: Not Released
:doc:`See all the changes. `
+Version 4.0.0-beta.1
+====================================================
+
+Release Date: Unreleased
+
+Highlights:
+
+- New View Layouts provide simple way to create site site view templates.
+- Fixed user guide CSS for proper wide table display
+- Converted UploadedFile to use system messages
+- Numerous database, migration & model bugs fixed
+- Refactored unit testing for appstarter & framework distributions
+
+New messages:
+
+- Database.tableNotFound
+- HTTP.uploadErr...
+
+App changes:
+
+- app/Config/Cache has new setting: database
+- app/Views/welcome_message has logo tinted
+- composer.json has a case correction
+- env adds CI_ENVIRONMENT suggestion
+
+:doc:`See all the changes. `
+
+Version 4.0.0-alpha.5
+====================================================
+
+Release Date: January 30, 2019
+
+**Alpha 5**
+
+Highlights:
+
+- updated PHP dependency to 7.2
+- new feature branches have been created for the email and queue modules,
+ so they don't impact the release of 4.0.0
+- dropped several language messages that were unused (eg Migrations.missingTable)
+ and added some new (eg Migrations.invalidType)
+- lots of bug fixes
+- code coverage is up to 78%
+
+:doc:`See all the changes. `
+
Version 4.0.0-alpha.4
====================================================
@@ -73,8 +119,8 @@ Release Date: September 28, 2018
Non-code changes:
- User Guide adapted or rewritten
- - [System message translations repository](https://github.com/bcit-ci/CodeIgniter4-translations)
- - [Roadmap subforum](https://forum.codeigniter.com/forum-33.html) for more transparent planning
+ - `System message translations repository `_
+ - `Roadmap subforum `_ for more transparent planning
New core classes:
- CodeIgniter (bootstrap)
@@ -93,6 +139,8 @@ Some new, some old & some borrowed packages, all namespaced.
:titlesonly:
next
+ v4.0.0-alpha.5
+ v4.0.0-alpha.4
v4.0.0-alpha.3
v4.0.0-alpha.2
v4.0.0-alpha.1
diff --git a/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst b/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst
index 8809d4512a..2b6819a610 100644
--- a/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst
+++ b/user_guide_src/source/changelogs/v4.0.0-alpha.3.rst
@@ -307,7 +307,7 @@ PRs merged:
- #1460 remove unneeded ternary check at HoneyPot
- #1457 use $paths->systemDirectory in public/index.php
- #1456 Beef up HTTP URI & Response testing
-- #1455 un-ignore application/Database/Migrations directory
+- #1455 un-ignore app/Database/Migrations directory
- #1454 add missing break; in loop at Email::getEncoding()
- #1453 BugFix if there extension has only one mime type
- #1451 remove unneeded $session->start(); check on RedirectResponse
@@ -318,7 +318,7 @@ PRs merged:
- #1445 using existing is_cli() function in HTTP\IncomingRequest
- #1444 Dox for reorganized repo admin (4 of 4)
- #1443 Fixes unit test output not captured
-- #1442 remove form view in application/View/ and form helper usage in create new items tutorial
+- #1442 remove form view in app/View/ and form helper usage in create new items tutorial
- #1440 Access to model's last inserted ID
- #1438 Tailor the last few repo org names (3 of 4)
- #1437 Replace repo org name in MOST php docs (2 of 4)
diff --git a/user_guide_src/source/changelogs/v4.0.0-alpha.5.rst b/user_guide_src/source/changelogs/v4.0.0-alpha.5.rst
new file mode 100644
index 0000000000..ecdd266ef0
--- /dev/null
+++ b/user_guide_src/source/changelogs/v4.0.0-alpha.5.rst
@@ -0,0 +1,265 @@
+Version 4.0.0-alpha.5
+====================================================
+
+Release Date: Jan 30, 2019
+
+**Next alpha release of CodeIgniter4**
+
+Highlights:
+
+- added $maxQueries setting to app/Config/Toolbar.php
+- updated PHP dependency to 7.2
+- new feature branches have been created for the email and queue modules, so they don't impact the release of 4.0.0
+- dropped several language messages that were unused (eg Migrations.missingTable) and added some new (eg Migrations.invalidType)
+- lots of bug fixes, especially for the database support
+- provided filters (CSRF, Honeypot, DebugToolbar) have been moved from app/Filters/ to system/Filters/
+- revisited the installation and tutorial sections of the user guide
+- code coverage is at 77% ... getting ever closer to our target of 80% :)
+
+We hope this will be the last alpha, and that the next pre-release will be our first beta ... fingers crossed!
+
+The list of changed files follows, with PR numbers shown.
+
+- admin/
+ - starter/
+ - README.md #1637
+ - app/Config/Paths.php #1685
+ - release-appstarter #1685
+
+- app/
+ - Config/
+ - Filters #1686
+ - Modules #1665
+ - Services #614216
+ - Toolbar
+
+- contributing/
+ - guidelines.rst #1671, #1673
+ - internals.rst #1671
+
+- public/
+ - index.php #1648, #1670
+
+- system/
+ - Autoloader/
+ - Autoloader #1665, #1672
+ - FileLocator #1665
+ - Commands/
+ - Database/MigrationRollback #1683
+ - Config/
+ - BaseConfig #1635
+ - BaseService #1635, #1665
+ - Paths #1626
+ - Services #614216, #3a4ade, #1643
+ - View #1616
+ - Database/
+ - BaseBuilder #1640, #1663, #1677
+ - BaseConnection #1677
+ - Config #6b8b8b, #1660
+ - MigrationRunner #81d371, #1660
+ - Query #1677
+ - Database/Postgre/
+ - Builder #d2b377
+ - Debug/Toolbar/Collectors/
+ - Logs #1654
+ - Views #3a4ade
+ - Events/
+ - Events #1635
+ - Exceptions/
+ - ConfigException #1660
+ - Files/
+ - Exceptions/FileException #1636
+ - File #1636
+ - Filters/
+ - Filters #1635, #1625, #6dab8f
+ - CSRF #1686
+ - DebugToolbar #1686
+ - Honeypot #1686
+ - Helpers/
+ - form_helper #1633
+ - html_helper #1538
+ - xml_helper #1641
+ - HTTP/
+ - ContentSecurityPolicy #1641, #1642
+ - URI #2e698a
+ - Language/
+ - /en/Files #1636
+ - Language #1641
+ - Log/
+ - Handlers/FileHandler #1641
+ - Router/
+ - RouteCollection #1665, #5951c3
+ - Router #9e435c, #7993a7, #1678
+ - Session/
+ - Handlers/BaseHandler #1684
+ - Handlers/FileHandler #1684
+ - Handlers/MemcachedHandler #1679
+ - Session #1679
+ - bootstrap #81d371, #1665
+ - Common #1660
+ - Entity #1623, #1622
+ - Model #1617, #1632, #1656, #1689
+
+- tests/
+ - README.md #1671
+
+- tests/system/
+ - API/
+ - ResponseTraitTest #1635
+ - Autoloader/
+ - AutoloaderTest #1665
+ - FileLocatorTest #1665, #1686
+ - CLI/
+ - CommandRunnerTest #1635
+ - CommandsTest #1635
+ - Config/
+ - BaseConfigTest #1635
+ - ConfigTest #1643
+ - ServicesTest #1635, #1643
+ - Database/Builder/
+ - AliasTest #bea1dd
+ - DeleteTest #1677
+ - GroupTest #1640
+ - InsertTest #1640, #1677
+ - LikeTest #1640, #1677
+ - SelectTest #1663
+ - UpdateTest #1640, #1677
+ - WhereTest #1640, #1677
+ - Database/Live/
+ - AliasTest #1675
+ - ConnectTest #1660, #1675
+ - ForgeTest #6b8b8b
+ - InsertTest #1677
+ - Migrations/MigrationRunnerTest #1660, #1675
+ - ModelTest #1617, #1689
+ - Events/
+ - EventTest #1635
+ - Filters/
+ - CSRFTest #1686
+ - DebugToolbarTest #1686
+ - FiltersTest #1635, #6dab8f, #1686
+ - HoneypotTest #1686
+ - Helpers/
+ - FormHelperTest #1633
+ - XMLHelperTest #1641
+ - Honeypot/
+ - HoneypotTest #1686
+ - HTTP/
+ - ContentSecurityPolicyTest #1641
+ - IncomingRequestTest #1641
+ - Language/
+ - LanguageTest #1643
+ - Router/
+ - RouteCollectionTest #5951c3
+ - RouterTest #9e435c
+ - Validation/
+ - RulesTest #1689
+ - View/
+ - ParserPluginTest #1669
+ - ParserTest #1669
+
+- user_guide_src/
+
+ - concepts/
+ - autoloader #1665
+ - structure #1648
+ - database/
+ - connecting #1660
+ - transactions #1645
+ - general/
+ - configuration #1643
+ - managing_apps #5f305a, #1648
+ - modules #1613, #1665
+ - helpers/
+ - form_helper #1633
+ - incoming/
+ - filters #1686
+ - index #4a1886
+ - methodspoofing #4a1886
+ - installation/
+ - index #1690, #1693
+ - installing_composer #1673, #1690
+ - installing_git #1673, #1690
+ - installing_manual #1673, #1690
+ - repositories #1673, #1690
+ - running #1690, #1691
+ - troubleshooting #1690, #1693
+ - libraries/
+ - honeypot #1686
+ - index #1643, #1690
+ - throttler #1686
+ - tutorial/
+ - create_news_item #1693
+ - index #1693
+ - news_section #1693
+ - static_pages #1693
+
+- composer.json #1670
+- contributing.md #1670
+- README.md #1670
+- spark #1648
+- .travis.yml #1649, #1670
+
+PRs merged:
+-----------
+
+- #1693 Docs/tutorial
+- #5951c3 Allow domain/sub-domain routes to overwrite existing routes
+- #1691 Update the running docs
+- #1690 Rework install docs
+- #bea1dd Additional AliasTests for potential LeftJoin issue
+- #1689 Model Validation Fix
+- #1687 Add copyright blocks to filters
+- #1686 Refactor/filters
+- #1685 Fix admin - app starter creation
+- #1684 Updating session id cleanup for filehandler
+- #1683 Fix migrate:refresh bug
+- #d2b377 Fix Postgres replace command to work new way of storing binds
+- #4a1886 Document method spoofing
+- #2e698a urldecode URI keys as well as values.
+- #1679 save_path - for memcached
+- #1678 fix route not replacing forward slashes
+- #1677 Implement Don't Escape feature for db engine
+- #1675 Add missing test group directives
+- #1674 Update changelog
+- #1673 Updated download & installation docs
+- #1672 Update Autoloader.php
+- #1670 Update PHP dependency to 7.2
+- #1671 Update docs
+- #1669 Enhance Parser & Plugin testing
+- #1665 Composer PSR4 namespaces are now part of the modules auto-discovery
+- #6dab8f Filters match case-insensitively
+- #1663 Fix bind issue that occurred when using whereIn
+- #1660 Migrations Tests and database tweaks
+- #1656 DBGroup in __get(), allows to validate "database" data outside the model
+- #1654 Toolbar - Return Logger::$logCache items
+- #1649 remove php 7.3 from "allow_failures" in travis config
+- #1648 Update "managing apps" docs
+- #1645 Fix transaction enabling confusing (docu)
+- #1643 Remove email module
+- #1642 CSP nonce attribute value in ""
+- #81d371 Safety checks for config files during autoload and migrations
+- #1641 More unit testing tweaks
+- #1640 Update getCompiledX methods in BaseBuilder
+- #1637 Fix starter README
+- #1636 Refactor Files module
+- #5f305a UG - Typo in managing apps
+- #1635 Unit testing enhancements
+- #1633 Uses csrf_field and form_hidden
+- #1632 DBGroup should be passed to ->run instead of ->setRules
+- #1631 move use statement after License doc at UploadedFile class
+- #1630 Update copyright to 2019
+- #1629 "application" to "app" directory doc and comments
+- #3a4ade view() now properly reads the app config again
+- #7993a7 Final piece to get translateURIDashes working appropriately
+- #9e435c TranslateURIDashes fix
+- #1626 clean up Paths::$viewDirectory property
+- #1625 After matches is not set empty
+- #1623 Property was not cast if was defined as nullable
+- #1622 Nullable support for __set
+- #1617 countAllResults() should respect soft deletes
+- #1616 Fix View config merge order
+- #614216 Moved honeypot service out of the app Services file to the system Services where it belongs
+- #6b8b8b Allow db forge and utils to take an array of connection info instead of a group name
+- #1613 Typo in documentation
+- #1538 img fix(?) - html_helper
diff --git a/user_guide_src/source/changelogs/v4.0.0-beta.1.rst b/user_guide_src/source/changelogs/v4.0.0-beta.1.rst
new file mode 100644
index 0000000000..af85242bbd
--- /dev/null
+++ b/user_guide_src/source/changelogs/v4.0.0-beta.1.rst
@@ -0,0 +1,179 @@
+Version 4.0.0-beta.1
+====================================================
+
+Release Date: Not released
+
+Highlights:
+
+- New View Layouts provide simple way to create site site view templates.
+- Fixed user guide CSS for proper wide table display
+- Converted UploadedFile to use system messages
+- Numerous database, migration & model bugs fixed
+- Refactored unit testing for appstarter & framework distributions
+
+New messages:
+
+- Database.tableNotFound
+- HTTP.uploadErr...
+
+App changes:
+
+- app/Config/Cache has new setting: database
+- app/Views/welcome_message has logo tinted
+- composer.json has a case correction
+- env adds CI_ENVIRONMENT suggestion
+
+The list of changed files follows, with PR numbers shown.
+
+- app/
+ - Config/
+ - Cache #1719
+ - Views/
+ - welome_message #1774
+
+- system/
+ - Cache/Handlers/
+ - RedisHandler #1719, #1723
+ - Config/
+ - Config #37dbc1
+ - Services #1704, #37dbc1
+ - Database/
+ - Exceptions/DatabaseException #1739
+ - Postgre/
+ - Builder #1733
+ - SQLite3/
+ - Connection #1739
+ - Forge #1739
+ - Table #1739
+ - BaseBuilder #36fbb8, #549d7d
+ - BaseConnection #549d7d, #1739
+ - Forge #1739
+ - MigrationRunner #1743
+ - Query #36fbb8
+ - Seeder #1722
+ - Debug/
+ - Exceptions #1704
+ - Files/
+ - UploadedFile #1708
+ - Helpers/
+ - date_helper #1768
+ - number_helper #1768
+ - security_helper #1768
+ - text_helper #1768
+ - url_helper #1768
+ - HTTP/
+ - Request #1725
+ - Language/en/
+ - Database #1739
+ - HTTP #1708
+ - View #1757
+ - Router/
+ - RouteCollection #1709, #1732
+ - Router #1764
+ - Test/
+ - ControllerResponse #1740
+ - ControllerTester #1740
+ - DOMParser #1740
+ - FeatureResponse #1740
+ - Validation/
+ - Rules #1738, #1743
+ - Validation #37dbc1, #1763
+ - View/
+ - View #1729
+ - Common #1741
+ - Entity #6e549a, #1739
+ - Model #4f4a37, #6e549a, #37dbc1, #1712, #1763
+
+- tests/system/
+ - Database/
+ - BaseQueryTest #36fbb8
+ - Live/
+ - SQLite3/AlterTableTest #1739, #1740
+ - ForgeTest #1739, #1745
+ - ModelTest #37dbc1, #4ff1f5, #1763
+ - Migrations/MigrationRunnerTest #1743
+ - Helpers/
+ - FilesystemHelperTest #1740
+ - I18n/
+ - TimeTest # 1736
+ - Test/
+ - DOMParserTest #1740
+ - Validation/
+ - ValidationTest #1763
+ - View/
+ - ViewTest #1729
+ - EntityTest #6e549a, #1736
+
+- user_guide_src/
+ - _themes/.../
+ - citheme.css #1696
+ - changelogs/
+ - v4.0.0-alpha.5 #1699
+ - database/
+ - migrate #1696
+ - dbmgmt/
+ - forge #1751
+ - installation/
+ - install_manual #1699
+ - running #1750
+ - intro/
+ - psr #1752
+ - libraries/
+ - caching #1719
+ - validation #1742
+ - models/
+ - entities #1744
+ - outgoing/
+ - index #1729
+ - view_layouts #1729
+ - testing/
+ - controllers #1740
+ - tutorial/
+ - static_pages #1763
+
+- composer.json #1755
+- .env #1749
+
+PRs merged:
+-----------
+
+- #1774 Housekeeping for beta.1
+- #1768 Helper changes - signatures & typos
+- #1764 Fix routing when no default route has been specified. Fixes #1758
+- #1763 Ensure validation works in Model with errors as part of rules. Fixes #1574
+- #1757 Correct the unneeded double-quote (typo)
+- #1755 lowercase 'vfsStream' in composer files
+- #1752 Fixed typo preventing link format
+- #1751 Guide: Moving misplaced text under correct heading
+- #1750 Remove reference to Encryption Key in User Guide
+- #1749 Adding environment to .env
+- #1745 Updated composite key tests for SQLite3 support. Fixes #1478
+- #1744 Update entity docs for current framework state. Fixes #1727
+- #1743 Manually sort migrations found instead of relying on the OS. Fixes #1666
+- #1742 Fix required_without rule bug.
+- #1741 Helpers with a specific namespace can be loaded now. Fixes #1726
+- #1740 Refactor test support for app starter
+- #1739 Fix typo
+- #1738 Fix required_with rule bug. Fixes #1728
+- #1737 Added support for dropTable and modifyTable with SQLite driver
+- #1736 Accommodate long travis execution times
+- #1733 Fix increment and decrement errors with Postgres
+- #1732 Don't check from CLI in Routes. Fixes #1724
+- #1729 New View Layout functionality for simple template
+- #1725 Update Request.php
+- #1723 Log an error if redis authentication is failed
+- #1722 Seeder adds default namespace to seeds
+- #1719 Update Cache RedisHandler to support select database
+- #4ff1f5 Additional tests for inserts and required validation failing (#1717)
+- #549d7d Another try at getting escaping working correctly both when in and out of models
+- #1712 Minor readability changes
+- #37dbc1 Ensure Model validation rules can be a group name
+- #1709 Fix resource routing websafe method order checking
+- #1708 Language for UploadedFile
+- #36fbb8 BaseBuilder should only turn off Connection's setEscapeFlags when running a query...
+- #6e549a Provide default baseURL that works with the development server for easier first time setup (Fixes #1646)
+- #1704 Fix viewsDirectory bug (#1701)
+- #4f4a37 remove debugging from Model.
+- #1699 Fix install link in user guide
+- #1696 Fix page structure etc
+- #1695 Tidy up code blocks in the user guide
\ No newline at end of file
diff --git a/user_guide_src/source/cli/cli.rst b/user_guide_src/source/cli/cli.rst
index f99ff92d7a..ec09ceda4d 100644
--- a/user_guide_src/source/cli/cli.rst
+++ b/user_guide_src/source/cli/cli.rst
@@ -38,7 +38,8 @@ Let's create a simple controller so you can see it in action. Using your
text editor, create a file called Tools.php, and put the following code
in it::
- namespace App\Controller;
+ `_
diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py
index 50848430e7..d983078643 100644
--- a/user_guide_src/source/conf.py
+++ b/user_guide_src/source/conf.py
@@ -50,7 +50,7 @@ copyright = u'2014-2019 British Columbia Institute of Technology'
# The short X.Y version.
version = '4.0-dev'
# The full version, including alpha/beta/rc tags.
-release = '4.0.0-alpha.4'
+release = '4.0.0-beta.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/user_guide_src/source/database/connecting.rst b/user_guide_src/source/database/connecting.rst
index c7ae039309..35397ccdb5 100644
--- a/user_guide_src/source/database/connecting.rst
+++ b/user_guide_src/source/database/connecting.rst
@@ -14,6 +14,11 @@ If the above function does **not** contain any information in the first
parameter it will connect to the default group specified in your database config
file. For most people, this is the preferred method of use.
+A convenience method exists that is purely a wrapper around the above line
+and is provided for your convenience::
+
+ $db = db_connect();
+
Available Parameters
--------------------
@@ -61,6 +66,37 @@ group names you are connecting to.
| $db->setDatabase($database2_name);
+Connecting with Custom Settings
+===============================
+
+You can pass in an array of database settings instead of a group name to get
+a connection that uses your custom settings. The array passed in must be
+the same format as the groups are defined in the configuration file::
+
+ $custom = [
+ 'DSN' => '',
+ 'hostname' => 'localhost',
+ 'username' => '',
+ 'password' => '',
+ 'database' => '',
+ 'DBDriver' => 'MySQLi',
+ 'DBPrefix' => '',
+ 'pConnect' => false,
+ 'DBDebug' => (ENVIRONMENT !== 'production'),
+ 'cacheOn' => false,
+ 'cacheDir' => '',
+ 'charset' => 'utf8',
+ 'DBCollat' => 'utf8_general_ci',
+ 'swapPre' => '',
+ 'encrypt' => false,
+ 'compress' => false,
+ 'strictOn' => false,
+ 'failover' => [],
+ 'port' => 3306,
+ ];
+ $db = \Config\Database::connect($custom);
+
+
Reconnecting / Keeping the Connection Alive
===========================================
diff --git a/user_guide_src/source/database/examples.rst b/user_guide_src/source/database/examples.rst
index a5dde34455..3722524f96 100644
--- a/user_guide_src/source/database/examples.rst
+++ b/user_guide_src/source/database/examples.rst
@@ -87,7 +87,7 @@ Standard Insert
$sql = "INSERT INTO mytable (title, name) VALUES (".$db->escape($title).", ".$db->escape($name).")";
$db->query($sql);
- echo $db->getAffectedRows();
+ echo $db->affectedRows();
Query Builder Query
===================
diff --git a/user_guide_src/source/database/metadata.rst b/user_guide_src/source/database/metadata.rst
index 9b71c0c9eb..2efd41b880 100644
--- a/user_guide_src/source/database/metadata.rst
+++ b/user_guide_src/source/database/metadata.rst
@@ -134,4 +134,19 @@ List the Indexes in a Table
**$db->getIndexData()**
-please write this, someone...
+Returns an array of objects containing index information.
+
+Usage example::
+
+ $keys = $db->getIndexData('table_name');
+
+ foreach ($keys as $key)
+ {
+ echo $key->name;
+ echo $key->type;
+ echo $key->fields; // array of field names
+ }
+
+The key types may be unique to the database you are using.
+For instance, MySQL will return one of primary, fulltext, spatial, index or unique
+for each key associated with a table.
diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst
index 4b8cff8846..e8188175ee 100644
--- a/user_guide_src/source/database/transactions.rst
+++ b/user_guide_src/source/database/transactions.rst
@@ -75,16 +75,15 @@ debugging is turned off, you can manage your own errors like this::
// generate an error... or use the log_message() function to log your error
}
-Enabling Transactions
-=====================
+Disabling Transactions
+======================
-Transactions are enabled automatically the moment you use
-$this->db->transStart(). If you would like to disable transactions you
+Transactions are enabled by default. If you would like to disable transactions you
can do so using $this->db->transOff()::
$this->db->transOff();
- $this->db->trans_Start();
+ $this->db->transStart();
$this->db->query('AN SQL QUERY...');
$this->db->transComplete();
diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst
index b894153558..ca6ff543c2 100644
--- a/user_guide_src/source/dbmgmt/forge.rst
+++ b/user_guide_src/source/dbmgmt/forge.rst
@@ -188,9 +188,6 @@ and unique keys with specific methods::
$forge->addPrimaryKey('blog_id');
// gives PRIMARY KEY `blog_id` (`blog_id`)
-Foreign Keys help to enforce relationships and actions across your tables. For tables that support Foreign Keys,
-you may add them directly in forge::
-
$forge->addUniqueKey(['blog_id', 'uri']);
// gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`)
@@ -198,7 +195,8 @@ you may add them directly in forge::
Adding Foreign Keys
===================
-::
+Foreign Keys help to enforce relationships and actions across your tables. For tables that support Foreign Keys,
+you may add them directly in forge::
$forge->addForeignKey('users_id','users','id');
// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`)
diff --git a/user_guide_src/source/dbmgmt/index.rst b/user_guide_src/source/dbmgmt/index.rst
index ef9e94e5b2..2868c0869d 100644
--- a/user_guide_src/source/dbmgmt/index.rst
+++ b/user_guide_src/source/dbmgmt/index.rst
@@ -2,7 +2,7 @@
Managing Databases
##################
-CodeIgniter comes with tools to restructure or sees your database.
+CodeIgniter comes with tools to restructure or seed your database.
.. toctree::
:titlesonly:
diff --git a/user_guide_src/source/dbmgmt/migration.rst b/user_guide_src/source/dbmgmt/migration.rst
index 1bb28167ea..96346fb369 100644
--- a/user_guide_src/source/dbmgmt/migration.rst
+++ b/user_guide_src/source/dbmgmt/migration.rst
@@ -115,14 +115,16 @@ another database is used for mission critical data. You can ensure that migratio
against the proper group by setting the ``$DBGroup`` property on your migration. This name must
match the name of the database group exactly::
- class Migration_Add_blog extends \CodeIgniter\Database\Migration
- {
- protected $DBGroup = 'alternate_db_group';
+ ` that are available from the command line to help
you work with migrations. These tools are not required to use migrations but might make things easier for those of you
@@ -184,7 +186,7 @@ that wish to use them. The tools primarily provide access to the same methods th
Migrates all database groups to the latest available migrations::
-> php spark migrate:latest
+ > php spark migrate:latest
You can use (latest) with the following options:
@@ -194,14 +196,14 @@ You can use (latest) with the following options:
This example will migrate Blog namespace to latest::
-> php spark migrate:latest -g test -n Blog
+ > php spark migrate:latest -g test -n Blog
**current**
Migrates the (App) namespace to match the version set in ``$currentVersion``. This will migrate both
up and down as needed to match the specified version::
- > php spark migrate:current
+ > php spark migrate:current
You can use (current) with the following options:
@@ -214,7 +216,7 @@ for the version. ::
// Asks you for the version...
> php spark migrate:version
- > Version:
+ Version:
// Sequential
> php spark migrate:version 007
diff --git a/user_guide_src/source/dbmgmt/seeds.rst b/user_guide_src/source/dbmgmt/seeds.rst
index 5f0cb0450d..150dd91649 100644
--- a/user_guide_src/source/dbmgmt/seeds.rst
+++ b/user_guide_src/source/dbmgmt/seeds.rst
@@ -13,7 +13,8 @@ connection and the forge through ``$this->db`` and ``$this->forge``, respectivel
stored within the **app/Database/Seeds** directory. The name of the file must match the name of the class.
::
- // app/Database/Seeds/SimpleSeeder.php
+ protocol;
- $mailpath = $config->mailpath;
+ $pageSize = $pager->perPage;
If no namespace is provided, it will look for the files in all available namespaces that have
been defined, as well as **/app/Config/**. All of the configuration files
@@ -51,7 +50,8 @@ If you need to create a new configuration file you would create a new file at yo
**/app/Config** by default. Then create the class and fill it with public properties that
represent your settings::
- namespace Config;
+ `
-that CodeIgniter uses. While any code can use the PSR4 autoloader and namespaces, the only way to take full advantage of
+that CodeIgniter uses. While any code can use the PSR4 autoloader and namespaces, the primary way to take full advantage of
modules is to namespace your code and add it to **app/Config/Autoload.php**, in the ``psr4`` section.
For example, let's say we want to keep a simple blog module that we can re-use between applications. We might create
@@ -96,6 +96,17 @@ Specify Discovery Items
With the **$activeExplorers** option, you can specify which items are automatically discovered. If the item is not
present, then no auto-discovery will happen for that item, but the others in the array will still be discovered.
+Discovery and Composer
+======================
+
+Packages that were installed via Composer will also be discovered by default. This only requires that the namespace
+that Composer knows about is a PSR4 namespace. PSR0 namespaces will not be detected.
+
+If you do not want all of Composer's known directories to be scanned when locating files, you can turn this off
+by editing the ``$discoverInComposer`` variable in ``Config\Modules.php``::
+
+ public $discoverInComposer = false;
+
==================
Working With Files
==================
diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst
index 89091d0854..a06489d5d9 100644
--- a/user_guide_src/source/helpers/form_helper.rst
+++ b/user_guide_src/source/helpers/form_helper.rst
@@ -90,6 +90,15 @@ The following functions are available:
The above examples would create a form similar to this::